GoodERP
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

1754 lines
73KB

  1. # -*- coding: utf-8 -*-
  2. from .utils import inherits, inherits_after, \
  3. create_name, safe_division, create_origin
  4. from itertools import islice
  5. from odoo import models, fields, api, _
  6. from odoo.exceptions import UserError
  7. class WhAssembly(models.Model):
  8. _name = 'wh.assembly'
  9. _description = '组装单'
  10. _inherit = ['mail.thread']
  11. _order = 'date DESC, id DESC'
  12. _inherits = {
  13. 'wh.move': 'move_id',
  14. }
  15. state = fields.Selection([('draft', '草稿'),
  16. ('feeding', '已发料'),
  17. ('done', '完成'),
  18. ('cancel', '已作废')],
  19. '状态', copy=False, default='draft',
  20. index=True,
  21. tracking=True,
  22. help='组装单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
  23. move_id = fields.Many2one(
  24. 'wh.move', '移库单', required=True, index=True, ondelete='cascade',
  25. help='组装单对应的移库单')
  26. bom_id = fields.Many2one(
  27. 'wh.bom', '物料清单', domain=[('type', '=', 'assembly')],
  28. context={'type': 'assembly'}, ondelete='restrict',
  29. readonly=True,
  30. help='组装单对应的物料清单')
  31. fee = fields.Float(
  32. '组装费用', digits='Amount',
  33. readonly=True,
  34. help='组装单对应的组装费用,组装费用+组装行入库成本作为子件的出库成本')
  35. goods_id = fields.Many2one('goods', string='组合件商品',
  36. readonly=True)
  37. lot = fields.Char('批号')
  38. goods_qty = fields.Float('组合件数量', default=1, digits='Quantity',
  39. readonly=True,
  40. help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
  41. voucher_id = fields.Many2one(
  42. 'voucher', copy=False, ondelete='restrict', string='入库凭证号')
  43. out_voucher_id = fields.Many2one(
  44. 'voucher', copy=False, ondelete='restrict', string='出库凭证号')
  45. company_id = fields.Many2one(
  46. 'res.company',
  47. string='公司',
  48. change_default=True,
  49. default=lambda self: self.env.company)
  50. details = fields.Html('明细', compute='_compute_details')
  51. @api.depends('move_id.line_in_ids', 'move_id.line_out_ids')
  52. def _compute_details(self):
  53. for v in self:
  54. vl = {'col': [], 'val': []}
  55. vl['col'] = ['', _('商品'), _('属性'), _('数量')]
  56. for l in v.move_id.line_in_ids:
  57. vl['val'].append([_('组合件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  58. for l in v.move_id.line_out_ids:
  59. vl['val'].append([_('子件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  60. v.details = v.company_id._get_html_table(vl)
  61. def apportion_cost(self, cost):
  62. for assembly in self:
  63. if not assembly.line_in_ids:
  64. continue
  65. collects = []
  66. ignore_move = [line.id for line in assembly.line_in_ids]
  67. for parent in assembly.line_in_ids:
  68. collects.append((
  69. parent, parent.goods_id.get_suggested_cost_by_warehouse(
  70. parent.warehouse_dest_id, parent.goods_qty,
  71. lot_id=parent.lot_id,
  72. attribute=parent.attribute_id,
  73. ignore_move=ignore_move)[0]))
  74. amount_total, collect_parent_cost = sum(
  75. collect[1] for collect in collects), 0
  76. for parent, amount in islice(collects, 0, len(collects) - 1):
  77. parent_cost = safe_division(amount, amount_total) * cost
  78. collect_parent_cost += parent_cost
  79. parent.write({
  80. 'cost_unit': safe_division(
  81. parent_cost, parent.goods_qty),
  82. 'cost': parent_cost,
  83. })
  84. # 最后一行数据使用总金额减去已经消耗的金额来计算
  85. last_parent_cost = cost - collect_parent_cost
  86. collects[-1][0].write({
  87. 'cost_unit': safe_division(
  88. last_parent_cost, collects[-1][0].goods_qty),
  89. 'cost': last_parent_cost,
  90. })
  91. return True
  92. def update_parent_cost(self):
  93. for assembly in self:
  94. cost = sum(child.cost for child in assembly.line_out_ids) + \
  95. assembly.fee
  96. assembly.apportion_cost(cost)
  97. return True
  98. @api.onchange('goods_id')
  99. def onchange_goods_id(self):
  100. if self.goods_id and not self.bom_id:
  101. self.line_in_ids = [(0,0,{'goods_id': self.goods_id.id, 'goods_uos_qty': 1, 'goods_qty': 1,
  102. 'uom_id': self.goods_id.uom_id.id, 'uos_id': self.goods_id.uos_id.id,
  103. 'type': 'in'})]
  104. @api.onchange('goods_qty')
  105. def onchange_goods_qty(self):
  106. """
  107. 改变商品数量时(wh_assembly 中的goods_qty) 根据物料清单的 数量的比例及成本价的计算
  108. 算出新的组合件或者子件的 数量 (line.goods_qty / parent_line_goods_qty * self.goods_qty
  109. line.goods_qty 子件商品数量
  110. parent_line_goods_qty 物料清单组合件商品数量
  111. self.goods_qty 所要的组合件的商品数量
  112. line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
  113. line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
  114. )
  115. :return:line_out_ids ,line_in_ids
  116. """
  117. line_out_ids, line_in_ids = [], []
  118. warehouse_id = self.env['warehouse'].search(
  119. [('type', '=', 'stock')], limit=1)
  120. if self.bom_id:
  121. line_in_ids = [(0,0,{'goods_id': line.goods_id.id,
  122. 'attribute_id': line.attribute_id.id,
  123. 'warehouse_id': self.env['warehouse'].get_warehouse_by_type(
  124. 'production').id,
  125. 'warehouse_dest_id': warehouse_id.id,
  126. 'uom_id': line.goods_id.uom_id.id,
  127. 'goods_qty': self.goods_qty,
  128. 'goods_uos_qty': self.goods_qty / line.goods_id.conversion,
  129. 'uos_id': line.goods_id.uos_id.id,
  130. 'type': 'in',
  131. }) for line in self.bom_id.line_parent_ids]
  132. parent_line_goods_qty = self.bom_id.line_parent_ids[0].goods_qty
  133. for line in self.bom_id.line_child_ids:
  134. cost, cost_unit = line.goods_id. \
  135. get_suggested_cost_by_warehouse(
  136. warehouse_id[0], line.goods_qty / parent_line_goods_qty * self.goods_qty)
  137. local_goods_qty = line.goods_qty / parent_line_goods_qty * self.goods_qty
  138. line_out_ids.append((0,0,{
  139. 'goods_id': line.goods_id.id,
  140. 'attribute_id': line.attribute_id.id,
  141. 'warehouse_id': warehouse_id.id,
  142. 'warehouse_dest_id': self.env[
  143. 'warehouse'].get_warehouse_by_type('production'),
  144. 'uom_id': line.goods_id.uom_id.id,
  145. 'goods_qty': local_goods_qty,
  146. 'cost_unit': cost_unit,
  147. 'cost': cost,
  148. 'goods_uos_qty': local_goods_qty / line.goods_id.conversion,
  149. 'uos_id': line.goods_id.uos_id.id,
  150. 'type': 'out',
  151. }))
  152. line_ids = []
  153. if line_out_ids:
  154. line_ids += line_out_ids
  155. if line_in_ids:
  156. line_ids += line_in_ids
  157. self.line_ids = False
  158. self.line_ids = line_ids
  159. def check_parent_length(self):
  160. for p in self:
  161. if not len(p.line_in_ids) or not len(p.line_out_ids):
  162. raise UserError(_('组合件和子件的商品都需要输入'))
  163. def create_voucher_line(self, data):
  164. return [self.env['voucher.line'].create(data_line) for data_line in data]
  165. def create_vourcher_line_data(self, assembly, voucher_row):
  166. """
  167. 准备入库凭证行数据
  168. 借:库存商品(商品上)
  169. 贷:生产成本-基本生产成本(核算分类上)
  170. :param assembly: 组装单
  171. :param voucher_row: 入库凭证
  172. :return:
  173. """
  174. line_out_data, line_in_data = [], []
  175. line_out_credit = 0.0
  176. for line_out in assembly.line_in_ids:
  177. if line_out.cost:
  178. line_out_credit += line_out.cost
  179. if line_out_credit: # 贷方行
  180. account_id = self.finance_category_id.account_id.id
  181. line_out_data.append({'credit': line_out_credit - assembly.fee,
  182. 'goods_id': False,
  183. 'voucher_id': voucher_row.id,
  184. 'account_id': account_id,
  185. 'name': '%s 原料 %s' % (assembly.move_id.name, assembly.move_id.note or '')
  186. })
  187. for line_in in assembly.line_in_ids: # 借方行
  188. if line_in.cost:
  189. account_id = line_in.goods_id.category_id.account_id.id
  190. line_in_data.append({'debit': line_in.cost,
  191. 'goods_id': line_in.goods_id.id,
  192. 'goods_qty': line_in.goods_qty,
  193. 'voucher_id': voucher_row.id,
  194. 'account_id': account_id,
  195. 'name': '%s 成品 %s' % (assembly.move_id.name, assembly.move_id.note or '')})
  196. return line_out_data + line_in_data
  197. def wh_assembly_create_voucher_line(self, assembly, voucher_row):
  198. """
  199. 创建入库凭证行
  200. :param assembly: 组装单
  201. :param voucher_row: 入库凭证
  202. :return:
  203. """
  204. voucher_line_data = []
  205. # 贷方行
  206. if assembly.fee:
  207. account_row = assembly.create_uid.company_id.operating_cost_account_id
  208. voucher_line_data.append({'name': '组装费用', 'account_id': account_row.id,
  209. 'credit': assembly.fee, 'voucher_id': voucher_row.id})
  210. voucher_line_data += self.create_vourcher_line_data(
  211. assembly, voucher_row)
  212. self.create_voucher_line(voucher_line_data)
  213. def pre_out_vourcher_line_data(self, assembly, voucher):
  214. """
  215. 准备出库凭证行数据
  216. 借:生产成本-基本生产成本(核算分类上)
  217. 贷:库存商品(商品上)
  218. :param assembly: 组装单
  219. :param voucher: 出库凭证
  220. :return: 出库凭证行数据
  221. """
  222. line_out_data, line_in_data = [], []
  223. line_out_debit = 0.0
  224. for line_out in assembly.line_out_ids:
  225. if line_out.cost:
  226. line_out_debit += line_out.cost
  227. if line_out_debit: # 借方行
  228. account_id = self.finance_category_id.account_id.id
  229. line_in_data.append({'debit': line_out_debit,
  230. 'goods_id': False,
  231. 'voucher_id': voucher.id,
  232. 'account_id': account_id,
  233. 'name': '%s 成品 %s' % (assembly.move_id.name, assembly.move_id.note or '')
  234. })
  235. for line_out in assembly.line_out_ids: # 贷方行
  236. if line_out.cost:
  237. account_id = line_out.goods_id.category_id.account_id.id
  238. line_out_data.append({'credit': line_out.cost,
  239. 'goods_id': line_out.goods_id.id,
  240. 'goods_qty': line_out.goods_qty,
  241. 'voucher_id': voucher.id,
  242. 'account_id': account_id,
  243. 'name': '%s 原料 %s' % (assembly.move_id.name, assembly.move_id.note or '')})
  244. return line_out_data + line_in_data
  245. def create_out_voucher_line(self, assembly, voucher):
  246. """
  247. 创建出库凭证行
  248. :param assembly: 组装单
  249. :param voucher: 出库凭证
  250. :return:
  251. """
  252. voucher_line_data = self.pre_out_vourcher_line_data(assembly, voucher)
  253. self.create_voucher_line(voucher_line_data)
  254. def wh_assembly_create_voucher(self):
  255. """
  256. 生成入库凭证并审核
  257. :return:
  258. """
  259. for assembly in self:
  260. voucher_row = self.env['voucher'].create({
  261. 'date': self.date,
  262. })
  263. self.wh_assembly_create_voucher_line(assembly, voucher_row) # 入库凭证
  264. if not voucher_row.line_ids:
  265. voucher_row.unlink()
  266. return
  267. assembly.voucher_id = voucher_row.id
  268. voucher_row.voucher_done()
  269. def create_out_voucher(self):
  270. """
  271. 生成出库凭证并审核
  272. :return:
  273. """
  274. for assembly in self:
  275. out_voucher = self.env['voucher'].create({
  276. 'date': self.date,
  277. })
  278. self.create_out_voucher_line(assembly, out_voucher) # 出库凭证
  279. if not out_voucher.line_ids:
  280. out_voucher.unlink()
  281. return
  282. old_voucher = assembly.out_voucher_id
  283. assembly.out_voucher_id = out_voucher.id
  284. out_voucher.voucher_done()
  285. if old_voucher:
  286. old_voucher.voucher_draft()
  287. old_voucher.unlink()
  288. def check_is_child_enable(self):
  289. for child_line in self.line_out_ids:
  290. for parent_line in self.line_in_ids:
  291. if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
  292. raise UserError(_('子件中不能包含与组合件中相同的 产品+属性,%s') % parent_line.goods_id.name)
  293. def approve_feeding(self):
  294. ''' 发料 '''
  295. for order in self:
  296. if order.state == 'feeding':
  297. raise UserError(_('请不要重复发料'))
  298. order.check_parent_length()
  299. order.check_is_child_enable()
  300. for line_out in order.line_out_ids:
  301. if line_out.state != 'done':
  302. line_out.action_done()
  303. order.create_out_voucher() # 生成出库凭证并审核
  304. order.state = 'feeding'
  305. return
  306. def cancel_feeding(self):
  307. ''' 退料 '''
  308. for order in self:
  309. if order.state == 'done':
  310. raise UserError(_('已入库不可退料'))
  311. for line_out in order.line_out_ids:
  312. if line_out.state != 'draft':
  313. line_out.action_draft()
  314. # 删除出库凭证
  315. voucher, order.out_voucher_id = order.out_voucher_id, False
  316. if voucher.state == 'done':
  317. voucher.voucher_draft()
  318. voucher.unlink()
  319. order.state = 'draft'
  320. return
  321. def approve_order(self):
  322. ''' 成品入库 '''
  323. for order in self:
  324. if order.state == 'done':
  325. raise UserError(_('请不要重复执行成品入库'))
  326. if order.state != 'feeding':
  327. raise UserError(_('请先投料'))
  328. order.move_id.check_qc_result() # 检验质检报告是否上传
  329. if order.lot: # 入库批次
  330. for line in order.line_in_ids:
  331. line.lot = order.lot
  332. order.line_in_ids.action_done() # 完成成品入库
  333. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  334. if wh_internal:
  335. wh_internal.approve_order()
  336. order.update_parent_cost()
  337. order.wh_assembly_create_voucher() # 生成入库凭证并审核
  338. order.approve_uid = self.env.uid
  339. order.approve_date = fields.Datetime.now(self)
  340. order.state = 'done'
  341. order.move_id.state = 'done'
  342. return
  343. def cancel_approved_order(self):
  344. for order in self:
  345. if order.state == 'feeding':
  346. raise UserError(_('请不要重复撤销 %s') % self._description)
  347. # 反审核入库到废品仓的移库单
  348. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  349. if wh_internal:
  350. wh_internal.cancel_approved_order()
  351. wh_internal.unlink()
  352. order.line_in_ids.action_draft()
  353. # 删除入库凭证
  354. voucher, order.voucher_id = order.voucher_id, False
  355. if voucher.state == 'done':
  356. voucher.voucher_draft()
  357. voucher.unlink()
  358. order.approve_uid = False
  359. order.approve_date = False
  360. order.state = 'feeding'
  361. order.move_id.state = 'draft'
  362. @ inherits()
  363. def unlink(self):
  364. for order in self:
  365. if order.state != 'draft':
  366. raise UserError(_('只能删除草稿状态的单据'))
  367. return order.move_id.unlink()
  368. @api.model_create_multi
  369. @create_name
  370. @create_origin
  371. def create(self, vals_list):
  372. for vals in vals_list:
  373. vals.update({'finance_category_id': self.env.ref(
  374. 'finance.categ_ass_disass').id})
  375. new_ids = super(WhAssembly, self).create(vals_list)
  376. for assembly in new_ids:
  377. assembly.update_parent_cost()
  378. return new_ids
  379. def write(self, vals):
  380. if 'line_in_ids' in vals:
  381. vals.pop('line_in_ids')
  382. if 'line_out_ids' in vals:
  383. vals.pop('line_out_ids')
  384. res = super(WhAssembly, self).write(vals)
  385. self.update_parent_cost()
  386. return res
  387. @api.onchange('bom_id')
  388. def onchange_bom(self):
  389. line_out_ids, line_in_ids = [], []
  390. domain = {}
  391. warehouse_id = self.env['warehouse'].search(
  392. [('type', '=', 'stock')], limit=1)
  393. if self.bom_id:
  394. line_in_ids = [(0, 0, {
  395. 'type': 'in',
  396. 'goods_id': line.goods_id.id,
  397. 'warehouse_id': self.env['warehouse'].get_warehouse_by_type(
  398. 'production').id,
  399. 'warehouse_dest_id': warehouse_id.id,
  400. 'uom_id': line.goods_id.uom_id.id,
  401. 'goods_qty': line.goods_qty,
  402. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  403. 'uos_id': line.goods_id.uos_id.id,
  404. 'attribute_id': line.attribute_id.id,
  405. }) for line in self.bom_id.line_parent_ids]
  406. for line in self.bom_id.line_child_ids:
  407. cost, cost_unit = line.goods_id. \
  408. get_suggested_cost_by_warehouse(
  409. warehouse_id[0], line.goods_qty)
  410. line_out_ids.append((0,0,{
  411. 'type': 'out',
  412. 'goods_id': line.goods_id.id,
  413. 'warehouse_id': warehouse_id.id,
  414. 'warehouse_dest_id': self.env[
  415. 'warehouse'].get_warehouse_by_type('production').id,
  416. 'uom_id': line.goods_id.uom_id.id,
  417. 'goods_qty': line.goods_qty,
  418. 'cost_unit': cost_unit,
  419. 'cost': cost,
  420. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  421. 'uos_id': line.goods_id.uos_id.id,
  422. 'attribute_id': line.attribute_id.id,
  423. }))
  424. self.line_in_ids = False
  425. self.line_out_ids = False
  426. else:
  427. self.goods_qty = 1
  428. if len(line_in_ids) == 1:
  429. """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
  430. 使试图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
  431. self.goods_qty = line_in_ids[0][2].get("goods_qty")
  432. self.goods_id = line_in_ids[0][2].get("goods_id")
  433. domain = {'goods_id': [('id', '=', self.goods_id.id)]}
  434. line_ids = []
  435. if line_out_ids:
  436. line_ids += line_out_ids
  437. if line_in_ids:
  438. line_ids += line_in_ids
  439. self.line_ids = False
  440. self.line_ids = line_ids
  441. return {'domain': domain}
  442. def update_bom(self):
  443. for assembly in self:
  444. if assembly.bom_id:
  445. return assembly.save_bom()
  446. else:
  447. return {
  448. 'type': 'ir.actions.act_window',
  449. 'res_model': 'save.bom.memory',
  450. 'view_mode': 'form',
  451. 'target': 'new',
  452. }
  453. def save_bom(self, name=''):
  454. for assembly in self:
  455. line_parent_ids = []
  456. line_child_ids = []
  457. for line in assembly.line_ids:
  458. if line.type == 'in':
  459. line_parent_ids.append((0, 0, {
  460. 'goods_id': line.goods_id.id,
  461. 'goods_qty': line.goods_qty,
  462. }))
  463. if line.type == 'out':
  464. line_child_ids.append((0, 0, {
  465. 'goods_id': line.goods_id.id,
  466. 'goods_qty': line.goods_qty,
  467. }))
  468. if assembly.bom_id:
  469. assembly.bom_id.line_parent_ids.unlink()
  470. assembly.bom_id.line_child_ids.unlink()
  471. assembly.bom_id.write({
  472. 'line_parent_ids': line_parent_ids,
  473. 'line_child_ids': line_child_ids})
  474. else:
  475. bom_id = self.env['wh.bom'].create({
  476. 'name': name,
  477. 'type': 'assembly',
  478. 'line_parent_ids': line_parent_ids,
  479. 'line_child_ids': line_child_ids,
  480. })
  481. assembly.bom_id = bom_id
  482. return True
  483. class OutSource(models.Model):
  484. _name = 'outsource'
  485. _description = '委外加工单'
  486. _inherit = ['mail.thread']
  487. _order = 'date DESC, id DESC'
  488. _inherits = {
  489. 'wh.move': 'move_id',
  490. }
  491. state = fields.Selection([('draft', '草稿'),
  492. ('feeding', '已发料'),
  493. ('done', '完成'),
  494. ('cancel', '已作废')],
  495. '状态', copy=False, default='draft',
  496. index=True,
  497. help='委外加工单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
  498. move_id = fields.Many2one('wh.move', '移库单', required=True, index=True, ondelete='cascade',
  499. help='委外加工单对应的移库单')
  500. bom_id = fields.Many2one('wh.bom', '物料清单', domain=[('type', '=', 'outsource')],
  501. context={'type': 'outsource'}, ondelete='restrict',
  502. readonly=False,
  503. help='委外加工单对应的物料清单')
  504. goods_id = fields.Many2one('goods', string='组合件商品',
  505. readonly=True)
  506. lot = fields.Char('批号')
  507. goods_qty = fields.Float('组合件数量', default=1, digits='Quantity',
  508. readonly=True,
  509. help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
  510. voucher_id = fields.Many2one(
  511. 'voucher', copy=False, ondelete='restrict', string='入库凭证号')
  512. out_voucher_id = fields.Many2one(
  513. 'voucher', copy=False, ondelete='restrict', string='出库凭证号')
  514. outsource_partner_id = fields.Many2one('partner', string='委外供应商',
  515. readonly=True,
  516. required=True)
  517. address_id = fields.Many2one('partner.address', '地址',
  518. domain="[('partner_id', '=', outsource_partner_id)]",
  519. help='联系地址')
  520. wh_assembly_id = fields.Many2one('wh.assembly', string='关联的组装单',
  521. readonly=True)
  522. outsource_fee = fields.Float(string='委外费用(含税)',
  523. digits='Amount',
  524. readonly=True)
  525. tax_amount = fields.Float(string='税额',
  526. digits='Amount',
  527. readonly=True)
  528. invoice_id = fields.Many2one('money.invoice',
  529. copy=False,
  530. ondelete='set null',
  531. string='发票号')
  532. company_id = fields.Many2one(
  533. 'res.company',
  534. string='公司',
  535. change_default=True,
  536. default=lambda self: self.env.company)
  537. details = fields.Html('明细', compute='_compute_details')
  538. @api.depends('move_id.line_in_ids', 'move_id.line_out_ids')
  539. def _compute_details(self):
  540. for v in self:
  541. vl = {'col': [], 'val': []}
  542. vl['col'] = ['', _('商品'), _('属性'), _('数量')]
  543. for l in v.move_id.line_in_ids:
  544. vl['val'].append([_('产出'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  545. for l in v.move_id.line_out_ids:
  546. vl['val'].append([_('投入'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  547. v.details = v.company_id._get_html_table(vl)
  548. @api.onchange('goods_id')
  549. def onchange_goods_id(self):
  550. if self.goods_id and not self.bom_id:
  551. self.line_in_ids = False
  552. self.line_in_ids = [(0,0,{'goods_id': self.goods_id.id, 'goods_uos_qty': 1, 'goods_qty': 1,
  553. 'uom_id': self.goods_id.uom_id.id, 'uos_id': self.goods_id.uos_id.id,
  554. 'type': 'in'})]
  555. @api.onchange('goods_qty')
  556. def onchange_goods_qty(self):
  557. """
  558. 改变商品数量时(outsource 中的goods_qty) 根据 物料清单 中的数量的比例
  559. 计算出新的组合件或子件的数量
  560. (line.goods_qty / parent_line_goods_qty * self.goods_qty
  561. line.goods_qty 子件商品数量
  562. parent_line_goods_qty 物料清单组合件商品数量
  563. self.goods_qty 所要的组合件的商品数量
  564. line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
  565. line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
  566. )
  567. :return:line_out_ids ,line_in_ids
  568. """
  569. line_out_ids, line_in_ids = [], []
  570. warehouse_id = self.env['warehouse'].search(
  571. [('type', '=', 'stock')], limit=1)
  572. if self.bom_id: # 存在 物料清单
  573. line_in_ids = [(0, 0, {'goods_id': line.goods_id.id,
  574. 'attribute_id': line.attribute_id.id,
  575. 'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
  576. 'warehouse_dest_id': warehouse_id.id,
  577. 'uom_id': line.goods_id.uom_id.id,
  578. 'goods_qty': self.goods_qty,
  579. 'goods_uos_qty': self.goods_qty / line.goods_id.conversion,
  580. 'uos_id': line.goods_id.uos_id.id,
  581. 'type': 'in'
  582. }) for line in self.bom_id.line_parent_ids]
  583. parent_line_goods_qty = self.bom_id.line_parent_ids[0].goods_qty
  584. for line in self.bom_id.line_child_ids:
  585. cost, cost_unit = line.goods_id.get_suggested_cost_by_warehouse(
  586. warehouse_id[0], line.goods_qty / parent_line_goods_qty * self.goods_qty)
  587. local_goods_qty = line.goods_qty / parent_line_goods_qty * self.goods_qty
  588. line_out_ids.append((0,0,{
  589. 'goods_id': line.goods_id.id,
  590. 'attribute_id': line.attribute_id.id,
  591. 'warehouse_id': warehouse_id.id,
  592. 'warehouse_dest_id': self.env['warehouse'].get_warehouse_by_type('production'),
  593. 'uom_id': line.goods_id.uom_id.id,
  594. 'goods_qty': local_goods_qty,
  595. 'cost_unit': cost_unit,
  596. 'cost': cost,
  597. 'goods_uos_qty': local_goods_qty / line.goods_id.conversion,
  598. 'uos_id': line.goods_id.uos_id.id,
  599. 'type': 'out',
  600. }))
  601. line_ids = []
  602. if line_out_ids:
  603. line_ids += line_out_ids
  604. if line_in_ids:
  605. line_ids += line_in_ids
  606. self.line_ids = False
  607. self.line_ids = line_ids
  608. @api.onchange('bom_id')
  609. def onchange_bom(self):
  610. line_out_ids, line_in_ids = [], []
  611. domain = {}
  612. warehouse_id = self.env['warehouse'].search(
  613. [('type', '=', 'stock')], limit=1)
  614. if self.bom_id:
  615. line_in_ids = [(0,0,{
  616. 'goods_id': line.goods_id.id,
  617. 'attribute_id': line.attribute_id.id,
  618. 'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
  619. 'warehouse_dest_id': warehouse_id.id,
  620. 'uom_id': line.goods_id.uom_id.id,
  621. 'goods_qty': line.goods_qty,
  622. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  623. 'uos_id': line.goods_id.uos_id.id,
  624. 'type': 'in',
  625. }) for line in self.bom_id.line_parent_ids]
  626. for line in self.bom_id.line_child_ids:
  627. cost, cost_unit = line.goods_id. \
  628. get_suggested_cost_by_warehouse(
  629. warehouse_id[0], line.goods_qty)
  630. line_out_ids.append((0,0,{
  631. 'goods_id': line.goods_id.id,
  632. 'attribute_id': line.attribute_id.id,
  633. 'warehouse_id': warehouse_id.id,
  634. 'warehouse_dest_id': self.env[
  635. 'warehouse'].get_warehouse_by_type('production').id,
  636. 'uom_id': line.goods_id.uom_id.id,
  637. 'goods_qty': line.goods_qty,
  638. 'cost_unit': cost_unit,
  639. 'cost': cost,
  640. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  641. 'uos_id': line.goods_id.uos_id.id,
  642. 'type': 'out',
  643. }))
  644. self.line_in_ids = False
  645. self.line_out_ids = False
  646. else:
  647. self.goods_qty = 1
  648. if len(line_in_ids) == 1:
  649. """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
  650. 使视图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
  651. self.goods_qty = line_in_ids[0][-1].get("goods_qty")
  652. self.goods_id = line_in_ids[0][-1].get("goods_id")
  653. domain = {'goods_id': [('id', '=', self.goods_id.id)]}
  654. line_ids = []
  655. if line_out_ids:
  656. line_ids += line_out_ids
  657. if line_in_ids:
  658. line_ids += line_in_ids
  659. self.line_ids = False
  660. self.line_ids = line_ids
  661. return {'domain': domain}
  662. def apportion_cost(self, cost):
  663. for outsource in self:
  664. if not outsource.line_in_ids:
  665. continue
  666. collects = []
  667. ignore_move = [line.id for line in outsource.line_in_ids]
  668. for parent in outsource.line_in_ids:
  669. collects.append((parent,
  670. parent.goods_id.get_suggested_cost_by_warehouse(
  671. parent.warehouse_dest_id, parent.goods_qty,
  672. lot_id=parent.lot_id,
  673. attribute=parent.attribute_id,
  674. ignore_move=ignore_move)[0]
  675. ))
  676. amount_total, collect_parent_cost = sum(
  677. collect[1] for collect in collects), 0
  678. for parent, amount in islice(collects, 0, len(collects) - 1):
  679. parent_cost = safe_division(amount, amount_total) * cost
  680. collect_parent_cost += parent_cost
  681. parent.write({
  682. 'cost_unit': safe_division(
  683. parent_cost, parent.goods_qty),
  684. 'cost': parent_cost,
  685. })
  686. # 最后一行数据使用总金额减去已经消耗的金额来计算
  687. last_parent_cost = cost - collect_parent_cost
  688. collects[-1][0].write({
  689. 'cost_unit': safe_division(
  690. last_parent_cost, collects[-1][0].goods_qty),
  691. 'cost': last_parent_cost,
  692. })
  693. return True
  694. def update_parent_cost(self):
  695. for outsource in self:
  696. if not outsource.outsource_fee:
  697. outsource_fee = sum(
  698. p.cost for p in outsource.line_in_ids)
  699. tax_amount = sum(
  700. p.price_taxed for p in outsource.line_in_ids)
  701. if outsource_fee:
  702. outsource.outsource_fee = outsource_fee
  703. outsource.tax_amount = tax_amount
  704. cost = sum(child.cost for child in outsource.line_out_ids) + \
  705. outsource.outsource_fee - outsource.tax_amount
  706. outsource.apportion_cost(cost)
  707. return True
  708. @inherits()
  709. def unlink(self):
  710. for order in self:
  711. if order.state != 'draft':
  712. raise UserError(_('只删除草稿状态的单据'))
  713. return order.move_id.unlink()
  714. @api.model_create_multi
  715. @create_name
  716. @create_origin
  717. def create(self, vals_list):
  718. for vals in vals_list:
  719. vals.update({'finance_category_id': self.env.ref(
  720. 'finance.categ_outsource').id})
  721. new_ids = super(OutSource, self).create(vals_list)
  722. for outsource in new_ids:
  723. outsource.update_parent_cost()
  724. return new_ids
  725. def write(self, vals):
  726. if 'line_in_ids' in vals:
  727. vals.pop('line_in_ids')
  728. if 'line_out_ids' in vals:
  729. vals.pop('line_out_ids')
  730. res = super(OutSource, self).write(vals)
  731. if vals.get('outsource_fee') or vals.get('tax_amount'):
  732. return res
  733. self.update_parent_cost()
  734. return res
  735. def check_parent_length(self):
  736. for outsource_line in self:
  737. if not len(outsource_line.line_in_ids) or not len(outsource_line.line_out_ids):
  738. raise UserError(_('委外加工单必须存在组合件和子件明细行。'))
  739. def _create_money_invoice(self):
  740. categ = self.env.ref('money.core_category_purchase')
  741. source_id = self.env['money.invoice'].create({
  742. 'name': self.name,
  743. 'partner_id': self.outsource_partner_id.id,
  744. 'category_id': categ.id,
  745. 'date': fields.Date.context_today(self),
  746. 'amount': self.outsource_fee,
  747. 'tax_amount':self.tax_amount,
  748. 'reconciled': 0,
  749. 'to_reconcile': self.outsource_fee,
  750. 'date_due': fields.Date.context_today(self),
  751. 'note': self.note or '',
  752. })
  753. if source_id:
  754. self.invoice_id = source_id.id
  755. return source_id
  756. def create_voucher_line(self, data):
  757. return [self.env['voucher.line'].create(data_line) for data_line in data]
  758. def create_vourcher_line_data(self, outsource, voucher_row):
  759. """
  760. 准备入库凭证行数据
  761. 借:库存商品(商品上)
  762. 贷:生产成本-基本生产成本(核算分类上)
  763. :param outsource: 委外加工单
  764. :param voucher_row: 入库凭证
  765. :return:
  766. """
  767. line_out_data, line_in_data = [], []
  768. line_out_credit = 0.0
  769. for line_out in outsource.line_in_ids:
  770. if line_out.cost:
  771. line_out_credit += line_out.cost
  772. if round(line_out_credit - outsource.outsource_fee + outsource.tax_amount, 2) > 0: # 贷方行
  773. account_id = self.finance_category_id.account_id.id
  774. line_out_data.append({'credit': line_out_credit - outsource.outsource_fee + outsource.tax_amount,
  775. 'goods_id': False,
  776. 'voucher_id': voucher_row.id,
  777. 'account_id': account_id,
  778. 'name': '%s 原料 %s' % (outsource.move_id.name, outsource.move_id.note or '')
  779. })
  780. for line_in in outsource.line_in_ids: # 借方行
  781. if line_in.cost:
  782. account_id = line_in.goods_id.category_id.account_id.id
  783. line_in_data.append({'debit': line_in.cost,
  784. 'goods_id': line_in.goods_id.id,
  785. 'goods_qty': line_in.goods_qty,
  786. 'voucher_id': voucher_row.id,
  787. 'account_id': account_id,
  788. 'name': '%s 成品 %s' % (outsource.move_id.name, outsource.move_id.note or '')
  789. })
  790. return line_out_data + line_in_data
  791. def pre_out_vourcher_line_data(self, outsource, voucher):
  792. """
  793. 准备出库凭证行数据
  794. 借:委托加工物资(核算分类上)
  795. 贷:库存商品(商品上)
  796. :param outsource: 委外加工单
  797. :param voucher: 出库凭证
  798. :return: 出库凭证行数据
  799. """
  800. line_out_data, line_in_data = [], []
  801. line_out_debit = 0.0
  802. for line_out in outsource.line_out_ids:
  803. if line_out.cost:
  804. line_out_debit += line_out.cost
  805. if line_out_debit: # 借方行
  806. account_id = self.finance_category_id.account_id.id
  807. line_in_data.append({'debit': line_out_debit,
  808. 'goods_id': False,
  809. 'voucher_id': voucher.id,
  810. 'account_id': account_id,
  811. 'name': '%s 成品 %s' % (outsource.move_id.name, outsource.move_id.note or '')
  812. })
  813. for line_out in outsource.line_out_ids: # 贷方行
  814. if line_out.cost:
  815. account_id = line_out.goods_id.category_id.account_id.id
  816. line_out_data.append({'credit': line_out.cost,
  817. 'goods_id': line_out.goods_id.id,
  818. 'goods_qty': line_out.goods_qty,
  819. 'voucher_id': voucher.id,
  820. 'account_id': account_id,
  821. 'name': '%s 原料 %s' % (outsource.move_id.name, outsource.move_id.note or '')})
  822. return line_out_data + line_in_data
  823. def outsource_create_voucher_line(self, outsource, voucher_row):
  824. """
  825. 创建入库凭证行
  826. :param outsource: 委外加工单
  827. :param voucher_row: 入库凭证
  828. :return:
  829. """
  830. voucher_line_data = []
  831. if outsource.outsource_fee:
  832. # 贷方行
  833. voucher_line_data.append({'name': '委外费用',
  834. 'account_id': self.env.ref('money.core_category_purchase').account_id.id, # 采购发票类别对应的科目
  835. 'credit': outsource.outsource_fee-outsource.tax_amount, 'voucher_id': voucher_row.id})
  836. voucher_line_data += self.create_vourcher_line_data(
  837. outsource, voucher_row)
  838. self.create_voucher_line(voucher_line_data)
  839. def create_out_voucher_line(self, outsource, voucher):
  840. """
  841. 创建出库凭证行
  842. :param outsource: 委外加工单
  843. :param voucher: 出库凭证
  844. :return:
  845. """
  846. voucher_line_data = self.pre_out_vourcher_line_data(outsource, voucher)
  847. self.create_voucher_line(voucher_line_data)
  848. def outsource_create_voucher(self):
  849. """
  850. 生成入库凭证并审核
  851. :return:
  852. """
  853. for outsource in self:
  854. voucher_row = self.env['voucher'].create({
  855. 'date': outsource.date,
  856. })
  857. self.outsource_create_voucher_line(outsource, voucher_row) # 入库凭证
  858. outsource.voucher_id = voucher_row.id
  859. voucher_row.voucher_done()
  860. def create_out_voucher(self):
  861. """
  862. 生成出库凭证并审核
  863. :return:
  864. """
  865. for outsource in self:
  866. out_voucher = self.env['voucher'].create({
  867. 'date': outsource.date,
  868. })
  869. self.create_out_voucher_line(outsource, out_voucher) # 出库凭证
  870. if not out_voucher.line_ids:
  871. out_voucher.unlink()
  872. return
  873. old_voucher = outsource.out_voucher_id
  874. outsource.out_voucher_id = out_voucher.id
  875. out_voucher.voucher_done()
  876. if old_voucher:
  877. old_voucher.voucher_draft()
  878. old_voucher.unlink()
  879. def check_is_child_enable(self):
  880. for child_line in self.line_out_ids:
  881. for parent_line in self.line_in_ids:
  882. if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
  883. raise UserError(_('子件中不能包含与组合件中相同的 产品+属性,%s') % parent_line.goods_id.name)
  884. def approve_feeding(self):
  885. ''' 发料 '''
  886. for order in self:
  887. if order.state == 'feeding':
  888. raise UserError(_('请不要重复发料'))
  889. # order.check_parent_length()
  890. order.check_is_child_enable()
  891. for line_out in order.line_out_ids:
  892. if line_out.state != 'done':
  893. line_out.action_done()
  894. order.create_out_voucher() # 生成出库凭证并审核
  895. order.state = 'feeding'
  896. return
  897. def cancel_feeding(self):
  898. ''' 退料 '''
  899. for order in self:
  900. if order.state == 'done':
  901. raise UserError(_('已入库不可退料'))
  902. for line_out in order.line_out_ids:
  903. if line_out.state != 'draft':
  904. line_out.action_draft()
  905. # 删除出库凭证
  906. voucher, order.out_voucher_id = order.out_voucher_id, False
  907. if voucher.state == 'done':
  908. voucher.voucher_draft()
  909. voucher.unlink()
  910. order.state = 'draft'
  911. return
  912. def approve_order(self):
  913. ''' 成品入库 '''
  914. for order in self:
  915. if order.state == 'done':
  916. raise UserError(_('请不要重复执行成品入库'))
  917. if order.state != 'feeding':
  918. raise UserError(_('请先投料'))
  919. order.move_id.check_qc_result() # 检验质检报告是否上传
  920. if order.lot: # 入库批次
  921. for line in order.line_in_ids:
  922. line.lot = order.lot
  923. order.line_in_ids.action_done() # 完成成品入库
  924. order.date = fields.Date.context_today(self)
  925. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  926. if wh_internal:
  927. wh_internal.approve_order()
  928. # 如果委外费用存在,生成 结算单
  929. if order.outsource_fee:
  930. order._create_money_invoice()
  931. order.update_parent_cost()
  932. order.outsource_create_voucher() # 生成入库凭证并审核
  933. order.approve_uid = self.env.uid
  934. order.approve_date = fields.Datetime.now(self)
  935. order.state = 'done'
  936. order.move_id.state = 'done'
  937. return
  938. def cancel_approved_order(self):
  939. for order in self:
  940. if order.state == 'feeding':
  941. raise UserError(_('请不要重复撤销 %s') % self._description)
  942. # 反审核入库到废品仓的移库单
  943. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  944. if wh_internal:
  945. wh_internal.cancel_approved_order()
  946. wh_internal.unlink()
  947. order.line_in_ids.action_draft()
  948. # 删除入库凭证
  949. voucher, order.voucher_id = order.voucher_id, False
  950. if voucher.state == 'done':
  951. voucher.voucher_draft()
  952. voucher.unlink()
  953. if order.invoice_id:
  954. if order.invoice_id.state == 'done':
  955. order.invoice_id.money_invoice_draft()
  956. order.invoice_id.unlink()
  957. order.approve_uid = False
  958. order.approve_date = False
  959. order.state = 'feeding'
  960. order.move_id.state = 'draft'
  961. class WhDisassembly(models.Model):
  962. _name = 'wh.disassembly'
  963. _description = '拆卸单'
  964. _inherit = ['mail.thread']
  965. _order = 'date DESC, id DESC'
  966. _inherits = {
  967. 'wh.move': 'move_id',
  968. }
  969. state = fields.Selection([('draft', '草稿'),
  970. ('feeding', '已发料'),
  971. ('done', '完成'),
  972. ('cancel', '已作废')],
  973. '状态', copy=False, default='draft',
  974. index=True,
  975. help='拆卸单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
  976. move_id = fields.Many2one(
  977. 'wh.move', '移库单', required=True, index=True, ondelete='cascade',
  978. help='拆卸单对应的移库单')
  979. bom_id = fields.Many2one(
  980. 'wh.bom', '物料清单', domain=[('type', '=', 'disassembly')],
  981. context={'type': 'disassembly'}, ondelete='restrict',
  982. readonly=True,
  983. help='拆卸单对应的物料清单')
  984. fee = fields.Float(
  985. '拆卸费用', digits='Amount',
  986. readonly=True,
  987. help='拆卸单对应的拆卸费用, 拆卸费用+拆卸行出库成本作为子件的入库成本')
  988. goods_id = fields.Many2one('goods', string='组合件商品',
  989. readonly=True)
  990. lot_id = fields.Many2one('wh.move.line', '批号',
  991. help='用于拆卸的组合件批号')
  992. goods_qty = fields.Float('组合件数量', default=1, digits='Quantity',
  993. readonly=True,
  994. help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
  995. voucher_id = fields.Many2one(
  996. 'voucher', copy=False, ondelete='restrict', string='入库凭证号')
  997. out_voucher_id = fields.Many2one(
  998. 'voucher', copy=False, ondelete='restrict', string='出库凭证号')
  999. company_id = fields.Many2one(
  1000. 'res.company',
  1001. string='公司',
  1002. change_default=True,
  1003. default=lambda self: self.env.company)
  1004. details = fields.Html('明细', compute='_compute_details')
  1005. @api.depends('move_id.line_in_ids', 'move_id.line_out_ids')
  1006. def _compute_details(self):
  1007. for v in self:
  1008. vl = {'col': [], 'val': []}
  1009. vl['col'] = ['', _('商品'), _('属性'), _('数量')]
  1010. for l in v.move_id.line_out_ids:
  1011. vl['val'].append([_('组合件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  1012. for l in v.move_id.line_in_ids:
  1013. vl['val'].append([_('子件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  1014. v.details = v.company_id._get_html_table(vl)
  1015. def apportion_cost(self, cost):
  1016. for assembly in self:
  1017. if not assembly.line_in_ids:
  1018. continue
  1019. collects = []
  1020. ignore_move = [line.id for line in assembly.line_in_ids]
  1021. for child in assembly.line_in_ids:
  1022. collects.append((
  1023. child, child.goods_id.get_suggested_cost_by_warehouse(
  1024. child.warehouse_dest_id, child.goods_qty,
  1025. lot_id=child.lot_id, attribute=child.attribute_id,
  1026. ignore_move=ignore_move)[0]))
  1027. amount_total, collect_child_cost = \
  1028. sum(collect[1] for collect in collects), 0
  1029. for child, amount in islice(collects, 0, len(collects) - 1):
  1030. child_cost = safe_division(amount, amount_total) * cost
  1031. collect_child_cost += child_cost
  1032. child.write({
  1033. 'cost_unit': safe_division(
  1034. child_cost, child.goods_qty),
  1035. 'cost': child_cost,
  1036. })
  1037. # 最后一行数据使用总金额减去已经消耗的金额来计算
  1038. last_child_cost = cost - collect_child_cost
  1039. collects[-1][0].write({
  1040. 'cost_unit': safe_division(
  1041. last_child_cost, collects[-1][0].goods_qty),
  1042. 'cost': last_child_cost,
  1043. })
  1044. return True
  1045. def update_child_cost(self):
  1046. for assembly in self:
  1047. cost = sum(child.cost for child in assembly.line_out_ids) + \
  1048. assembly.fee
  1049. assembly.apportion_cost(cost)
  1050. return True
  1051. def check_parent_length(self):
  1052. for whd in self:
  1053. if not len(whd.line_in_ids) or not len(whd.line_out_ids):
  1054. raise UserError(_('组合件和子件的商品都必须输入'))
  1055. def create_voucher_line(self, data):
  1056. return [self.env['voucher.line'].create(data_line) for data_line in data]
  1057. def create_vourcher_line_data(self, disassembly, voucher_row):
  1058. """
  1059. 准备入库凭证行数据
  1060. 借:库存商品(商品上)
  1061. 贷:生产成本-基本生产成本(核算分类上)
  1062. :param disassembly: 拆卸单
  1063. :param voucher_row: 入库库凭证
  1064. :return:
  1065. """
  1066. line_out_data, line_in_data = [], []
  1067. line_in_credit = 0.0
  1068. for line_in in disassembly.line_in_ids:
  1069. if line_in.cost:
  1070. line_in_credit += line_in.cost
  1071. if line_in_credit: # 贷方行
  1072. account_id = self.finance_category_id.account_id.id
  1073. line_out_data.append({'credit': line_in_credit,
  1074. 'goods_id': False,
  1075. 'voucher_id': voucher_row.id,
  1076. 'account_id': account_id,
  1077. 'name': '%s 原料 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
  1078. })
  1079. for line_in in disassembly.line_in_ids: # 借方行
  1080. if line_in.cost:
  1081. account_id = line_in.goods_id.category_id.account_id.id
  1082. line_in_data.append({'debit': line_in.cost,
  1083. 'goods_id': line_in.goods_id.id,
  1084. 'goods_qty': line_in.goods_qty,
  1085. 'voucher_id': voucher_row.id,
  1086. 'account_id': account_id,
  1087. 'name': '%s 成品 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
  1088. })
  1089. return line_out_data + line_in_data
  1090. def pre_out_vourcher_line_data(self, disassembly, voucher):
  1091. """
  1092. 准备出库凭证行数据
  1093. 借:生产成本-基本生产成本(核算分类上)
  1094. 贷:库存商品(商品上)
  1095. :param disassembly: 拆卸单
  1096. :param voucher: 出库凭证
  1097. :return: 出库凭证行数据
  1098. """
  1099. line_out_data, line_in_data = [], []
  1100. line_out_debit = 0.0
  1101. for line_out in disassembly.line_out_ids:
  1102. line_out_debit += line_out.cost
  1103. if line_out_debit: # 借方行
  1104. account_id = self.finance_category_id.account_id.id
  1105. line_in_data.append({'debit': line_out_debit,
  1106. 'goods_id': False,
  1107. 'voucher_id': voucher.id,
  1108. 'account_id': account_id,
  1109. 'name': '%s 成品 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
  1110. })
  1111. for line_out in disassembly.line_out_ids: # 贷方行
  1112. if line_out.cost:
  1113. account_id = line_out.goods_id.category_id.account_id.id
  1114. line_out_data.append({'credit': line_out.cost + disassembly.fee,
  1115. 'goods_id': line_out.goods_id.id,
  1116. 'goods_qty': line_out.goods_qty,
  1117. 'voucher_id': voucher.id,
  1118. 'account_id': account_id,
  1119. 'name': '%s 原料 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')})
  1120. return line_out_data + line_in_data
  1121. def wh_disassembly_create_voucher_line(self, disassembly, voucher_row):
  1122. """
  1123. 创建入库凭证行
  1124. :param disassembly:
  1125. :param voucher_row:
  1126. :return:
  1127. """
  1128. voucher_line_data = []
  1129. voucher_line_data += self.create_vourcher_line_data(
  1130. disassembly, voucher_row)
  1131. self.create_voucher_line(voucher_line_data)
  1132. def create_out_voucher_line(self, disassembly, voucher):
  1133. """
  1134. 创建出库凭证行
  1135. :param disassembly: 拆卸单
  1136. :param voucher: 出库凭证
  1137. :return:
  1138. """
  1139. voucher_line_data = []
  1140. # 借方行
  1141. if disassembly.fee:
  1142. account = disassembly.create_uid.company_id.operating_cost_account_id
  1143. voucher_line_data.append({'name': '拆卸费用', 'account_id': account.id,
  1144. 'debit': disassembly.fee, 'voucher_id': voucher.id})
  1145. voucher_line_data += self.pre_out_vourcher_line_data(
  1146. disassembly, voucher)
  1147. self.create_voucher_line(voucher_line_data)
  1148. def wh_disassembly_create_voucher(self):
  1149. """
  1150. 生成入库凭证并审核
  1151. :return:
  1152. """
  1153. for disassembly in self:
  1154. voucher_row = self.env['voucher'].create({
  1155. 'date': self.date,
  1156. })
  1157. self.wh_disassembly_create_voucher_line(
  1158. disassembly, voucher_row) # 入库凭证
  1159. disassembly.voucher_id = voucher_row.id
  1160. voucher_row.voucher_done()
  1161. def create_out_voucher(self):
  1162. """
  1163. 生成出库凭证并审核
  1164. :return:
  1165. """
  1166. for disassembly in self:
  1167. out_voucher = self.env['voucher'].create({
  1168. 'date': self.date,
  1169. })
  1170. self.create_out_voucher_line(disassembly, out_voucher) # 出库凭证
  1171. old_voucher = disassembly.out_voucher_id
  1172. disassembly.out_voucher_id = out_voucher.id
  1173. out_voucher.voucher_done()
  1174. if old_voucher:
  1175. old_voucher.voucher_draft()
  1176. old_voucher.unlink()
  1177. def check_is_child_enable(self):
  1178. for child_line in self.line_in_ids:
  1179. for parent_line in self.line_out_ids:
  1180. if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
  1181. raise UserError(_('子件中不能包含与组合件中相同的 产品+属性,%s') % parent_line.goods_id.name)
  1182. def approve_feeding(self):
  1183. ''' 发料 '''
  1184. for order in self:
  1185. if order.state == 'feeding':
  1186. raise UserError(_('请不要重复发料'))
  1187. order.check_parent_length()
  1188. order.check_is_child_enable()
  1189. for line_out in order.line_out_ids:
  1190. if line_out.state != 'done':
  1191. if order.lot_id: #出库批次
  1192. line_out.lot_id = order.lot_id
  1193. line_out.action_done()
  1194. order.create_out_voucher() # 生成出库凭证并审核
  1195. order.state = 'feeding'
  1196. return
  1197. def cancel_feeding(self):
  1198. ''' 退料 '''
  1199. for order in self:
  1200. if order.state == 'done':
  1201. raise UserError(_('已入库不可退料'))
  1202. for line_out in order.line_out_ids:
  1203. if line_out.state != 'draft':
  1204. line_out.action_draft()
  1205. # 删除出库凭证
  1206. voucher, order.out_voucher_id = order.out_voucher_id, False
  1207. if voucher.state == 'done':
  1208. voucher.voucher_draft()
  1209. voucher.unlink()
  1210. order.state = 'draft'
  1211. return
  1212. def approve_order(self):
  1213. ''' 成品入库 '''
  1214. for order in self:
  1215. if order.state == 'done':
  1216. raise UserError(_('请不要重复执行成品入库'))
  1217. if order.state != 'feeding':
  1218. raise UserError(_('请先投料'))
  1219. order.move_id.check_qc_result() # 检验质检报告是否上传
  1220. order.line_in_ids.action_done() # 完成成品入库
  1221. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  1222. if wh_internal:
  1223. wh_internal.approve_order()
  1224. order.update_child_cost()
  1225. order.wh_disassembly_create_voucher() # 生成入库凭证并审核
  1226. order.approve_uid = self.env.uid
  1227. order.approve_date = fields.Datetime.now(self)
  1228. order.state = 'done'
  1229. order.move_id.state = 'done'
  1230. return
  1231. def cancel_approved_order(self):
  1232. for order in self:
  1233. if order.state == 'feeding':
  1234. raise UserError(_('请不要重复撤销 %s') % self._description)
  1235. # 反审核入库到废品仓的移库单
  1236. wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
  1237. if wh_internal:
  1238. wh_internal.cancel_approved_order()
  1239. wh_internal.unlink()
  1240. order.line_in_ids.action_draft()
  1241. # 删除入库凭证
  1242. voucher, order.voucher_id = order.voucher_id, False
  1243. if voucher.state == 'done':
  1244. voucher.voucher_draft()
  1245. voucher.unlink()
  1246. order.approve_uid = False
  1247. order.approve_date = False
  1248. order.state = 'feeding'
  1249. order.move_id.state = 'draft'
  1250. @inherits()
  1251. def unlink(self):
  1252. for order in self:
  1253. if order.state != 'draft':
  1254. raise UserError(_('只删除草稿状态的单据'))
  1255. return order.move_id.unlink()
  1256. @api.model_create_multi
  1257. @create_name
  1258. @create_origin
  1259. def create(self, vals_list):
  1260. for vals in vals_list:
  1261. vals.update({'finance_category_id': self.env.ref(
  1262. 'finance.categ_ass_disass').id})
  1263. new_ids = super(WhDisassembly, self).create(vals_list)
  1264. for disassembly in new_ids:
  1265. disassembly.update_child_cost()
  1266. return new_ids
  1267. def write(self, vals):
  1268. if 'line_ids' in vals:
  1269. if 'line_in_ids' in vals:
  1270. vals.pop('line_in_ids')
  1271. if 'line_out_ids' in vals:
  1272. vals.pop('line_out_ids')
  1273. res = super(WhDisassembly, self).write(vals)
  1274. self.update_child_cost()
  1275. return res
  1276. @api.onchange('goods_id')
  1277. def onchange_goods_id(self):
  1278. if self.goods_id and not self.bom_id:
  1279. warehouse_id = self.env['warehouse'].search(
  1280. [('type', '=', 'stock')], limit=1)
  1281. self.line_out_ids = [(0,0,{'goods_id': self.goods_id.id, 'goods_uos_qty': 1, 'goods_qty': 1,
  1282. 'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
  1283. 'warehouse_dest_id': warehouse_id.id,
  1284. 'uom_id': self.goods_id.uom_id.id,
  1285. 'uos_id': self.goods_id.uos_id.id,
  1286. 'type': 'out',
  1287. })]
  1288. @api.onchange('goods_qty')
  1289. def onchange_goods_qty(self):
  1290. """
  1291. 改变商品数量时(wh_assembly 中的goods_qty) 根据物料清单的 数量的比例及成本价的计算
  1292. 算出新的组合件或者子件的 数量 (line.goods_qty / parent_line_goods_qty * self.goods_qty
  1293. line.goods_qty 子件商品数量
  1294. parent_line_goods_qty 物料清单组合件商品数量
  1295. self.goods_qty 所要的组合件的商品数量
  1296. line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
  1297. line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
  1298. )
  1299. :return:line_out_ids ,line_in_ids
  1300. """
  1301. warehouse_id = self.env['warehouse'].search(
  1302. [('type', '=', 'stock')], limit=1)
  1303. line_out_ids, line_in_ids = [], []
  1304. parent_line = self.bom_id.line_parent_ids
  1305. if warehouse_id and self.bom_id and parent_line and self.bom_id.line_child_ids:
  1306. cost, cost_unit = parent_line.goods_id \
  1307. .get_suggested_cost_by_warehouse(
  1308. warehouse_id, self.goods_qty)
  1309. line_out_ids.append((0,0,{
  1310. 'goods_id': parent_line.goods_id.id,
  1311. 'attribute_id': parent_line.attribute_id.id,
  1312. 'warehouse_id': self.env[
  1313. 'warehouse'].get_warehouse_by_type('production').id,
  1314. 'warehouse_dest_id': warehouse_id.id,
  1315. 'uom_id': parent_line.goods_id.uom_id.id,
  1316. 'goods_qty': self.goods_qty,
  1317. 'goods_uos_qty': self.goods_qty / parent_line.goods_id.conversion,
  1318. 'uos_id': parent_line.goods_id.uos_id.id,
  1319. 'cost_unit': cost_unit,
  1320. 'cost': cost,
  1321. 'type': 'out',
  1322. }))
  1323. line_in_ids = [(0,0,{
  1324. 'goods_id': line.goods_id.id,
  1325. 'attribute_id': line.attribute_id.id,
  1326. 'warehouse_id': warehouse_id.id,
  1327. 'warehouse_dest_id': self.env[
  1328. 'warehouse'].get_warehouse_by_type('production').id,
  1329. 'uom_id': line.goods_id.uom_id.id,
  1330. 'goods_qty': line.goods_qty / parent_line.goods_qty * self.goods_qty,
  1331. 'goods_uos_qty': line.goods_qty / parent_line.goods_qty * self.goods_qty / line.goods_id.conversion,
  1332. 'uos_id':line.goods_id.uos_id.id,
  1333. 'type': 'in',
  1334. }) for line in self.bom_id.line_child_ids]
  1335. # self.line_in_ids = False
  1336. # self.line_out_ids = False
  1337. # self.line_out_ids = line_out_ids
  1338. # self.line_in_ids = line_in_ids
  1339. if line_out_ids:
  1340. line_ids += line_out_ids
  1341. if line_in_ids:
  1342. line_ids += line_in_ids
  1343. self.line_ids = False
  1344. self.line_ids = line_ids
  1345. @api.onchange('bom_id')
  1346. def onchange_bom(self):
  1347. line_out_ids, line_in_ids = [], []
  1348. domain = {}
  1349. warehouse_id = self.env['warehouse'].search(
  1350. [('type', '=', 'stock')], limit=1)
  1351. if self.bom_id:
  1352. line_out_ids = []
  1353. for line in self.bom_id.line_parent_ids:
  1354. cost, cost_unit = line.goods_id \
  1355. .get_suggested_cost_by_warehouse(
  1356. warehouse_id, line.goods_qty)
  1357. line_out_ids.append((0,0,{
  1358. 'goods_id': line.goods_id,
  1359. 'attribute_id': line.attribute_id.id,
  1360. 'warehouse_id': self.env[
  1361. 'warehouse'].get_warehouse_by_type('production').id,
  1362. 'warehouse_dest_id': warehouse_id.id,
  1363. 'uom_id': line.goods_id.uom_id.id,
  1364. 'goods_qty': line.goods_qty,
  1365. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  1366. 'uos_id': line.goods_id.uos_id.id,
  1367. 'cost_unit': cost_unit,
  1368. 'cost': cost,
  1369. 'type': 'out',
  1370. }))
  1371. line_in_ids = [(0,0,{
  1372. 'goods_id': line.goods_id.id,
  1373. 'attribute_id': line.attribute_id.id,
  1374. 'warehouse_id': warehouse_id,
  1375. 'warehouse_dest_id': self.env[
  1376. 'warehouse'].get_warehouse_by_type('production').id,
  1377. 'uom_id': line.goods_id.uom_id.id,
  1378. 'goods_qty': line.goods_qty,
  1379. 'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
  1380. 'uos_id':line.goods_id.uos_id.id,
  1381. 'type': 'in',
  1382. }) for line in self.bom_id.line_child_ids]
  1383. self.line_in_ids = False
  1384. self.line_out_ids = False
  1385. else:
  1386. self.goods_qty = 1
  1387. if len(line_out_ids) == 1 and line_out_ids:
  1388. """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
  1389. 使试图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
  1390. self.goods_qty = line_out_ids[0][-1].get("goods_qty")
  1391. self.goods_id = line_out_ids[0][-1].get("goods_id")
  1392. domain = {'goods_id': [('id', '=', self.goods_id.id)]}
  1393. line_ids = []
  1394. if line_out_ids:
  1395. line_ids += line_out_ids
  1396. if line_in_ids:
  1397. line_ids += line_in_ids
  1398. self.line_ids = False
  1399. self.line_ids = line_ids
  1400. return {'domain': domain}
  1401. def update_bom(self):
  1402. for disassembly in self:
  1403. if disassembly.bom_id:
  1404. return disassembly.save_bom()
  1405. else:
  1406. return {
  1407. 'type': 'ir.actions.act_window',
  1408. 'res_model': 'save.bom.memory',
  1409. 'view_mode': 'form',
  1410. 'target': 'new',
  1411. }
  1412. def save_bom(self, name=''):
  1413. for disassembly in self:
  1414. line_child_ids = [[0, False, {
  1415. 'goods_id': line.goods_id.id,
  1416. 'goods_qty': line.goods_qty,
  1417. }] for line in disassembly.line_in_ids]
  1418. line_parent_ids = [[0, False, {
  1419. 'goods_id': line.goods_id.id,
  1420. 'goods_qty': line.goods_qty,
  1421. }] for line in disassembly.line_out_ids]
  1422. if disassembly.bom_id:
  1423. disassembly.bom_id.line_parent_ids.unlink()
  1424. disassembly.bom_id.line_child_ids.unlink()
  1425. disassembly.bom_id.write({
  1426. 'line_parent_ids': line_parent_ids,
  1427. 'line_child_ids': line_child_ids})
  1428. else:
  1429. bom_id = self.env['wh.bom'].create({
  1430. 'name': name,
  1431. 'type': 'disassembly',
  1432. 'line_parent_ids': line_parent_ids,
  1433. 'line_child_ids': line_child_ids,
  1434. })
  1435. disassembly.bom_id = bom_id
  1436. return True
  1437. class WhBom(models.Model):
  1438. _name = 'wh.bom'
  1439. _inherit = ['mail.thread']
  1440. _description = '物料清单'
  1441. BOM_TYPE = [
  1442. ('assembly', '组装单'),
  1443. ('disassembly', '拆卸单'),
  1444. ('outsource', '委外加工单'),
  1445. ]
  1446. name = fields.Char('物料清单名称',
  1447. help='组装/拆卸物料清单名称')
  1448. type = fields.Selection(
  1449. BOM_TYPE, '类型', default=lambda self: self.env.context.get('type'),
  1450. help='类型: 组装单、拆卸单')
  1451. version_control_id = fields.Many2one('core.value',
  1452. string='版本控制',
  1453. ondelete='restrict',
  1454. domain=[('type', '=', 'bom_version')],
  1455. help='版本控制:数据来源于系统基础配置')
  1456. line_parent_ids = fields.One2many(
  1457. 'wh.bom.line', 'bom_id', '组合件', domain=[('type', '=', 'parent')],
  1458. context={'type': 'parent'}, copy=True,
  1459. help='物料清单对应的组合件行')
  1460. line_child_ids = fields.One2many(
  1461. 'wh.bom.line', 'bom_id', '子件', domain=[('type', '=', 'child')],
  1462. context={'type': 'child'}, copy=True,
  1463. help='物料清单对应的子件行')
  1464. active = fields.Boolean('启用', default=True)
  1465. company_id = fields.Many2one(
  1466. 'res.company',
  1467. string='公司',
  1468. change_default=True,
  1469. default=lambda self: self.env.company)
  1470. goods_id = fields.Many2one('goods', related='line_parent_ids.goods_id', string='组合商品')
  1471. @api.constrains('line_parent_ids', 'line_child_ids')
  1472. def check_parent_child_unique(self):
  1473. """判断同一个产品不能是组合件又是子件"""
  1474. for wb in self:
  1475. for child_line in wb.line_child_ids:
  1476. for parent_line in wb.line_parent_ids:
  1477. if child_line.goods_id == parent_line.goods_id and child_line.attribute_id == parent_line.attribute_id:
  1478. raise UserError(_('组合件和子件不能相同,产品:%s') % parent_line.goods_id.name)
  1479. @api.onchange('line_parent_ids', 'line_child_ids')
  1480. def check_parent_length(self):
  1481. """判断组合件必填"""
  1482. for p in self:
  1483. if not p.name: # 新增记录,不处理
  1484. continue
  1485. elif not len(p.line_parent_ids) and len(p.line_child_ids): # 控制物料清单优先录入组合件,再录入子件
  1486. raise UserError(_('请先选择组合件,然后选择子件'))
  1487. details = fields.Html('明细', compute='_compute_details')
  1488. @api.depends('line_parent_ids', 'line_child_ids')
  1489. def _compute_details(self):
  1490. for v in self:
  1491. vl = {'col': [], 'val': []}
  1492. vl['col'] = ['', _('商品'), _('属性'), _('数量')]
  1493. for l in v.line_parent_ids:
  1494. vl['val'].append([_('组合件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  1495. for l in v.line_child_ids:
  1496. vl['val'].append([_('子件'), l.goods_id.name or '', l.attribute_id.name or '', l.goods_qty or ''])
  1497. v.details = v.company_id._get_html_table(vl)
  1498. def name_get(self):
  1499. ret = super().name_get()
  1500. result = []
  1501. for r in ret:
  1502. result.append((r[0], '%s %s' %
  1503. (r[1], self.browse(r[0]).version_control_id.name or '')))
  1504. return result
  1505. @api.model_create_multi
  1506. def create(self, vals_list):
  1507. ret = super().create(vals_list)
  1508. for r in ret:
  1509. for l in r.line_parent_ids:
  1510. if r.type == 'assembly' and l.goods_id.get_way != 'self':
  1511. l.goods_id.get_way = 'self'
  1512. if r.type == 'outsource' and l.goods_id.get_way != 'ous':
  1513. l.goods_id.get_way = 'ous'
  1514. return ret
  1515. class WhBomLine(models.Model):
  1516. _name = 'wh.bom.line'
  1517. _description = '物料清单明细'
  1518. BOM_LINE_TYPE = [
  1519. ('parent', '组合件'),
  1520. ('child', '子件'),
  1521. ]
  1522. @api.depends('goods_id')
  1523. def _compute_uom_uos(self):
  1524. for wml in self:
  1525. if wml.goods_id:
  1526. wml.uom_id = wml.goods_id.uom_id
  1527. wml.uos_id = wml.goods_id.uos_id
  1528. bom_id = fields.Many2one('wh.bom', '物料清单', ondelete='cascade',
  1529. help='子件行/组合件行对应的物料清单')
  1530. type = fields.Selection(
  1531. BOM_LINE_TYPE, '类型',
  1532. default=lambda self: self.env.context.get('type'),
  1533. help='类型: 组合件、子件')
  1534. goods_id = fields.Many2one('goods', '商品', ondelete='restrict',
  1535. help='子件行/组合件行上的商品')
  1536. goods_qty = fields.Float(
  1537. '数量', digits='Quantity',
  1538. default=1.0,
  1539. help='子件行/组合件行上的商品数量')
  1540. attribute_id = fields.Many2one('attribute', '属性', ondelete='restrict')
  1541. uom_id = fields.Many2one('uom', string='单位', ondelete='restrict', compute=_compute_uom_uos,
  1542. help='商品的计量单位', store=True)
  1543. uos_id = fields.Many2one('uom', string='辅助单位', ondelete='restrict', compute=_compute_uom_uos,
  1544. readonly=True, help='商品的辅助单位', store=True)
  1545. last_cost = fields.Float('最近成本', digits='Amount', compute='_get_last_cost')
  1546. company_id = fields.Many2one(
  1547. 'res.company',
  1548. string='公司',
  1549. change_default=True,
  1550. default=lambda self: self.env.company)
  1551. @api.constrains('goods_qty')
  1552. def check_goods_qty(self):
  1553. """验证商品数量大于0"""
  1554. for wbl in self:
  1555. if wbl.goods_qty <= 0:
  1556. raise UserError(_('商品 %s 的数量必须大于0') % wbl.goods_id.name)
  1557. def _get_last_cost(self):
  1558. for wbl in self:
  1559. wbl.last_cost = 0
  1560. last_in = self.env['wh.move.line'].search(
  1561. [('goods_id', '=', wbl.goods_id.id),
  1562. ('type', '=', 'in'),
  1563. ('state', '=', 'done')],
  1564. order="date desc", limit=1)
  1565. if last_in:
  1566. wbl.last_cost = last_in.cost_unit * wbl.goods_qty
上海开阖软件有限公司 沪ICP备12045867号-1