GoodERP
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

441 lignes
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. for l in v.line_in_ids:
  123. vl['val'].append([l.goods_id.name,l.goods_qty])
  124. for l in v.line_out_ids:
  125. vl['val'].append([l.goods_id.name,l.goods_qty])
  126. v.details = v.company_id._get_html_table(vl)
  127. def scan_barcode_move_line_operation(self, line, conversion):
  128. """
  129. 在原移库明细行中更新数量和辅助数量,不创建新行
  130. :return:
  131. """
  132. line.goods_qty += 1
  133. line.goods_uos_qty = line.goods_qty / conversion
  134. return True
  135. def scan_barcode_inventory_line_operation(self, line, conversion):
  136. '''盘点单明细行数量增加'''
  137. line.inventory_qty += 1
  138. line.inventory_uos_qty = line.inventory_qty / conversion
  139. line.difference_qty += 1
  140. line.difference_uos_qty = line.difference_qty / conversion
  141. return True
  142. def scan_barcode_move_in_out_operation(self, move, att, conversion, goods, val):
  143. """
  144. 对仓库各种移库单据上扫码的统一处理
  145. :return: 是否创建新的明细行
  146. """
  147. create_line = False
  148. loop_field = 'line_in_ids' if val['type'] == 'in' else 'line_out_ids'
  149. for line in move[loop_field]:
  150. line.cost_unit = (line.goods_id.price if val['type'] in ['out', 'internal']
  151. else line.goods_id.cost) # 其他出入库单 、内部调拨单
  152. line.price_taxed = (line.goods_id.price if val['type'] == 'out'
  153. else line.goods_id.cost) # 采购或销售单据
  154. # 如果商品属性或商品上存在条码,且明细行上已经存在该商品,则数量累加
  155. if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
  156. create_line = self.scan_barcode_move_line_operation(
  157. line, conversion)
  158. return create_line
  159. def scan_barcode_inventory_operation(self, move, att, conversion, goods, val):
  160. '''盘点单扫码操作'''
  161. create_line = False
  162. for line in move.line_ids:
  163. # 如果商品属性上存在条码 或 商品上存在条码
  164. if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
  165. create_line = self.scan_barcode_inventory_line_operation(
  166. line, conversion)
  167. return create_line
  168. def scan_barcode_each_model_operation(self, model_name, order_id, att, goods, conversion):
  169. val = {}
  170. create_line = False # 是否创建新的明细行
  171. order = self.env[model_name].browse(order_id)
  172. if model_name in ['wh.out', 'wh.in', 'wh.internal']:
  173. move = order.move_id
  174. # 在其他出库单上扫描条码
  175. if model_name == 'wh.out':
  176. val['type'] = 'out'
  177. # 在其他入库单上扫描条码
  178. if model_name == 'wh.in':
  179. val['type'] = 'in'
  180. # 销售出入库单的二维码
  181. if model_name == 'sell.delivery':
  182. move = order.sell_move_id
  183. val['type'] = order.is_return and 'in' or 'out'
  184. # 采购出入库单的二维码
  185. if model_name == 'buy.receipt':
  186. move = order.buy_move_id
  187. val['type'] = order.is_return and 'out' or 'in'
  188. # 调拔单的扫描条码
  189. if model_name == 'wh.internal':
  190. val['type'] = 'internal'
  191. if model_name != 'wh.inventory':
  192. create_line = self.scan_barcode_move_in_out_operation(
  193. move, att, conversion, goods, val)
  194. # 盘点单的扫码
  195. if model_name == 'wh.inventory':
  196. move = order
  197. create_line = self.scan_barcode_inventory_operation(
  198. move, att, conversion, goods, val)
  199. return move, create_line, val
  200. def check_goods_qty(self, goods, attribute, warehouse):
  201. '''SQL来取指定商品,属性,仓库,的当前剩余数量'''
  202. if attribute:
  203. change_conditions = "AND line.attribute_id = %s" % attribute.id
  204. elif goods:
  205. change_conditions = "AND line.goods_id = %s" % goods.id
  206. else:
  207. change_conditions = "AND 1 = 0"
  208. self.env.cr.execute('''
  209. SELECT sum(line.qty_remaining) as qty
  210. FROM wh_move_line line
  211. WHERE line.warehouse_dest_id = %s
  212. AND line.state = 'done'
  213. %s
  214. ''' % (warehouse.id, change_conditions,))
  215. return self.env.cr.fetchone()
  216. def prepare_move_line_data(self, att, val, goods, move):
  217. """
  218. 准备移库单明细数据
  219. :return: 字典
  220. """
  221. # 若传入的商品属性 att 上条码存在则取属性对应的商品,否则取传入的商品 goods
  222. goods = att and att.goods_id or goods
  223. goods_id = goods.id
  224. uos_id = goods.uos_id.id
  225. uom_id = goods.uom_id.id
  226. tax_rate = goods.tax_rate
  227. attribute_id = att and att.id or False
  228. conversion = goods.conversion
  229. # 采购入库取成本价,销售退货取销售价;采购退货取成本价,销售发货取销售价
  230. price_taxed = move._name == 'buy.receipt' and goods.cost or goods.price
  231. cost_unit = val.get('type') != 'in' and 0 or goods.cost
  232. val.update({
  233. 'goods_id': goods_id,
  234. 'attribute_id': attribute_id,
  235. 'warehouse_id': move.warehouse_id.id,
  236. 'uos_id': uos_id,
  237. 'uom_id': uom_id,
  238. })
  239. if move._name != 'wh.inventory':
  240. val.update({
  241. 'warehouse_dest_id': move.warehouse_dest_id.id,
  242. 'goods_uos_qty': 1.0 / conversion,
  243. 'goods_qty': 1,
  244. 'price_taxed': price_taxed,
  245. 'tax_rate': tax_rate,
  246. 'cost_unit': cost_unit,
  247. 'move_id': move.id})
  248. else:
  249. val.update({
  250. 'inventory_uos_qty': 1.0 / conversion,
  251. 'inventory_qty': 1,
  252. 'real_uos_qty': 0,
  253. 'real_qty': 0,
  254. 'difference_uos_qty': 1.0 / conversion,
  255. 'difference_qty': 1,
  256. 'inventory_id': move.id})
  257. return val
  258. @api.model
  259. def check_barcode(self, model_name, order_id, att, goods):
  260. pass
  261. @api.model
  262. def scan_barcode(self, model_name, barcode, order_id):
  263. """
  264. 扫描条码
  265. :param model_name: 模型名
  266. :param barcode: 条码
  267. :param order_id: 单据id
  268. :return:
  269. """
  270. att = self.env['attribute'].search([('ean', '=', barcode)])
  271. goods = self.env['goods'].search([('barcode', '=', barcode)])
  272. line_model = (model_name == 'wh.inventory' and 'wh.inventory.line'
  273. or 'wh.move.line')
  274. if not att and not goods:
  275. raise UserError('条码为 %s 的商品不存在' % (barcode))
  276. else:
  277. self.check_barcode(model_name, order_id, att, goods)
  278. conversion = att and att.goods_id.conversion or goods.conversion
  279. move, create_line, val = self.scan_barcode_each_model_operation(
  280. model_name, order_id, att, goods, conversion)
  281. if not create_line:
  282. self.env[line_model].create(
  283. self.prepare_move_line_data(att, val, goods, move))
  284. def check_qc_result(self):
  285. """
  286. 检验质检报告是否上传
  287. :return:
  288. """
  289. qc_rule = self.env['qc.rule'].search([
  290. ('move_type', '=', self.origin),
  291. ('warehouse_id', '=', self.warehouse_id.id),
  292. ('warehouse_dest_id', '=', self.warehouse_dest_id.id)])
  293. if qc_rule and not self.qc_result:
  294. raise UserError('请先上传质检报告')
  295. def prev_approve_order(self):
  296. """
  297. 确认单据之前所做的处理
  298. :return:
  299. """
  300. for order in self:
  301. if not order.line_out_ids and not order.line_in_ids:
  302. raise UserError('单据的明细行不可以为空')
  303. order.check_qc_result()
  304. def approve_order(self):
  305. """
  306. 确认单据
  307. :return:
  308. """
  309. for order in self:
  310. order.prev_approve_order()
  311. order.line_out_ids.action_done()
  312. order.line_in_ids.action_done()
  313. # 每次移库完成,清空库位上商品数量为0的商品和属性(不合逻辑的数据)
  314. for loc in self.env['location'].search([('save_qty', '=', 0),
  315. ('goods_id', '!=', False)
  316. ]):
  317. if not loc.current_qty:
  318. continue # pragma: no cover
  319. return self.write({
  320. 'approve_uid': self.env.uid,
  321. 'approve_date': fields.Datetime.now(self),
  322. })
  323. def prev_cancel_approved_order(self):
  324. pass
  325. def cancel_approved_order(self):
  326. """
  327. 撤销确认单据
  328. :return:
  329. """
  330. for order in self:
  331. order.prev_cancel_approved_order()
  332. order.line_out_ids.action_draft()
  333. order.line_in_ids.action_draft()
  334. return self.write({
  335. 'approve_uid': False,
  336. 'approve_date': False,
  337. })
  338. def write(self, vals):
  339. """
  340. 作废明细行
  341. """
  342. if vals.get('state', False) == 'cancel':
  343. for order in self:
  344. order.line_out_ids.action_cancel()
  345. order.line_in_ids.action_cancel()
  346. return super(WhMove, self).write(vals)
  347. def create_zero_wh_in(self, wh_in, model_name):
  348. """
  349. 创建一个缺货向导
  350. :param wh_in: 单据实例
  351. :param model_name: 单据模型
  352. :return:
  353. """
  354. all_line_message = ""
  355. today = fields.Datetime.now()
  356. line_in_ids = []
  357. goods_list = []
  358. for line in wh_in.line_out_ids:
  359. if line.goods_id.no_stock:
  360. continue
  361. result = self.check_goods_qty(
  362. line.goods_id, line.attribute_id, wh_in.warehouse_id)
  363. result = result[0] or 0
  364. if line.goods_qty > result and not line.lot_id and not self.env.context.get('wh_in_line_ids'):
  365. # 在销售出库时如果临时缺货,自动生成一张盘盈入库单
  366. if (line.goods_id.id, line.attribute_id.id) in goods_list:
  367. continue
  368. goods_list.append((line.goods_id.id, line.attribute_id.id))
  369. all_line_message += '商品 %s ' % line.goods_id.name
  370. if line.attribute_id:
  371. all_line_message += ' 型号%s' % line.attribute_id.name
  372. line_in_ids.append((0, 0, {
  373. 'goods_id': line.goods_id.id,
  374. 'attribute_id': line.attribute_id.id,
  375. 'goods_uos_qty': 0,
  376. 'uos_id': line.uos_id.id,
  377. 'goods_qty': 0,
  378. 'uom_id': line.uom_id.id,
  379. 'cost_unit': line.goods_id.cost,
  380. 'state': 'done',
  381. 'date': today}))
  382. all_line_message += u" 当前库存量不足,继续出售请点击确定,并及时盘点库存\n"
  383. if line.goods_qty <= 0 or line.price_taxed < 0:
  384. raise UserError('商品 %s 的数量和含税单价不能小于0。' % line.goods_id.name)
  385. if line_in_ids:
  386. vals = {
  387. 'type': 'inventory',
  388. 'warehouse_id': self.env.ref('warehouse.warehouse_inventory').id,
  389. 'warehouse_dest_id': wh_in.warehouse_id.id,
  390. 'state': 'done',
  391. 'date': today,
  392. 'line_in_ids': line_in_ids}
  393. return self.env[model_name].with_context(
  394. {'active_model': model_name}
  395. ).open_dialog('goods_inventory', {
  396. 'message': all_line_message,
  397. 'args': [vals],
  398. })
上海开阖软件有限公司 沪ICP备12045867号-1