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

1769 linhas
74KB

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