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.

1742 lines
73KB

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