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.

501 lines
19KB

  1. from .utils import create_name, safe_division
  2. from odoo.exceptions import UserError
  3. from odoo import models
  4. from odoo import _
  5. from odoo import fields
  6. from odoo import api
  7. from odoo.tools import float_compare, float_is_zero
  8. class WhInventory(models.Model):
  9. _name = 'wh.inventory'
  10. _description = '盘点单'
  11. _inherit = ['mail.thread']
  12. _order = 'date DESC, id DESC'
  13. INVENTORY_STATE = [
  14. ('draft', '草稿'),
  15. ('query', '查询中'),
  16. ('confirmed', '待确认盘盈盘亏'),
  17. ('done', '完成'),
  18. ]
  19. @api.model
  20. def _get_default_warehouse_impl(self):
  21. if self.env.context.get('warehouse_type', 'stock'):
  22. return self.env['warehouse'].get_warehouse_by_type(
  23. self.env.context.get('warehouse_type', 'stock'))
  24. @api.model
  25. def _get_default_warehouse(self):
  26. '''获取盘点仓库'''
  27. return self._get_default_warehouse_impl()
  28. date = fields.Date('日期', default=fields.Date.context_today,
  29. help='盘点单创建日期,默认为当前天')
  30. name = fields.Char('名称', copy=False, default='/',
  31. help='单据编号,创建时会自动生成')
  32. warehouse_id = fields.Many2one('warehouse', '仓库', required=True, default=_get_default_warehouse,
  33. help='盘点单盘点的仓库')
  34. goods = fields.Many2many('goods', string='商品',
  35. help='盘点单盘点的商品')
  36. out_id = fields.Many2one('wh.out', '盘亏单据', copy=False,
  37. help='盘亏生成的其他出库单单据')
  38. in_id = fields.Many2one('wh.in', '盘盈单据', copy=False,
  39. help='盘盈生成的其他入库单单据')
  40. state = fields.Selection(
  41. INVENTORY_STATE, '状态', copy=False, default='draft',
  42. index=True,
  43. tracking=True,
  44. help='盘点单状态,新建时状态为草稿;'
  45. '点击查询后为确认后状态为查询中;'
  46. '有盘亏盘盈时生成的其他出入库单没有确认时状态为待确认盘盈盘亏;'
  47. '盘亏盘盈生成的其他出入库单确认后状态为完成')
  48. line_ids = fields.One2many(
  49. 'wh.inventory.line', 'inventory_id', '明细', copy=False,
  50. help='盘点单的明细行')
  51. note = fields.Text('备注',
  52. help='可以为该单据添加一些需要的标识信息')
  53. company_id = fields.Many2one(
  54. 'res.company',
  55. string='公司',
  56. change_default=True,
  57. default=lambda self: self.env.company)
  58. def requery_inventory(self):
  59. self.delete_confirmed_wh()
  60. self.state = 'query'
  61. @api.model_create_multi
  62. @create_name
  63. def create(self, vals_list):
  64. return super(WhInventory, self).create(vals_list)
  65. def unlink(self):
  66. for inventory in self:
  67. inventory.delete_confirmed_wh()
  68. return super(WhInventory, self).unlink()
  69. def delete_confirmed_wh(self):
  70. for inventory in self:
  71. if inventory.state == 'confirmed':
  72. if (inventory.out_id and inventory.out_id.state == 'done') \
  73. or (inventory.in_id and inventory.in_id.state == 'done'):
  74. raise UserError('请先撤销掉相关的盘盈盘亏单据')
  75. else:
  76. inventory.out_id.unlink()
  77. inventory.in_id.unlink()
  78. return True
  79. def check_done(self):
  80. for inventory in self:
  81. if inventory.state == 'confirmed' and \
  82. (not inventory.out_id or inventory.out_id.state == 'done') and \
  83. (not inventory.in_id or inventory.in_id.state == 'done'):
  84. self.state = 'done'
  85. return True
  86. if inventory.state == 'done' and \
  87. (not inventory.out_id or inventory.out_id.state != 'done') and \
  88. (not inventory.in_id or inventory.in_id.state != 'done'):
  89. self.state = 'confirmed'
  90. return True
  91. return False
  92. def open_out(self):
  93. for inventory in self:
  94. return {
  95. 'type': 'ir.actions.act_window',
  96. 'res_model': 'wh.out',
  97. 'view_mode': 'form',
  98. 'res_id': inventory.out_id.id,
  99. }
  100. def open_in(self):
  101. for inventory in self:
  102. return {
  103. 'type': 'ir.actions.act_window',
  104. 'res_model': 'wh.in',
  105. 'view_mode': 'form',
  106. 'res_id': inventory.in_id.id,
  107. }
  108. def delete_line(self):
  109. self.line_ids.unlink()
  110. def create_losses_out(self, inventory, out_line):
  111. inventory_warehouse = self.env.ref('warehouse.warehouse_inventory')
  112. out_vals = {
  113. 'warehouse_dest_id': inventory_warehouse.id,
  114. 'warehouse_id': inventory.warehouse_id.id,
  115. 'type': 'inventory',
  116. 'line_out_ids': [],
  117. }
  118. for line in out_line:
  119. out_vals['line_out_ids'].append(
  120. [0, False, line.get_move_line(wh_type='out')])
  121. out_id = self.env['wh.out'].create(out_vals)
  122. inventory.out_id = out_id
  123. def create_overage_in(self, inventory, in_line):
  124. inventory_warehouse = self.env.ref('warehouse.warehouse_inventory')
  125. in_vals = {
  126. 'warehouse_dest_id': inventory.warehouse_id.id,
  127. 'warehouse_id': inventory_warehouse.id,
  128. 'type': 'inventory',
  129. 'line_in_ids': [],
  130. }
  131. for line in in_line:
  132. in_vals['line_in_ids'].append(
  133. [0, False, line.get_move_line(wh_type='in')])
  134. in_id = self.env['wh.in'].create(in_vals)
  135. inventory.in_id = in_id
  136. def generate_inventory(self):
  137. for inventory in self:
  138. if self.state in ['done', 'confirmed']:
  139. raise UserError('请不要重复点击生成盘点单据按钮')
  140. out_line, in_line = [], []
  141. for line in inventory.line_ids:
  142. if line.difference_qty < 0:
  143. out_line.append(line)
  144. elif line.difference_qty > 0:
  145. in_line.append(line)
  146. if out_line:
  147. self.create_losses_out(inventory, out_line)
  148. if in_line:
  149. self.create_overage_in(inventory, in_line)
  150. if len(out_line) + len(in_line) == 0:
  151. inventory.state = 'done'
  152. if out_line or in_line:
  153. inventory.state = 'confirmed'
  154. return True
  155. def get_line_detail(self):
  156. for inventory in self:
  157. sql_text = '''
  158. SELECT wh.id as warehouse_id,
  159. goods.id as goods_id,
  160. line.attribute_id as attribute_id,
  161. line.lot as lot,
  162. uom.id as uom_id,
  163. uos.id as uos_id,
  164. sum(line.qty_remaining) as qty,
  165. sum(line.uos_qty_remaining) as uos_qty
  166. FROM wh_move_line line
  167. LEFT JOIN goods goods ON line.goods_id = goods.id
  168. LEFT JOIN uom uom ON goods.uom_id = uom.id
  169. LEFT JOIN uom uos ON goods.uos_id = uos.id
  170. LEFT JOIN warehouse wh ON line.warehouse_dest_id = wh.id
  171. WHERE line.qty_remaining != 0
  172. AND wh.type = 'stock'
  173. AND line.state = 'done'
  174. %s
  175. GROUP BY
  176. wh.id, line.lot, line.attribute_id, goods.id, uom.id, uos.id
  177. ORDER BY
  178. goods.id, line.lot
  179. '''
  180. extra_text = ' AND wh.id = %s' % inventory.warehouse_id.id
  181. if inventory.goods:
  182. goods_ids = inventory.goods.ids
  183. goods_ids.append(0)
  184. extra_text += " AND goods.id IN {ids}".format(
  185. ids=tuple(set(goods_ids)))
  186. inventory.env.cr.execute(sql_text % extra_text)
  187. res = inventory.env.cr.dictfetchall()
  188. for line in res:
  189. # 盘点单查询的盘点数量不应该包含移库在途的 #1358
  190. for int_line in self.env['wh.move.line'].search(
  191. [('goods_id', '=', line['goods_id']),
  192. ('attribute_id', '=', line['attribute_id']),
  193. ('lot_id', '=', line['lot']),
  194. ('warehouse_id', '=', line['warehouse_id']),
  195. ('type', '=', 'internal'),
  196. ('state', '=', 'draft')]):
  197. line['qty'] -= int_line.goods_qty
  198. line['uos_qty'] -= int_line.goods_uos_qty
  199. if not line['qty']:
  200. res.remove(line)
  201. return res
  202. def query_inventory(self):
  203. line_obj = self.env['wh.inventory.line']
  204. for inventory in self:
  205. inventory.delete_line()
  206. line_ids = inventory.get_line_detail()
  207. for line in line_ids:
  208. line_obj.create_wh_inventory_line_by_data(inventory.id, line)
  209. if line_ids:
  210. inventory.state = 'query'
  211. return True
  212. class WhInventoryLine(models.Model):
  213. _name = 'wh.inventory.line'
  214. _description = '盘点单明细'
  215. LOT_TYPE = [
  216. ('out', '出库'),
  217. ('in', '入库'),
  218. ('nothing', '不做处理'),
  219. ]
  220. @api.depends('inventory_qty', 'real_qty', 'inventory_uos_qty', 'real_uos_qty')
  221. def _get_difference_qty(self):
  222. for line in self:
  223. line.difference_qty = line.inventory_qty - line.real_qty
  224. line.difference_uos_qty = line.inventory_uos_qty - line.real_uos_qty
  225. if float_is_zero(line.difference_qty, 2) and not float_is_zero(line.difference_uos_qty, 2):
  226. line.difference_qty = line.difference_uos_qty * line.goods_id.conversion
  227. if not float_is_zero(line.difference_qty, 2) and line.difference_uos_qty == 0:
  228. line.difference_uos_qty = line.difference_qty / line.goods_id.conversion
  229. @api.depends('inventory_uos_qty', 'real_uos_qty')
  230. def _get_difference_uos_qty(self):
  231. for line in self:
  232. line.difference_uos_qty = line.inventory_uos_qty - line.real_uos_qty
  233. inventory_id = fields.Many2one('wh.inventory', '盘点', ondelete='cascade',
  234. help='盘点单行对应的盘点单')
  235. warehouse_id = fields.Many2one('warehouse', '仓库', required=True, ondelete='restrict',
  236. help='盘点单行对应的仓库')
  237. goods_id = fields.Many2one('goods', '商品', required=True, ondelete='restrict',
  238. help='盘点单行对应的商品')
  239. attribute_id = fields.Many2one('attribute', '属性', ondelete='restrict',
  240. help='盘点单行对应的商品的属性')
  241. using_batch = fields.Boolean(
  242. related='goods_id.using_batch', string='批号管理',
  243. help='盘点单行对应的商品是否使用批号管理,是True否则False')
  244. force_batch_one = fields.Boolean(
  245. related='goods_id.force_batch_one', string='每批号数量为1',
  246. help='盘点单行对应的商品是否使用每批号数量为1,是True否则False')
  247. lot = fields.Char('批号',
  248. help='盘点单行对应的商品批号')
  249. new_lot = fields.Char('盘盈批号',
  250. help='盘点单行对应的商品盘盈批号')
  251. new_lot_id = fields.Many2one('wh.move.line', '盘亏批号',
  252. ondelete='restrict',
  253. help='盘点单行对应的商品盘亏批号')
  254. lot_type = fields.Selection(LOT_TYPE, '批号类型', default='nothing',
  255. help='批号类型: 出库、入库、不做处理')
  256. uom_id = fields.Many2one('uom', '单位', ondelete='restrict',
  257. help='盘点单行对应的商品的计量单位')
  258. uos_id = fields.Many2one('uom', '辅助单位', ondelete='restrict',
  259. help='盘点单行对应的商品的辅助单位')
  260. real_qty = fields.Float(
  261. '账面数量', digits='Quantity',
  262. help='盘点单行对应的商品的账面数量')
  263. real_uos_qty = fields.Float(
  264. '账面辅助数量', digits='Quantity',
  265. help='盘点单行对应的商品的账面辅助数量')
  266. inventory_qty = fields.Float(
  267. '实际数量', digits='Quantity',
  268. required=True,
  269. help='盘点单行对应的商品的实际数量')
  270. inventory_uos_qty = fields.Float(
  271. '实际辅助数量', digits='Quantity',
  272. required=True,
  273. help='盘点单行对应的商品的实际辅助数量')
  274. difference_qty = fields.Float(
  275. '差异数量', digits='Quantity',
  276. compute='_get_difference_qty',
  277. help='盘点单行对应的商品的差异数量')
  278. difference_uos_qty = fields.Float(
  279. '差异辅助数量', digits='Quantity',
  280. compute='_get_difference_uos_qty',
  281. help='盘点单行对应的商品的差异辅助数量')
  282. company_id = fields.Many2one(
  283. 'res.company',
  284. string='公司',
  285. change_default=True,
  286. default=lambda self: self.env.company)
  287. def check_difference_identical(self):
  288. if self.difference_qty * self.difference_uos_qty < 0:
  289. self.inventory_qty = self.real_qty
  290. self.inventory_uos_qty = self.real_uos_qty
  291. return {'warning': {
  292. 'title': '错误',
  293. 'message': '盘盈盘亏数量应该与辅助单位的盘盈盘亏数量盈亏方向一致',
  294. }}
  295. def create_wh_inventory_line_by_data(self, inventory_id, line_data):
  296. self.create({
  297. 'inventory_id': inventory_id,
  298. 'warehouse_id': line_data.get('warehouse_id'),
  299. 'goods_id': line_data.get('goods_id'),
  300. 'attribute_id': line_data.get('attribute_id'),
  301. 'lot': line_data.get('lot'),
  302. 'uom_id': line_data.get('uom_id'),
  303. 'uos_id': line_data.get('uos_id'),
  304. 'real_qty': line_data.get('qty'),
  305. 'real_uos_qty': line_data.get('uos_qty'),
  306. 'inventory_qty': line_data.get('qty'),
  307. 'inventory_uos_qty': line_data.get('uos_qty'),
  308. })
  309. def line_role_back(self):
  310. self.inventory_qty = self.real_qty
  311. self.inventory_uos_qty = self.real_uos_qty
  312. self.difference_qty = 0
  313. self.difference_uos_qty = 0
  314. self.new_lot = ''
  315. self.new_lot_id = False
  316. self.lot_type = 'nothing'
  317. @api.onchange('inventory_qty')
  318. def onchange_qty(self):
  319. self.ensure_one()
  320. if self.goods_id and self.goods_id.using_batch:
  321. if self.goods_id.force_batch_one and self.difference_qty:
  322. flag = self.difference_qty > 0 and 1 or -1
  323. if abs(self.difference_qty) != 1:
  324. self.line_role_back()
  325. return {'warning': {
  326. 'title': '警告',
  327. 'message': _('商品上设置了序号为1,此时一次只能盘亏或盘盈一个商品数量'),
  328. }}
  329. if self.difference_qty > 0:
  330. self.lot_type = 'in'
  331. self.new_lot = self.new_lot or self.lot
  332. self.new_lot_id = False
  333. elif self.difference_qty < 0:
  334. self.lot_type = 'out'
  335. self.new_lot = ''
  336. else:
  337. self.lot_type = 'nothing'
  338. self.new_lot_id = False
  339. self.new_lot = ''
  340. return self.check_difference_identical()
  341. @api.onchange('inventory_uos_qty')
  342. def onchange_uos_qty(self):
  343. self.inventory_qty = self.goods_id.conversion_unit(
  344. self.inventory_uos_qty)
  345. def get_move_line(self, wh_type='in', context=None):
  346. inventory_warehouse = self.env['warehouse'] \
  347. .get_warehouse_by_type('inventory')
  348. for inventory in self:
  349. cost, cost_unit = inventory.goods_id. \
  350. get_suggested_cost_by_warehouse(
  351. inventory.warehouse_id, abs(inventory.difference_qty),
  352. lot_id=inventory.new_lot_id,
  353. attribute=inventory.attribute_id)
  354. if wh_type == 'out':
  355. lot_id = self.env['wh.move.line'].search([('warehouse_dest_id', '=', inventory.warehouse_id.id),
  356. ('goods_id', '=', inventory.goods_id.id),
  357. ('lot', '=', inventory.lot),
  358. ('state', '=', 'done')
  359. ], limit=1)
  360. res = {
  361. 'type': wh_type,
  362. 'lot': inventory.lot,
  363. 'lot_id': lot_id.id,
  364. 'goods_id': inventory.goods_id.id,
  365. 'attribute_id': inventory.attribute_id.id,
  366. 'uom_id': inventory.uom_id.id,
  367. 'uos_id': inventory.uos_id.id,
  368. 'cost_unit': cost_unit,
  369. 'cost': cost,
  370. }
  371. elif wh_type == 'in':
  372. res = {
  373. 'type': wh_type,
  374. 'lot': inventory.new_lot,
  375. 'lot_id': inventory.new_lot_id.id,
  376. 'goods_id': inventory.goods_id.id,
  377. 'attribute_id': inventory.attribute_id.id,
  378. 'uom_id': inventory.uom_id.id,
  379. 'uos_id': inventory.uos_id.id,
  380. 'cost_unit': cost_unit,
  381. 'cost': cost,
  382. }
  383. difference_qty, difference_uos_qty = abs(
  384. inventory.difference_qty), abs(inventory.difference_uos_qty)
  385. res.update({'goods_qty': difference_qty})
  386. return res
  387. class WhOut(models.Model):
  388. _inherit = 'wh.out'
  389. inventory_ids = fields.One2many('wh.inventory', 'out_id', '盘点单',
  390. help='其他出库单行对应的盘点单行,盘亏的情况')
  391. def approve_order(self):
  392. res = super(WhOut, self).approve_order()
  393. for order in self:
  394. order.inventory_ids.check_done()
  395. return res
  396. def cancel_approved_order(self):
  397. res = super(WhOut, self).cancel_approved_order()
  398. for order in self:
  399. order.inventory_ids.check_done()
  400. return res
  401. class WhIn(models.Model):
  402. _inherit = 'wh.in'
  403. inventory_ids = fields.One2many('wh.inventory', 'in_id', '盘点单',
  404. help='其他入库单行对应的盘点单行,盘盈的情况')
  405. def approve_order(self):
  406. res = super(WhIn, self).approve_order()
  407. for order in self:
  408. order.inventory_ids.check_done()
  409. return res
  410. def cancel_approved_order(self):
  411. res = super(WhIn, self).cancel_approved_order()
  412. for order in self:
  413. order.inventory_ids.check_done()
  414. return res
上海开阖软件有限公司 沪ICP备12045867号-1