GoodERP
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

450 行
18KB

  1. from odoo import models, fields, api
  2. from odoo.exceptions import UserError
  3. class WhMove(models.Model):
  4. _name = 'wh.move'
  5. _description = '移库单'
  6. MOVE_STATE = [
  7. ('draft', '草稿'),
  8. ('done', '已完成'),
  9. ('cancel', '已作废'),]
  10. @api.depends('line_out_ids', 'line_in_ids')
  11. def _compute_total_qty(self):
  12. for wm in self:
  13. goods_total = 0
  14. if wm.line_in_ids:
  15. # 入库商品总数
  16. goods_total = sum(line.goods_qty for line in wm.line_in_ids)
  17. elif wm.line_out_ids:
  18. # 出库商品总数
  19. goods_total = sum(line.goods_qty for line in wm.line_out_ids)
  20. wm.total_qty = goods_total
  21. @api.model
  22. def _get_default_warehouse_impl(self):
  23. if self.env.context.get('warehouse_type', 'stock'):
  24. return self.env['warehouse'].get_warehouse_by_type(
  25. self.env.context.get('warehouse_type', 'stock'))
  26. @api.model
  27. def _get_default_warehouse_dest_impl(self):
  28. if self.env.context.get('warehouse_dest_type', 'stock'):
  29. return self.env['warehouse'].get_warehouse_by_type(
  30. self.env.context.get('warehouse_dest_type', 'stock'))
  31. @api.model
  32. def _get_default_warehouse(self):
  33. '''获取调出仓库'''
  34. return self._get_default_warehouse_impl()
  35. @api.model
  36. def _get_default_warehouse_dest(self):
  37. '''获取调入仓库'''
  38. return self._get_default_warehouse_dest_impl()
  39. origin = fields.Char('移库类型', required=True,
  40. help='移库类型')
  41. name = fields.Char('单据编号', copy=False, default='/',
  42. help='单据编号,创建时会自动生成')
  43. ref = fields.Char('外部单号')
  44. state = fields.Selection(MOVE_STATE, '状态', copy=False, default='draft',
  45. index=True,
  46. tracking=True,
  47. help='移库单状态标识,新建时状态为草稿;确认后状态为已确认')
  48. partner_id = fields.Many2one('partner', '业务伙伴', ondelete='restrict',
  49. help='该单据对应的业务伙伴')
  50. date = fields.Date('单据日期', required=True, copy=False, default=fields.Date.context_today,
  51. help='单据创建日期,默认为当前天')
  52. warehouse_id = fields.Many2one('warehouse', '调出仓库',
  53. ondelete='restrict',
  54. required=True,
  55. readonly=True,
  56. domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
  57. states={'draft': [('readonly', False)]},
  58. default=_get_default_warehouse,
  59. help='移库单的来源仓库')
  60. warehouse_dest_id = fields.Many2one('warehouse', '调入仓库',
  61. ondelete='restrict',
  62. required=True,
  63. readonly=False,
  64. domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
  65. states={'done': [('readonly', True)]},
  66. default=_get_default_warehouse_dest,
  67. help='移库单的目的仓库')
  68. approve_uid = fields.Many2one('res.users', '确认人',
  69. copy=False, ondelete='restrict',
  70. help='移库单的确认人')
  71. approve_date = fields.Datetime('确认日期', copy=False)
  72. line_ids = fields.One2many('wh.move.line', 'move_id', '出入库明细', copy=False)
  73. line_out_ids = fields.One2many('wh.move.line', 'move_id', '出库明细',
  74. domain=[
  75. ('type', 'in', ['out', 'internal'])],
  76. copy=True,
  77. help='出库类型的移库单对应的出库明细')
  78. line_in_ids = fields.One2many('wh.move.line', 'move_id', '入库明细',
  79. domain=[('type', '=', 'in')],
  80. context={'type': 'in'}, copy=True,
  81. help='入库类型的移库单对应的入库明细')
  82. in_goods_id = fields.Many2one('goods', string='入库商品',
  83. related='line_in_ids.goods_id')
  84. out_goods_id = fields.Many2one('goods', string='出库商品',
  85. related='line_out_ids.goods_id')
  86. auxiliary_id = fields.Many2one(
  87. 'auxiliary.financing', '辅助核算', help='辅助核算是对账务处理的一种补充,即实现更广泛的账务处理,\
  88. 以适应企业管理和决策的需要.辅助核算一般通过核算项目来实现', ondelete='restrict')
  89. note = fields.Text('备注',
  90. copy=False,
  91. help='可以为该单据添加一些需要的标识信息')
  92. total_qty = fields.Float('商品总数', compute=_compute_total_qty,
  93. digits='Quantity', store=True,
  94. help='该移库单的入/出库明细行包含的商品总数')
  95. user_id = fields.Many2one(
  96. 'res.users',
  97. '经办人',
  98. ondelete='restrict',
  99. tracking=True,
  100. states={'done': [('readonly', True)]},
  101. default=lambda self: self.env.user,
  102. help='单据经办人'
  103. )
  104. express_type = fields.Char(string='承运商')
  105. express_code = fields.Char('快递单号', copy=False)
  106. company_id = fields.Many2one(
  107. 'res.company',
  108. string='公司',
  109. change_default=True,
  110. default=lambda self: self.env.company)
  111. qc_result = fields.Binary('质检报告',
  112. help='点击上传质检报告')
  113. finance_category_id = fields.Many2one(
  114. 'core.category',
  115. string='收发类别',
  116. ondelete='restrict',
  117. states={'done': [('readonly', True)]},
  118. domain=[('type', '=', 'finance'), ('note', '!=', '由系统创建')],
  119. context={'type': 'finance'},
  120. help='生成凭证时从此字段上取商品科目的对方科目',
  121. )
  122. details = fields.Html('明细',compute='_compute_details')
  123. @api.depends('line_in_ids', 'line_out_ids')
  124. def _compute_details(self):
  125. for v in self:
  126. vl = {'col':[],'val':[]}
  127. vl['col'] = ['商品','数量']
  128. for l in v.line_in_ids:
  129. vl['val'].append([l.goods_id.name,l.goods_qty])
  130. for l in v.line_out_ids:
  131. vl['val'].append([l.goods_id.name,l.goods_qty])
  132. v.details = v.company_id._get_html_table(vl)
  133. def scan_barcode_move_line_operation(self, line, conversion):
  134. """
  135. 在原移库明细行中更新数量和辅助数量,不创建新行
  136. :return:
  137. """
  138. line.goods_qty += 1
  139. line.goods_uos_qty = line.goods_qty / conversion
  140. return True
  141. def scan_barcode_inventory_line_operation(self, line, conversion):
  142. '''盘点单明细行数量增加'''
  143. line.inventory_qty += 1
  144. line.inventory_uos_qty = line.inventory_qty / conversion
  145. line.difference_qty += 1
  146. line.difference_uos_qty = line.difference_qty / conversion
  147. return True
  148. def scan_barcode_move_in_out_operation(self, move, att, conversion, goods, val):
  149. """
  150. 对仓库各种移库单据上扫码的统一处理
  151. :return: 是否创建新的明细行
  152. """
  153. create_line = False
  154. loop_field = 'line_in_ids' if val['type'] == 'in' else 'line_out_ids'
  155. for line in move[loop_field]:
  156. line.cost_unit = (line.goods_id.price if val['type'] in ['out', 'internal']
  157. else line.goods_id.cost) # 其他出入库单 、内部调拨单
  158. line.price_taxed = (line.goods_id.price if val['type'] == 'out'
  159. else line.goods_id.cost) # 采购或销售单据
  160. # 如果商品属性或商品上存在条码,且明细行上已经存在该商品,则数量累加
  161. if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
  162. create_line = self.scan_barcode_move_line_operation(
  163. line, conversion)
  164. return create_line
  165. def scan_barcode_inventory_operation(self, move, att, conversion, goods, val):
  166. '''盘点单扫码操作'''
  167. create_line = False
  168. for line in move.line_ids:
  169. # 如果商品属性上存在条码 或 商品上存在条码
  170. if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
  171. create_line = self.scan_barcode_inventory_line_operation(
  172. line, conversion)
  173. return create_line
  174. def scan_barcode_each_model_operation(self, model_name, order_id, att, goods, conversion):
  175. val = {}
  176. create_line = False # 是否创建新的明细行
  177. order = self.env[model_name].browse(order_id)
  178. if model_name in ['wh.out', 'wh.in', 'wh.internal']:
  179. move = order.move_id
  180. # 在其他出库单上扫描条码
  181. if model_name == 'wh.out':
  182. val['type'] = 'out'
  183. # 在其他入库单上扫描条码
  184. if model_name == 'wh.in':
  185. val['type'] = 'in'
  186. # 销售出入库单的二维码
  187. if model_name == 'sell.delivery':
  188. move = order.sell_move_id
  189. val['type'] = order.is_return and 'in' or 'out'
  190. # 采购出入库单的二维码
  191. if model_name == 'buy.receipt':
  192. move = order.buy_move_id
  193. val['type'] = order.is_return and 'out' or 'in'
  194. # 调拔单的扫描条码
  195. if model_name == 'wh.internal':
  196. val['type'] = 'internal'
  197. if model_name != 'wh.inventory':
  198. create_line = self.scan_barcode_move_in_out_operation(
  199. move, att, conversion, goods, val)
  200. # 盘点单的扫码
  201. if model_name == 'wh.inventory':
  202. move = order
  203. create_line = self.scan_barcode_inventory_operation(
  204. move, att, conversion, goods, val)
  205. return move, create_line, val
  206. def check_goods_qty(self, goods, attribute, warehouse):
  207. '''SQL来取指定商品,属性,仓库,的当前剩余数量'''
  208. if attribute:
  209. change_conditions = "AND line.attribute_id = %s" % attribute.id
  210. elif goods:
  211. change_conditions = "AND line.goods_id = %s" % goods.id
  212. else:
  213. change_conditions = "AND 1 = 0"
  214. self.env.cr.execute('''
  215. SELECT sum(line.qty_remaining) as qty
  216. FROM wh_move_line line
  217. WHERE line.warehouse_dest_id = %s
  218. AND line.state = 'done'
  219. %s
  220. ''' % (warehouse.id, change_conditions,))
  221. return self.env.cr.fetchone()
  222. def prepare_move_line_data(self, att, val, goods, move):
  223. """
  224. 准备移库单明细数据
  225. :return: 字典
  226. """
  227. # 若传入的商品属性 att 上条码存在则取属性对应的商品,否则取传入的商品 goods
  228. goods = att and att.goods_id or goods
  229. goods_id = goods.id
  230. uos_id = goods.uos_id.id
  231. uom_id = goods.uom_id.id
  232. tax_rate = goods.tax_rate
  233. attribute_id = att and att.id or False
  234. conversion = goods.conversion
  235. # 采购入库取成本价,销售退货取销售价;采购退货取成本价,销售发货取销售价
  236. price_taxed = move._name == 'buy.receipt' and goods.cost or goods.price
  237. cost_unit = val.get('type') != 'in' and 0 or goods.cost
  238. val.update({
  239. 'goods_id': goods_id,
  240. 'attribute_id': attribute_id,
  241. 'warehouse_id': move.warehouse_id.id,
  242. 'uos_id': uos_id,
  243. 'uom_id': uom_id,
  244. })
  245. if move._name != 'wh.inventory':
  246. val.update({
  247. 'warehouse_dest_id': move.warehouse_dest_id.id,
  248. 'goods_uos_qty': 1.0 / conversion,
  249. 'goods_qty': 1,
  250. 'price_taxed': price_taxed,
  251. 'tax_rate': tax_rate,
  252. 'cost_unit': cost_unit,
  253. 'move_id': move.id})
  254. else:
  255. val.update({
  256. 'inventory_uos_qty': 1.0 / conversion,
  257. 'inventory_qty': 1,
  258. 'real_uos_qty': 0,
  259. 'real_qty': 0,
  260. 'difference_uos_qty': 1.0 / conversion,
  261. 'difference_qty': 1,
  262. 'inventory_id': move.id})
  263. return val
  264. @api.model
  265. def check_barcode(self, model_name, order_id, att, goods):
  266. pass
  267. @api.model
  268. def scan_barcode(self, model_name, barcode, order_id):
  269. """
  270. 扫描条码
  271. :param model_name: 模型名
  272. :param barcode: 条码
  273. :param order_id: 单据id
  274. :return:
  275. """
  276. att = self.env['attribute'].search([('ean', '=', barcode)])
  277. goods = self.env['goods'].search([('barcode', '=', barcode)])
  278. line_model = (model_name == 'wh.inventory' and 'wh.inventory.line'
  279. or 'wh.move.line')
  280. if not att and not goods:
  281. raise UserError('条码为 %s 的商品不存在' % (barcode))
  282. else:
  283. self.check_barcode(model_name, order_id, att, goods)
  284. conversion = att and att.goods_id.conversion or goods.conversion
  285. move, create_line, val = self.scan_barcode_each_model_operation(
  286. model_name, order_id, att, goods, conversion)
  287. if not create_line:
  288. self.env[line_model].create(
  289. self.prepare_move_line_data(att, val, goods, move))
  290. def check_qc_result(self):
  291. """
  292. 检验质检报告是否上传
  293. :return:
  294. """
  295. qc_rule = self.env['qc.rule'].search([
  296. ('move_type', '=', self.origin),
  297. ('warehouse_id', '=', self.warehouse_id.id),
  298. ('warehouse_dest_id', '=', self.warehouse_dest_id.id)])
  299. if qc_rule and not self.qc_result:
  300. raise UserError('请先上传质检报告')
  301. def prev_approve_order(self):
  302. """
  303. 确认单据之前所做的处理
  304. :return:
  305. """
  306. for order in self:
  307. if not order.line_out_ids and not order.line_in_ids:
  308. raise UserError('单据的明细行不可以为空')
  309. order.check_qc_result()
  310. def approve_order(self):
  311. """
  312. 确认单据
  313. :return:
  314. """
  315. for order in self:
  316. order.prev_approve_order()
  317. order.line_out_ids.action_done()
  318. order.line_in_ids.action_done()
  319. # 每次移库完成,清空库位上商品数量为0的商品和属性(不合逻辑的数据)
  320. for loc in self.env['location'].search([('save_qty', '=', 0),
  321. ('goods_id', '!=', False)
  322. ]):
  323. if not loc.current_qty:
  324. continue # pragma: no cover
  325. return self.write({
  326. 'approve_uid': self.env.uid,
  327. 'approve_date': fields.Datetime.now(self),
  328. })
  329. def prev_cancel_approved_order(self):
  330. pass
  331. def cancel_approved_order(self):
  332. """
  333. 撤销确认单据
  334. :return:
  335. """
  336. for order in self:
  337. order.prev_cancel_approved_order()
  338. order.line_out_ids.action_draft()
  339. order.line_in_ids.action_draft()
  340. return self.write({
  341. 'approve_uid': False,
  342. 'approve_date': False,
  343. })
  344. def write(self, vals):
  345. """
  346. 作废明细行
  347. """
  348. if vals.get('state', False) == 'cancel':
  349. for order in self:
  350. order.line_out_ids.action_cancel()
  351. order.line_in_ids.action_cancel()
  352. return super(WhMove, self).write(vals)
  353. def create_zero_wh_in(self, wh_in, model_name):
  354. """
  355. 创建一个缺货向导
  356. :param wh_in: 单据实例
  357. :param model_name: 单据模型
  358. :return:
  359. """
  360. all_line_message = ""
  361. today = fields.Datetime.now()
  362. line_in_ids = []
  363. goods_list = []
  364. for line in wh_in.line_out_ids:
  365. if line.goods_id.no_stock:
  366. continue
  367. result = self.check_goods_qty(
  368. line.goods_id, line.attribute_id, wh_in.warehouse_id)
  369. result = result[0] or 0
  370. if line.goods_qty > result and not line.lot_id and not self.env.context.get('wh_in_line_ids'):
  371. # 在销售出库时如果临时缺货,自动生成一张盘盈入库单
  372. if (line.goods_id.id, line.attribute_id.id) in goods_list:
  373. continue
  374. goods_list.append((line.goods_id.id, line.attribute_id.id))
  375. all_line_message += '商品 %s ' % line.goods_id.name
  376. if line.attribute_id:
  377. all_line_message += ' 型号%s' % line.attribute_id.name
  378. line_in_ids.append((0, 0, {
  379. 'goods_id': line.goods_id.id,
  380. 'attribute_id': line.attribute_id.id,
  381. 'goods_uos_qty': 0,
  382. 'uos_id': line.uos_id.id,
  383. 'goods_qty': 0,
  384. 'uom_id': line.uom_id.id,
  385. 'cost_unit': line.goods_id.cost,
  386. 'state': 'done',
  387. 'date': today}))
  388. all_line_message += u" 当前库存量不足,继续出售请点击确定,并及时盘点库存\n"
  389. if line.goods_qty <= 0 or line.price_taxed < 0:
  390. raise UserError('商品 %s 的数量和含税单价不能小于0。' % line.goods_id.name)
  391. if line_in_ids:
  392. vals = {
  393. 'type': 'inventory',
  394. 'warehouse_id': self.env.ref('warehouse.warehouse_inventory').id,
  395. 'warehouse_dest_id': wh_in.warehouse_id.id,
  396. 'state': 'done',
  397. 'date': today,
  398. 'line_in_ids': line_in_ids}
  399. return self.env[model_name].with_context(
  400. {'active_model':model_name}
  401. ).open_dialog('goods_inventory', {
  402. 'message': all_line_message,
  403. 'args': [vals],
  404. })
上海开阖软件有限公司 沪ICP备12045867号-1