GoodERP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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