GoodERP
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

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