osbzr/gooderp_addons

View on GitHub
warehouse/models/production.py

Summary

Maintainability
F
6 days
Test Coverage
# -*- coding: utf-8 -*-

from odoo.osv import osv
from utils import inherits, inherits_after, \
    create_name, safe_division, create_origin
import odoo.addons.decimal_precision as dp
from itertools import islice
from odoo import models, fields, api
from odoo.exceptions import UserError


class WhAssembly(models.Model):
    _name = 'wh.assembly'
    _description = u'组装单'
    _inherit = ['mail.thread']
    _order = 'date DESC, id DESC'

    _inherits = {
        'wh.move': 'move_id',
    }

    state = fields.Selection([('draft', u'草稿'),
                              ('feeding', u'已发料'),
                              ('done', u'完成'),
                              ('cancel', u'已作废')],
                             u'状态', copy=False, default='draft',
                             index=True,
                             help=u'组装单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
    move_id = fields.Many2one(
        'wh.move', u'移库单', required=True, index=True, ondelete='cascade',
        help=u'组装单对应的移库单')
    bom_id = fields.Many2one(
        'wh.bom', u'物料清单', domain=[('type', '=', 'assembly')],
        context={'type': 'assembly'}, ondelete='restrict',
        readonly=True,
        states={'draft': [('readonly', False)], 'feeding': [
            ('readonly', False)]},
        help=u'组装单对应的物料清单')
    fee = fields.Float(
        u'组装费用', digits=dp.get_precision('Amount'),
        readonly=True,
        states={'draft': [('readonly', False)], 'feeding': [
            ('readonly', False)]},
        help=u'组装单对应的组装费用,组装费用+组装行入库成本作为子件的出库成本')
    is_many_to_many_combinations = fields.Boolean(u'专家模式', default=False, help=u"通用情况是一对多的组合,当为False时\
                            视图只能选则一个商品作为组合件,(选择物料清单后)此时选择数量会更改子件的数量,当为True时则可选择多个组合件,此时组合件商品数量\
                            不会自动影响子件的数量")
    goods_id = fields.Many2one('goods', string=u'组合件商品',
                               readonly=True,
                               states={'draft': [('readonly', False)], 'feeding': [('readonly', False)]})
    lot = fields.Char(u'批号')
    goods_qty = fields.Float(u'组合件数量', default=1, digits=dp.get_precision('Quantity'),
                             readonly=True,
                             states={'draft': [('readonly', False)], 'feeding': [
                                 ('readonly', False)]},
                             help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
    voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'入库凭证号')
    out_voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'出库凭证号')

    def apportion_cost(self, cost):
        for assembly in self:
            if not assembly.line_in_ids:
                continue

            collects = []
            ignore_move = [line.id for line in assembly.line_in_ids]
            for parent in assembly.line_in_ids:
                collects.append((
                    parent, parent.goods_id.get_suggested_cost_by_warehouse(
                        parent.warehouse_dest_id, parent.goods_qty,
                        lot_id=parent.lot_id,
                        attribute=parent.attribute_id,
                        ignore_move=ignore_move)[0]))

            amount_total, collect_parent_cost = sum(
                collect[1] for collect in collects), 0
            for parent, amount in islice(collects, 0, len(collects) - 1):
                parent_cost = safe_division(amount, amount_total) * cost
                collect_parent_cost += parent_cost
                parent.write({
                    'cost_unit': safe_division(
                        parent_cost, parent.goods_qty),
                    'cost': parent_cost,
                })

            # 最后一行数据使用总金额减去已经消耗的金额来计算
            last_parent_cost = cost - collect_parent_cost
            collects[-1][0].write({
                'cost_unit': safe_division(
                    last_parent_cost, collects[-1][0].goods_qty),
                'cost': last_parent_cost,
            })

        return True

    def update_parent_cost(self):
        for assembly in self:
            cost = sum(child.cost for child in assembly.line_out_ids) + \
                assembly.fee
            assembly.apportion_cost(cost)
        return True

    @api.onchange('goods_id')
    def onchange_goods_id(self):
        if self.goods_id and not self.bom_id:
            self.line_in_ids = [{'goods_id': self.goods_id.id, 'product_uos_qty': 1, 'goods_qty': 1,
                                 'uom_id': self.goods_id.uom_id.id, 'uos_id': self.goods_id.uos_id.id,
                                 'type': 'in'}]

    @api.onchange('goods_qty')
    def onchange_goods_qty(self):
        """
        改变商品数量时(wh_assembly 中的goods_qty) 根据物料清单的 数量的比例及成本价的计算
        算出新的组合件或者子件的 数量 (line.goods_qty / parent_line_goods_qty * self.goods_qty
        line.goods_qty 子件商品数量
        parent_line_goods_qty 物料清单组合件商品数量
        self.goods_qty 所要的组合件的商品数量
        line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
        line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
        )
        :return:line_out_ids ,line_in_ids
        """
        line_out_ids, line_in_ids = [], []
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        if self.bom_id:
            line_in_ids = [{'goods_id': line.goods_id.id,
                            'attribute_id': line.attribute_id.id,
                            'warehouse_id': self.env['warehouse'].get_warehouse_by_type(
                                'production').id,
                            'warehouse_dest_id': warehouse_id.id,
                            'uom_id': line.goods_id.uom_id.id,
                            'goods_qty': self.goods_qty,
                            'goods_uos_qty': self.goods_qty / line.goods_id.conversion,
                            'uos_id': line.goods_id.uos_id.id,
                            'type': 'in',
                            } for line in self.bom_id.line_parent_ids]
            parent_line_goods_qty = self.bom_id.line_parent_ids[0].goods_qty
            for line in self.bom_id.line_child_ids:
                cost, cost_unit = line.goods_id. \
                    get_suggested_cost_by_warehouse(
                        warehouse_id[0], line.goods_qty / parent_line_goods_qty * self.goods_qty)
                local_goods_qty = line.goods_qty / parent_line_goods_qty * self.goods_qty
                line_out_ids.append({
                    'goods_id': line.goods_id.id,
                    'attribute_id': line.attribute_id.id,
                    'warehouse_id': warehouse_id.id,
                    'warehouse_dest_id': self.env[
                        'warehouse'].get_warehouse_by_type('production'),
                    'uom_id': line.goods_id.uom_id.id,
                    'goods_qty':  local_goods_qty,
                    'cost_unit': cost_unit,
                    'cost': cost,
                    'goods_uos_qty': local_goods_qty / line.goods_id.conversion,
                    'uos_id': line.goods_id.uos_id.id,
                    'type': 'out',
                })
            self.line_in_ids = False
            self.line_out_ids = False
            if line_in_ids:
                self.line_in_ids = line_in_ids
            if line_out_ids:
                self.line_out_ids = line_out_ids
        elif self.line_in_ids:
            self.line_in_ids[0].goods_qty = self.goods_qty

    @api.one
    def check_parent_length(self):
        if not len(self.line_in_ids) or not len(self.line_out_ids):
            raise UserError(u'组合件和子件的商品必须存在')

    def create_voucher_line(self, data):
        return [self.env['voucher.line'].create(data_line) for data_line in data]

    def create_vourcher_line_data(self, assembly, voucher_row):
        """
        准备入库凭证行数据
        借:库存商品(商品上)
        贷:生产成本-基本生产成本(核算分类上)
        :param assembly: 组装单
        :param voucher_row: 入库凭证
        :return:
        """
        line_out_data, line_in_data = [], []
        line_out_credit = 0.0
        for line_out in assembly.line_out_ids:
            if line_out.cost:
                line_out_credit += line_out.cost

        if line_out_credit:  # 贷方行
            account_id = self.finance_category_id.account_id.id
            line_out_data.append({'credit': line_out_credit,
                                  'goods_id': False,
                                  'voucher_id': voucher_row.id,
                                  'account_id': account_id,
                                  'name': u'%s 原料 %s' % (assembly.move_id.name, assembly.move_id.note or '')
                                  })
        for line_in in assembly.line_in_ids:  # 借方行
            if line_in.cost:
                account_id = line_in.goods_id.category_id.account_id.id
                line_in_data.append({'debit': line_in.cost,
                                     'goods_id': line_in.goods_id.id,
                                     'goods_qty': line_in.goods_qty,
                                     'voucher_id': voucher_row.id,
                                     'account_id': account_id,
                                     'name': u'%s 成品 %s' % (assembly.move_id.name, assembly.move_id.note or '')})
        return line_out_data + line_in_data

    def wh_assembly_create_voucher_line(self, assembly, voucher_row):
        """
        创建入库凭证行
        :param assembly: 组装单
        :param voucher_row: 入库凭证
        :return:
        """
        voucher_line_data = []
        # 贷方行
        if assembly.fee:
            account_row = assembly.create_uid.company_id.operating_cost_account_id
            voucher_line_data.append({'name': u'组装费用', 'account_id': account_row.id,
                                      'credit': assembly.fee, 'voucher_id': voucher_row.id})
        voucher_line_data += self.create_vourcher_line_data(
            assembly, voucher_row)

        self.create_voucher_line(voucher_line_data)

    def pre_out_vourcher_line_data(self, assembly, voucher):
        """
        准备出库凭证行数据
        借:生产成本-基本生产成本(核算分类上)
        贷:库存商品(商品上)
        :param assembly: 组装单
        :param voucher: 出库凭证
        :return: 出库凭证行数据
        """
        line_out_data, line_in_data = [], []
        line_out_debit = 0.0
        for line_out in assembly.line_out_ids:
            if line_out.cost:
                line_out_debit += line_out.cost

        if line_out_debit:  # 借方行
            account_id = self.finance_category_id.account_id.id
            line_in_data.append({'debit': line_out_debit,
                                 'goods_id': False,
                                 'voucher_id': voucher.id,
                                 'account_id': account_id,
                                 'name': u'%s 成品 %s' % (assembly.move_id.name, assembly.move_id.note or '')
                                 })
        for line_out in assembly.line_out_ids:  # 贷方行
            if line_out.cost:
                account_id = line_out.goods_id.category_id.account_id.id
                line_out_data.append({'credit': line_out.cost,
                                      'goods_id': line_out.goods_id.id,
                                      'goods_qty': line_out.goods_qty,
                                      'voucher_id': voucher.id,
                                      'account_id': account_id,
                                      'name': u'%s 原料 %s' % (assembly.move_id.name, assembly.move_id.note or '')})
        return line_out_data + line_in_data

    def create_out_voucher_line(self, assembly, voucher):
        """
        创建出库凭证行
        :param assembly: 组装单
        :param voucher: 出库凭证
        :return:
        """
        voucher_line_data = self.pre_out_vourcher_line_data(assembly, voucher)

        self.create_voucher_line(voucher_line_data)

    def wh_assembly_create_voucher(self):
        """
        生成入库凭证并审核
        :return:
        """
        for assembly in self:
            voucher_row = self.env['voucher'].create({'date': assembly.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.wh_assembly_create_voucher_line(assembly, voucher_row)  # 入库凭证
            assembly.voucher_id = voucher_row.id
            voucher_row.voucher_done()

    def create_out_voucher(self):
        """
        生成出库凭证并审核
        :return:
        """
        for assembly in self:
            out_voucher = self.env['voucher'].create({'date': assembly.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.create_out_voucher_line(assembly, out_voucher)  # 出库凭证
            assembly.out_voucher_id = out_voucher.id
            out_voucher.voucher_done()

    @api.multi
    def check_is_child_enable(self):
        for child_line in self.line_out_ids:
            for parent_line in self.line_in_ids:
                if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
                    raise UserError(u'子件中不能包含与组合件中相同的 产品+属性,%s' % parent_line.goods_id.name)

    @api.multi
    def approve_feeding(self):
        ''' 发料 '''
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复发料')
            order.check_parent_length()
            order.check_is_child_enable()

            for line_out in order.line_out_ids:
                if line_out.state != 'done':
                    line_out.action_done()

            order.create_out_voucher()  # 生成出库凭证并审核
            order.state = 'feeding'
            return

    @api.multi
    def approve_order(self):
        ''' 成品入库 '''
        for order in self:
            if order.state == 'done':
                raise UserError(u'请不要重复执行成品入库')
            if order.state != 'feeding':
                raise UserError(u'请先投料')
            order.move_id.check_qc_result()  # 检验质检报告是否上传
            if order.lot:                     # 入库批次
                for line in order.line_in_ids:
                    line.lot = order.lot
            order.line_in_ids.action_done()  # 完成成品入库

            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.approve_order()

            order.update_parent_cost()
            order.wh_assembly_create_voucher()  # 生成入库凭证并审核

            order.approve_uid = self.env.uid
            order.approve_date = fields.Datetime.now(self)
            order.state = 'done'
            order.move_id.state = 'done'
            return

    @api.multi
    def cancel_approved_order(self):
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复撤销')
            # 反审核入库到废品仓的移库单
            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.cancel_approved_order()
                wh_internal.unlink()
            order.line_in_ids.action_draft()

            # 删除入库凭证
            voucher, order.voucher_id = order.voucher_id, False
            if voucher.state == 'done':
                voucher.voucher_draft()
            voucher.unlink()

            order.approve_uid = False
            order.approve_date = False
            order.state = 'feeding'
            order.move_id.state = 'draft'

    @api.multi
    @ inherits()
    def unlink(self):
        for order in self:
            if order.state != 'draft':
                raise UserError(u'只能删除草稿状态的单据')

        return order.move_id.unlink()

    @api.model
    @create_name
    @create_origin
    def create(self, vals):
        vals.update({'finance_category_id': self.env.ref(
            'finance.categ_ass_disass').id})
        self = super(WhAssembly, self).create(vals)
        self.update_parent_cost()
        return self

    @api.multi
    def write(self, vals):
        res = super(WhAssembly, self).write(vals)
        self.update_parent_cost()
        return res

    @api.onchange('bom_id')
    def onchange_bom(self):
        line_out_ids, line_in_ids = [], []
        domain = {}
        # TODO
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        if self.bom_id:
            line_in_ids = [{
                'type': 'in',
                'goods_id': line.goods_id.id,
                'warehouse_id': self.env['warehouse'].get_warehouse_by_type(
                    'production').id,
                'warehouse_dest_id': warehouse_id.id,
                'uom_id': line.goods_id.uom_id.id,
                'goods_qty': line.goods_qty,
                'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                'uos_id': line.goods_id.uos_id.id,
                'attribute_id': line.attribute_id.id,
            } for line in self.bom_id.line_parent_ids]

            for line in self.bom_id.line_child_ids:
                cost, cost_unit = line.goods_id. \
                    get_suggested_cost_by_warehouse(
                        warehouse_id[0], line.goods_qty)
                line_out_ids.append({
                    'type': 'out',
                    'goods_id': line.goods_id.id,
                    'warehouse_id': warehouse_id.id,
                    'warehouse_dest_id': self.env[
                            'warehouse'].get_warehouse_by_type('production').id,
                    'uom_id': line.goods_id.uom_id.id,
                    'goods_qty': line.goods_qty,
                    'cost_unit': cost_unit,
                    'cost': cost,
                    'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                    'uos_id': line.goods_id.uos_id.id,
                    'attribute_id': line.attribute_id.id,
                })
            self.line_in_ids = False
            self.line_out_ids = False
        else:
            self.goods_qty = 1

        if len(line_in_ids) == 1:
            """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
                使试图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
            self.is_many_to_many_combinations = False
            self.goods_qty = line_in_ids[0].get("goods_qty")
            self.goods_id = line_in_ids[0].get("goods_id")
            domain = {'goods_id': [('id', '=', self.goods_id.id)]}

        elif len(line_in_ids) > 1:
            self.is_many_to_many_combinations = True
        if line_out_ids:
            self.line_out_ids = line_out_ids
        # /odoo-china/odoo/fields.py[1664]行添加的参数
        # 调用self.line_in_ids = line_in_ids的时候,此时会为其额外添加一个参数(6, 0, [])
        # 在write函数的源代码中,会直接使用原表/odoo-china/odoo/osv/fields.py(839)来删除所有数据
        # 此时,上一步赋值的数据将会被直接删除,(不确定是bug,还是特性)
        if line_in_ids:
            self.line_in_ids = line_in_ids
        return {'domain': domain}

    @api.multi
    def update_bom(self):
        for assembly in self:
            if assembly.bom_id:
                return assembly.save_bom()
            else:
                return {
                    'type': 'ir.actions.act_window',
                    'res_model': 'save.bom.memory',
                    'view_mode': 'form',
                    'target': 'new',
                }

    def save_bom(self, name=''):
        for assembly in self:
            line_parent_ids = [[0, False, {
                'goods_id': line.goods_id.id,
                'goods_qty': line.goods_qty,
            }] for line in assembly.line_in_ids]

            line_child_ids = [[0, False, {
                'goods_id': line.goods_id.id,
                'goods_qty': line.goods_qty,
            }] for line in assembly.line_out_ids]

            if assembly.bom_id:
                assembly.bom_id.line_parent_ids.unlink()
                assembly.bom_id.line_child_ids.unlink()

                assembly.bom_id.write({
                    'line_parent_ids': line_parent_ids,
                    'line_child_ids': line_child_ids})
            else:
                bom_id = self.env['wh.bom'].create({
                    'name': name,
                    'type': 'assembly',
                    'line_parent_ids': line_parent_ids,
                    'line_child_ids': line_child_ids,
                })
                assembly.bom_id = bom_id

        return True


class outsource(models.Model):
    _name = 'outsource'
    _description = u'委外加工单'
    _inherit = ['mail.thread']
    _order = 'date DESC, id DESC'

    _inherits = {
        'wh.move': 'move_id',
    }

    state = fields.Selection([('draft', u'草稿'),
                              ('feeding', u'已发料'),
                              ('done', u'完成'),
                              ('cancel', u'已作废')],
                             u'状态', copy=False, default='draft',
                             index=True,
                             help=u'委外加工单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
    move_id = fields.Many2one('wh.move', u'移库单', required=True, index=True, ondelete='cascade',
                              help=u'委外加工单对应的移库单')
    bom_id = fields.Many2one('wh.bom', u'物料清单', domain=[('type', '=', 'outsource')],
                             context={'type': 'outsource'}, ondelete='restrict',
                             readonly=True,
                             states={'draft': [('readonly', False)], 'feeding': [
                                 ('readonly', False)]},
                             help=u'委外加工单对应的物料清单')
    is_many_to_many_combinations = fields.Boolean(u'专家模式', default=False, help=u"通用情况是一对多的组合,当为False时\
                            视图只能选则一个商品作为组合件,(选择物料清单后)此时选择数量会更改子件的数量,当为True时则可选择多个组合件,此时组合件商品数量\
                            不会自动影响子件的数量")
    goods_id = fields.Many2one('goods', string=u'组合件商品',
                               readonly=True,
                               states={'draft': [('readonly', False)], 'feeding': [('readonly', False)]})
    lot = fields.Char(u'批号')
    goods_qty = fields.Float(u'组合件数量', default=1, digits=dp.get_precision('Quantity'),
                             readonly=True,
                             states={'draft': [('readonly', False)], 'feeding': [
                                 ('readonly', False)]},
                             help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
    voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'入库凭证号')
    out_voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'出库凭证号')

    outsource_partner_id = fields.Many2one('partner', string=u'委外供应商',
                                           readonly=True,
                                           states={'draft': [('readonly', False)], 'feeding': [
                                               ('readonly', False)]},
                                           required=True)
    wh_assembly_id = fields.Many2one('wh.assembly', string=u'关联的组装单',
                                     readonly=True,
                                     states={'draft': [('readonly', False)], 'feeding': [('readonly', False)]})
    outsource_fee = fields.Float(string=u'委外费用',
                                 digits=dp.get_precision('Amount'),
                                 readonly=True,
                                 states={'draft': [('readonly', False)], 'feeding': [('readonly', False)]})
    invoice_id = fields.Many2one('money.invoice',
                                 copy=False,
                                 ondelete='set null',
                                 string=u'发票号')

    @api.onchange('goods_id')
    def onchange_goods_id(self):
        if self.goods_id and not self.bom_id:
            self.line_in_ids = False
            self.line_in_ids = [{'goods_id': self.goods_id.id, 'product_uos_qty': 1, 'goods_qty': 1,
                                 'uom_id': self.goods_id.uom_id.id, 'uos_id': self.goods_id.uos_id.id,
                                 'type': 'in'}]

    @api.onchange('goods_qty')
    def onchange_goods_qty(self):
        """
        改变商品数量时(outsource 中的goods_qty) 根据 物料清单 中的数量的比例
        计算出新的组合件或子件的数量
    (line.goods_qty / parent_line_goods_qty * self.goods_qty
        line.goods_qty 子件商品数量
        parent_line_goods_qty 物料清单组合件商品数量
        self.goods_qty 所要的组合件的商品数量
        line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
        line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
        )
        :return:line_out_ids ,line_in_ids
        """
        line_out_ids, line_in_ids = [], []
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        if self.bom_id:  # 存在 物料清单
            line_in_ids = [{'goods_id': line.goods_id.id,
                            'attribute_id': line.attribute_id.id,
                            'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
                            'warehouse_dest_id': warehouse_id.id,
                            'uom_id': line.goods_id.uom_id.id,
                            'goods_qty': self.goods_qty,
                            'goods_uos_qty': self.goods_qty / line.goods_id.conversion,
                            'uos_id': line.goods_id.uos_id.id,
                            'type': 'in'
                            } for line in self.bom_id.line_parent_ids]

            parent_line_goods_qty = self.bom_id.line_parent_ids[0].goods_qty

            for line in self.bom_id.line_child_ids:
                cost, cost_unit = line.goods_id.get_suggested_cost_by_warehouse(
                    warehouse_id[0], line.goods_qty / parent_line_goods_qty * self.goods_qty)

                local_goods_qty = line.goods_qty / parent_line_goods_qty * self.goods_qty

                line_out_ids.append({
                    'goods_id': line.goods_id.id,
                    'attribute_id': line.attribute_id.id,
                    'warehouse_id': warehouse_id.id,
                    'warehouse_dest_id': self.env['warehouse'].get_warehouse_by_type('production'),
                    'uom_id': line.goods_id.uom_id.id,
                    'goods_qty': local_goods_qty,
                    'cost_unit': cost_unit,
                    'cost': cost,
                    'goods_uos_qty': local_goods_qty / line.goods_id.conversion,
                    'uos_id': line.goods_id.uos_id.id,
                    'type': 'out',
                })

            self.line_in_ids = False
            self.line_out_ids = False
            self.line_out_ids = line_out_ids
            self.line_in_ids = line_in_ids
        elif self.line_in_ids:  # 不存在 物料清单,有组合单行
            self.line_in_ids[0].goods_qty = self.goods_qty

    @api.onchange('bom_id')
    def onchange_bom(self):
        line_out_ids, line_in_ids = [], []
        domain = {}
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        if self.bom_id:
            line_in_ids = [{
                'goods_id': line.goods_id.id,
                'attribute_id': line.attribute_id.id,
                'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
                'warehouse_dest_id': warehouse_id.id,
                'uom_id': line.goods_id.uom_id.id,
                'goods_qty': line.goods_qty,
                'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                'uos_id': line.goods_id.uos_id.id,
                'type': 'in',
            } for line in self.bom_id.line_parent_ids]

            for line in self.bom_id.line_child_ids:
                cost, cost_unit = line.goods_id. \
                    get_suggested_cost_by_warehouse(
                        warehouse_id[0], line.goods_qty)
                line_out_ids.append({
                    'goods_id': line.goods_id.id,
                    'attribute_id': line.attribute_id.id,
                    'warehouse_id': warehouse_id.id,
                    'warehouse_dest_id': self.env[
                        'warehouse'].get_warehouse_by_type('production').id,
                    'uom_id': line.goods_id.uom_id.id,
                    'goods_qty': line.goods_qty,
                    'cost_unit': cost_unit,
                    'cost': cost,
                    'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                    'uos_id': line.goods_id.uos_id.id,
                    'type': 'out',
                })
            self.line_in_ids = False
            self.line_out_ids = False
        else:
            self.goods_qty = 1

        if len(line_in_ids) == 1:
            """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
                使视图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
            self.is_many_to_many_combinations = False
            self.goods_qty = line_in_ids[0].get("goods_qty")
            self.goods_id = line_in_ids[0].get("goods_id")
            domain = {'goods_id': [('id', '=', self.goods_id.id)]}
        elif len(line_in_ids) > 1:
            self.is_many_to_many_combinations = True

        if line_out_ids:
            self.line_out_ids = line_out_ids
        if line_in_ids:
            self.line_in_ids = line_in_ids

        return {'domain': domain}

    def apportion_cost(self, cost):
        for outsource in self:
            if not outsource.line_in_ids:
                continue

            collects = []
            ignore_move = [line.id for line in outsource.line_in_ids]
            for parent in outsource.line_in_ids:
                collects.append((parent,
                                 parent.goods_id.get_suggested_cost_by_warehouse(
                                     parent.warehouse_dest_id, parent.goods_qty,
                                     lot_id=parent.lot_id,
                                     attribute=parent.attribute_id,
                                     ignore_move=ignore_move)[0]
                                 ))

            amount_total, collect_parent_cost = sum(
                collect[1] for collect in collects), 0
            for parent, amount in islice(collects, 0, len(collects) - 1):
                parent_cost = safe_division(amount, amount_total) * cost
                collect_parent_cost += parent_cost
                parent.write({
                    'cost_unit': safe_division(
                        parent_cost, parent.goods_qty),
                    'cost': parent_cost,
                })

            # 最后一行数据使用总金额减去已经消耗的金额来计算
            last_parent_cost = cost - collect_parent_cost
            collects[-1][0].write({
                'cost_unit': safe_division(
                    last_parent_cost, collects[-1][0].goods_qty),
                'cost': last_parent_cost,
            })

        return True

    def update_parent_cost(self):
        for outsource in self:
            cost = sum(child.cost for child in outsource.line_out_ids) + \
                outsource.outsource_fee
            outsource.apportion_cost(cost)
        return True

    @api.multi
    @inherits()
    def unlink(self):
        for order in self:
            if order.state != 'draft':
                raise UserError(u'只删除草稿状态的单据')

        return order.move_id.unlink()

    @api.model
    @create_name
    @create_origin
    def create(self, vals):
        vals.update({'finance_category_id': self.env.ref(
            'finance.categ_outsource').id})
        self = super(outsource, self).create(vals)
        self.update_parent_cost()
        return self

    @api.multi
    def write(self, vals):
        res = super(outsource, self).write(vals)
        self.update_parent_cost()
        return res

    @api.multi
    def check_parent_length(self):
        for outsource_line in self:
            if not len(outsource_line.line_in_ids) or not len(outsource_line.line_out_ids):
                raise UserError(u'委外加工单必须存在组合件和子件明细行。')

    def _create_money_invoice(self):
        categ = self.env.ref('money.core_category_purchase')
        source_id = self.env['money.invoice'].create({
            'name': self.name,
            'partner_id': self.outsource_partner_id.id,
            'category_id': categ.id,
            'date': fields.Date.context_today(self),
            'amount': self.outsource_fee,
            'reconciled': 0,
            'to_reconcile': self.outsource_fee,
            'date_due': fields.Date.context_today(self),
            'note': self.note or '',
        })
        if source_id:
            self.invoice_id = source_id.id
        return source_id

    def create_voucher_line(self, data):
        return [self.env['voucher.line'].create(data_line) for data_line in data]

    def create_vourcher_line_data(self, outsource, voucher_row):
        """
        准备入库凭证行数据
        借:库存商品(商品上)
        贷:生产成本-基本生产成本(核算分类上)
        :param outsource: 委外加工单
        :param voucher_row: 入库凭证
        :return:
        """
        line_out_data, line_in_data = [], []
        line_out_credit = 0.0
        for line_out in outsource.line_out_ids:
            if line_out.cost:
                line_out_credit += line_out.cost

        if line_out_credit:  # 贷方行
            account_id = self.finance_category_id.account_id.id
            line_out_data.append({'credit': line_out_credit,
                                  'goods_id': False,
                                  'voucher_id': voucher_row.id,
                                  'account_id': account_id,
                                  'name': u'%s 原料 %s' % (outsource.move_id.name, outsource.move_id.note or '')
                                  })
        for line_in in outsource.line_in_ids:  # 借方行
            if line_in.cost:
                account_id = line_in.goods_id.category_id.account_id.id
                line_in_data.append({'debit': line_in.cost,
                                     'goods_id': line_in.goods_id.id,
                                     'goods_qty': line_in.goods_qty,
                                     'voucher_id': voucher_row.id,
                                     'account_id': account_id,
                                     'name': u'%s 成品 %s' % (outsource.move_id.name, outsource.move_id.note or '')
                                     })
        return line_out_data + line_in_data

    def pre_out_vourcher_line_data(self, outsource, voucher):
        """
        准备出库凭证行数据
        借:委托加工物资(核算分类上)
        贷:库存商品(商品上)
        :param outsource: 委外加工单
        :param voucher: 出库凭证
        :return: 出库凭证行数据
        """
        line_out_data, line_in_data = [], []
        line_out_debit = 0.0
        for line_out in outsource.line_out_ids:
            if line_out.cost:
                line_out_debit += line_out.cost

        if line_out_debit:  # 借方行
            account_id = self.finance_category_id.account_id.id
            line_in_data.append({'debit': line_out_debit,
                                 'goods_id': False,
                                 'voucher_id': voucher.id,
                                 'account_id': account_id,
                                 'name': u'%s 成品 %s' % (outsource.move_id.name, outsource.move_id.note or '')
                                 })
        for line_out in outsource.line_out_ids:  # 贷方行
            if line_out.cost:
                account_id = line_out.goods_id.category_id.account_id.id
                line_out_data.append({'credit': line_out.cost,
                                      'goods_id': line_out.goods_id.id,
                                      'goods_qty': line_out.goods_qty,
                                      'voucher_id': voucher.id,
                                      'account_id': account_id,
                                      'name': u'%s 原料 %s' % (outsource.move_id.name, outsource.move_id.note or '')})
        return line_out_data + line_in_data

    def outsource_create_voucher_line(self, outsource, voucher_row):
        """
        创建入库凭证行
        :param outsource: 委外加工单
        :param voucher_row: 入库凭证
        :return:
        """
        voucher_line_data = []
        if outsource.outsource_fee:
            # 贷方行
            voucher_line_data.append({'name': u'委外费用',
                                      'account_id': self.env.ref('money.core_category_purchase').account_id.id, # 采购发票类别对应的科目
                                      'credit': outsource.outsource_fee, 'voucher_id': voucher_row.id})

        voucher_line_data += self.create_vourcher_line_data(
            outsource, voucher_row)
        self.create_voucher_line(voucher_line_data)

    def create_out_voucher_line(self, outsource, voucher):
        """
        创建出库凭证行
        :param outsource: 委外加工单
        :param voucher: 出库凭证
        :return:
        """
        voucher_line_data = self.pre_out_vourcher_line_data(outsource, voucher)

        self.create_voucher_line(voucher_line_data)

    def outsource_create_voucher(self):
        """
        生成入库凭证并审核
        :return:
        """
        for outsource in self:
            voucher_row = self.env['voucher'].create({'date': outsource.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.outsource_create_voucher_line(outsource, voucher_row)  # 入库凭证

            outsource.voucher_id = voucher_row.id
            voucher_row.voucher_done()

    def create_out_voucher(self):
        """
        生成出库凭证并审核
        :return:
        """
        for outsource in self:
            out_voucher = self.env['voucher'].create({'date': outsource.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.create_out_voucher_line(outsource, out_voucher)  # 出库凭证

            outsource.out_voucher_id = out_voucher.id
            out_voucher.voucher_done()

    @api.multi
    def check_is_child_enable(self):
        for child_line in self.line_out_ids:
            for parent_line in self.line_in_ids:
                if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
                    raise UserError(u'子件中不能包含与组合件中相同的 产品+属性,%s' % parent_line.goods_id.name)

    @api.multi
    def approve_feeding(self):
        ''' 发料 '''
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复发料')
            order.check_parent_length()
            order.check_is_child_enable()

            for line_out in order.line_out_ids:
                if line_out.state != 'done':
                    line_out.action_done()

            order.create_out_voucher()  # 生成出库凭证并审核
            order.state = 'feeding'
            return

    @api.multi
    def approve_order(self):
        ''' 成品入库 '''
        for order in self:
            if order.state == 'done':
                raise UserError(u'请不要重复执行成品入库')
            if order.state != 'feeding':
                raise UserError(u'请先投料')
            order.move_id.check_qc_result()  # 检验质检报告是否上传
            if order.lot:                     # 入库批次
                for line in order.line_in_ids:
                    line.lot = order.lot
            order.line_in_ids.action_done()  # 完成成品入库

            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.approve_order()

            # 如果委外费用存在,生成 结算单
            if order.outsource_fee:
                order._create_money_invoice()

            order.update_parent_cost()
            order.outsource_create_voucher()  # 生成入库凭证并审核

            order.approve_uid = self.env.uid
            order.approve_date = fields.Datetime.now(self)
            order.state = 'done'
            order.move_id.state = 'done'
            return

    @api.multi
    def cancel_approved_order(self):
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复撤销')
            # 反审核入库到废品仓的移库单
            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.cancel_approved_order()
                wh_internal.unlink()
            order.line_in_ids.action_draft()

            # 删除入库凭证
            voucher, order.voucher_id = order.voucher_id, False
            if voucher.state == 'done':
                voucher.voucher_draft()
            voucher.unlink()

            if order.invoice_id:
                order.invoice_id.money_invoice_draft()
                order.invoice_id.unlink()

            order.approve_uid = False
            order.approve_date = False
            order.state = 'feeding'
            order.move_id.state = 'draft'


class WhDisassembly(models.Model):
    _name = 'wh.disassembly'
    _description = u'拆卸单'
    _inherit = ['mail.thread']
    _order = 'date DESC, id DESC'

    _inherits = {
        'wh.move': 'move_id',
    }

    state = fields.Selection([('draft', u'草稿'),
                              ('feeding', u'已发料'),
                              ('done', u'完成'),
                              ('cancel', u'已作废')],
                             u'状态', copy=False, default='draft',
                             index=True,
                             help=u'拆卸单状态标识,新建时状态为草稿;发料后状态为已发料,可以多次投料;成品入库后状态为完成。')
    move_id = fields.Many2one(
        'wh.move', u'移库单', required=True, index=True, ondelete='cascade',
        help=u'拆卸单对应的移库单')
    bom_id = fields.Many2one(
        'wh.bom', u'物料清单', domain=[('type', '=', 'disassembly')],
        context={'type': 'disassembly'}, ondelete='restrict',
        readonly=True,
        states={'draft': [('readonly', False)], 'feeding': [
            ('readonly', False)]},
        help=u'拆卸单对应的物料清单')
    fee = fields.Float(
        u'拆卸费用', digits=dp.get_precision('Amount'),
        readonly=True,
        states={'draft': [('readonly', False)], 'feeding': [
            ('readonly', False)]},
        help=u'拆卸单对应的拆卸费用, 拆卸费用+拆卸行出库成本作为子件的入库成本')
    is_many_to_many_combinations = fields.Boolean(u'专家模式', default=False, help=u"通用情况是一对多的组合,当为False时\
                            视图只能选则一个商品作为组合件,(选择物料清单后)此时选择数量会更改子件的数量,当为True时则可选择多个组合件,此时组合件商品数量\
                            不会自动影响子件的数量")
    goods_id = fields.Many2one('goods', string=u'组合件商品',
                               readonly=True,
                               states={'draft': [('readonly', False)], 'feeding': [('readonly', False)]},)
    lot_id = fields.Many2one('wh.move.line', u'批号',
                             help=u'用于拆卸的组合件批号')
    goods_qty = fields.Float(u'组合件数量', default=1, digits=dp.get_precision('Quantity'),
                             readonly=True,
                             states={'draft': [('readonly', False)], 'feeding': [
                                 ('readonly', False)]},
                             help=u"(选择使用物料清单后)当更改这个数量的时候后自动的改变相应的子件的数量")
    voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'入库凭证号')
    out_voucher_id = fields.Many2one(
        'voucher', copy=False, ondelete='restrict', string=u'出库凭证号')

    def apportion_cost(self, cost):
        for assembly in self:
            if not assembly.line_in_ids:
                continue

            collects = []
            ignore_move = [line.id for line in assembly.line_in_ids]
            for child in assembly.line_in_ids:
                collects.append((
                    child, child.goods_id.get_suggested_cost_by_warehouse(
                        child.warehouse_dest_id, child.goods_qty,
                        lot_id=child.lot_id, attribute=child.attribute_id,
                        ignore_move=ignore_move)[0]))

            amount_total, collect_child_cost = \
                sum(collect[1] for collect in collects), 0
            for child, amount in islice(collects, 0, len(collects) - 1):
                child_cost = safe_division(amount, amount_total) * cost
                collect_child_cost += child_cost
                child.write({
                    'cost_unit': safe_division(
                            child_cost, child.goods_qty),
                    'cost': child_cost,
                })

            # 最后一行数据使用总金额减去已经消耗的金额来计算
            last_child_cost = cost - collect_child_cost
            collects[-1][0].write({
                'cost_unit': safe_division(
                    last_child_cost, collects[-1][0].goods_qty),
                'cost': last_child_cost,
            })

        return True

    def update_child_cost(self):
        for assembly in self:
            cost = sum(child.cost for child in assembly.line_out_ids) + \
                assembly.fee
            assembly.apportion_cost(cost)
        return True

    @api.one
    def check_parent_length(self):
        if not len(self.line_in_ids) or not len(self.line_out_ids):
            raise UserError(u'组合件和子件的商品必须存在')

    def create_voucher_line(self, data):
        return [self.env['voucher.line'].create(data_line) for data_line in data]

    def create_vourcher_line_data(self, disassembly, voucher_row):
        """
        准备入库凭证行数据
        借:库存商品(商品上)
        贷:生产成本-基本生产成本(核算分类上)
        :param disassembly: 拆卸单
        :param voucher_row: 入库库凭证
        :return:
        """
        line_out_data, line_in_data = [], []
        line_in_credit = 0.0
        for line_in in disassembly.line_in_ids:
            if line_in.cost:
                line_in_credit += line_in.cost

        if line_in_credit:  # 贷方行
            account_id = self.finance_category_id.account_id.id
            line_out_data.append({'credit': line_in_credit,
                                  'goods_id': False,
                                  'voucher_id': voucher_row.id,
                                  'account_id': account_id,
                                  'name': u'%s 原料 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
                                  })
        for line_in in disassembly.line_in_ids:  # 借方行
            if line_in.cost:
                account_id = line_in.goods_id.category_id.account_id.id
                line_in_data.append({'debit': line_in.cost,
                                     'goods_id': line_in.goods_id.id,
                                     'goods_qty': line_in.goods_qty,
                                     'voucher_id': voucher_row.id,
                                     'account_id': account_id,
                                     'name': u'%s 成品 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
                                     })
        return line_out_data + line_in_data

    def pre_out_vourcher_line_data(self, disassembly, voucher):
        """
        准备出库凭证行数据
        借:生产成本-基本生产成本(核算分类上)
        贷:库存商品(商品上)
        :param disassembly: 拆卸单
        :param voucher: 出库凭证
        :return: 出库凭证行数据
        """
        line_out_data, line_in_data = [], []
        line_out_debit = 0.0
        for line_out in disassembly.line_out_ids:
            line_out_debit += line_out.cost

        if line_out_debit:  # 借方行
            account_id = self.finance_category_id.account_id.id
            line_in_data.append({'debit': line_out_debit,
                                 'goods_id': False,
                                 'voucher_id': voucher.id,
                                 'account_id': account_id,
                                 'name': u'%s 成品 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')
                                 })
        for line_out in disassembly.line_out_ids:  # 贷方行
            if line_out.cost:
                account_id = line_out.goods_id.category_id.account_id.id
                line_out_data.append({'credit': line_out.cost + disassembly.fee,
                                      'goods_id': line_out.goods_id.id,
                                      'goods_qty': line_out.goods_qty,
                                      'voucher_id': voucher.id,
                                      'account_id': account_id,
                                      'name': u'%s 原料 %s' % (disassembly.move_id.name, disassembly.move_id.note or '')})
        return line_out_data + line_in_data

    def wh_disassembly_create_voucher_line(self, disassembly, voucher_row):
        """
        创建入库凭证行
        :param disassembly:
        :param voucher_row:
        :return:
        """
        voucher_line_data = []
        voucher_line_data += self.create_vourcher_line_data(
            disassembly, voucher_row)
        self.create_voucher_line(voucher_line_data)

    def create_out_voucher_line(self, disassembly, voucher):
        """
        创建出库凭证行
        :param disassembly: 拆卸单
        :param voucher: 出库凭证
        :return:
        """
        voucher_line_data = []
        # 借方行
        if disassembly.fee:
            account = disassembly.create_uid.company_id.operating_cost_account_id
            voucher_line_data.append({'name': u'拆卸费用', 'account_id': account.id,
                                      'debit': disassembly.fee, 'voucher_id': voucher.id})
        voucher_line_data += self.pre_out_vourcher_line_data(
            disassembly, voucher)

        self.create_voucher_line(voucher_line_data)

    def wh_disassembly_create_voucher(self):
        """
        生成入库凭证并审核
        :return:
        """
        for disassembly in self:
            voucher_row = self.env['voucher'].create(
                {'date': disassembly.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.wh_disassembly_create_voucher_line(
                disassembly, voucher_row)   # 入库凭证
            disassembly.voucher_id = voucher_row.id
            voucher_row.voucher_done()

    def create_out_voucher(self):
        """
        生成出库凭证并审核
        :return:
        """
        for disassembly in self:
            out_voucher = self.env['voucher'].create(
                {'date': disassembly.date, 'ref': '%s,%s' % (self._name, self.id)})
            self.create_out_voucher_line(disassembly, out_voucher)  # 出库凭证
            disassembly.out_voucher_id = out_voucher.id
            out_voucher.voucher_done()

    @api.multi
    def check_is_child_enable(self):
        for child_line in self.line_in_ids:
            for parent_line in self.line_out_ids:
                if child_line.goods_id.id == parent_line.goods_id.id and child_line.attribute_id.id == parent_line.attribute_id.id:
                    raise UserError(u'子件中不能包含与组合件中相同的 产品+属性,%s' % parent_line.goods_id.name)

    def approve_feeding(self):
        ''' 发料 '''
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复发料')
            order.check_parent_length()
            order.check_is_child_enable()

            for line_out in order.line_out_ids:
                if line_out.state != 'done':
                    if order.lot_id:      #出库批次
                        line_out.lot_id = order.lot_id
                    line_out.action_done()

            order.create_out_voucher()   # 生成出库凭证并审核
            order.state = 'feeding'
            return

    @api.multi
    def approve_order(self):
        ''' 成品入库 '''
        for order in self:
            if order.state == 'done':
                raise UserError(u'请不要重复执行成品入库')
            if order.state != 'feeding':
                raise UserError(u'请先投料')
            order.move_id.check_qc_result()  # 检验质检报告是否上传
            order.line_in_ids.action_done()  # 完成成品入库

            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.approve_order()

            order.update_child_cost()
            order.wh_disassembly_create_voucher()  # 生成入库凭证并审核

            order.approve_uid = self.env.uid
            order.approve_date = fields.Datetime.now(self)
            order.state = 'done'
            order.move_id.state = 'done'
            return

    @api.multi
    def cancel_approved_order(self):
        for order in self:
            if order.state == 'feeding':
                raise UserError(u'请不要重复撤销')
            # 反审核入库到废品仓的移库单
            wh_internal = self.env['wh.internal'].search([('ref', '=', order.move_id.name)])
            if wh_internal:
                wh_internal.cancel_approved_order()
                wh_internal.unlink()
            order.line_in_ids.action_draft()
            # 删除入库凭证
            voucher, order.voucher_id = order.voucher_id, False
            if voucher.state == 'done':
                voucher.voucher_draft()
            voucher.unlink()

            order.approve_uid = False
            order.approve_date = False
            order.state = 'feeding'
            order.move_id.state = 'draft'

    @api.multi
    @inherits()
    def unlink(self):
        for order in self:
            if order.state != 'draft':
                raise UserError(u'只删除草稿状态的单据')

        return order.move_id.unlink()

    @api.model
    @create_name
    @create_origin
    def create(self, vals):
        vals.update({'finance_category_id': self.env.ref(
            'finance.categ_ass_disass').id})
        self = super(WhDisassembly, self).create(vals)
        self.update_child_cost()
        return self

    @api.multi
    def write(self, vals):
        res = super(WhDisassembly, self).write(vals)
        self.update_child_cost()
        return res

    @api.onchange('goods_id')
    def onchange_goods_id(self):
        if self.goods_id and not self.bom_id:
            warehouse_id = self.env['warehouse'].search(
                [('type', '=', 'stock')], limit=1)
            self.line_out_ids = [{'goods_id': self.goods_id.id, 'product_uos_qty': 1, 'goods_qty': 1,
                                  'warehouse_id': self.env['warehouse'].get_warehouse_by_type('production').id,
                                  'warehouse_dest_id': warehouse_id.id,
                                  'uom_id': self.goods_id.uom_id.id,
                                  'uos_id': self.goods_id.uos_id.id,
                                  'type': 'out',
                                  }]

    @api.onchange('goods_qty')
    def onchange_goods_qty(self):
        """
        改变商品数量时(wh_assembly 中的goods_qty) 根据物料清单的 数量的比例及成本价的计算
        算出新的组合件或者子件的 数量 (line.goods_qty / parent_line_goods_qty * self.goods_qty
        line.goods_qty 子件商品数量
        parent_line_goods_qty 物料清单组合件商品数量
        self.goods_qty 所要的组合件的商品数量
        line.goods_qty /parent_line_goods_qty 得出子件和组合件的比例
        line.goods_qty / parent_line_goods_qty * self.goods_qty 得出子件实际的数量的数量
        )
        :return:line_out_ids ,line_in_ids
        """
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        line_out_ids, line_in_ids = [], []
        parent_line = self.bom_id.line_parent_ids
        if warehouse_id and self.bom_id and parent_line and self.bom_id.line_child_ids:
            cost, cost_unit = parent_line.goods_id \
                .get_suggested_cost_by_warehouse(
                    warehouse_id, self.goods_qty)

            line_out_ids.append({
                'goods_id': parent_line.goods_id.id,
                'attribute_id': parent_line.attribute_id.id,
                'warehouse_id': self.env[
                    'warehouse'].get_warehouse_by_type('production').id,
                'warehouse_dest_id': warehouse_id.id,
                'uom_id': parent_line.goods_id.uom_id.id,
                'goods_qty': self.goods_qty,
                'goods_uos_qty': self.goods_qty / parent_line.goods_id.conversion,
                'uos_id': parent_line.goods_id.uos_id.id,
                'cost_unit': cost_unit,
                'cost': cost,
                'type': 'out',
            })

            line_in_ids = [{
                'goods_id': line.goods_id.id,
                'attribute_id': line.attribute_id.id,
                'warehouse_id': warehouse_id.id,
                'warehouse_dest_id': self.env[
                    'warehouse'].get_warehouse_by_type('production').id,
                'uom_id': line.goods_id.uom_id.id,
                'goods_qty': line.goods_qty / parent_line.goods_qty * self.goods_qty,
                'goods_uos_qty': line.goods_qty / parent_line.goods_qty * self.goods_qty / line.goods_id.conversion,
                'uos_id':line.goods_id.uos_id.id,
                'type': 'in',
            } for line in self.bom_id.line_child_ids]

            self.line_in_ids = False
            self.line_out_ids = False
            self.line_out_ids = line_out_ids
            self.line_in_ids = line_in_ids
        elif self.line_out_ids:
            self.line_out_ids[0].goods_qty = self.goods_qty

    @api.onchange('bom_id')
    def onchange_bom(self):
        line_out_ids, line_in_ids = [], []
        domain = {}
        # TODO
        warehouse_id = self.env['warehouse'].search(
            [('type', '=', 'stock')], limit=1)
        if self.bom_id:
            line_out_ids = []
            for line in self.bom_id.line_parent_ids:
                cost, cost_unit = line.goods_id \
                    .get_suggested_cost_by_warehouse(
                        warehouse_id, line.goods_qty)
                line_out_ids.append({
                    'goods_id': line.goods_id,
                    'attribute_id': line.attribute_id.id,
                    'warehouse_id': self.env[
                        'warehouse'].get_warehouse_by_type('production').id,
                    'warehouse_dest_id': warehouse_id.id,
                    'uom_id': line.goods_id.uom_id.id,
                    'goods_qty': line.goods_qty,
                    'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                    'uos_id': line.goods_id.uos_id.id,
                    'cost_unit': cost_unit,
                    'cost': cost,
                    'type': 'out',
                })

            line_in_ids = [{
                'goods_id': line.goods_id.id,
                'attribute_id': line.attribute_id.id,
                'warehouse_id': warehouse_id,
                'warehouse_dest_id': self.env[
                    'warehouse'].get_warehouse_by_type('production').id,
                'uom_id': line.goods_id.uom_id.id,
                'goods_qty': line.goods_qty,
                'goods_uos_qty': line.goods_qty / line.goods_id.conversion,
                'uos_id':line.goods_id.uos_id.id,
                'type': 'in',
            } for line in self.bom_id.line_child_ids]

            self.line_in_ids = False
            self.line_out_ids = False
        else:
            self.goods_qty = 1
        if len(line_out_ids) == 1 and line_out_ids:
            """当物料清单中只有一个组合件的时候,默认本单据只有一个组合件 设置is_many_to_many_combinations 为False
             使试图只能在 many2one中选择一个商品(并且只能选择在物料清单中的商品),并且回写数量"""
            self.is_many_to_many_combinations = ''
            self.goods_qty = line_out_ids[0].get("goods_qty")
            self.goods_id = line_out_ids[0].get("goods_id")
            domain = {'goods_id': [('id', '=', self.goods_id.id)]}

        elif len(line_out_ids) > 1:
            self.is_many_to_many_combinations = True
        if line_out_ids:
            self.line_out_ids = line_out_ids
        if line_in_ids:
            self.line_in_ids = line_in_ids
        return {'domain': domain}

    @api.multi
    def update_bom(self):
        for disassembly in self:
            if disassembly.bom_id:
                return disassembly.save_bom()
            else:
                return {
                    'type': 'ir.actions.act_window',
                    'res_model': 'save.bom.memory',
                    'view_mode': 'form',
                    'target': 'new',
                }

    def save_bom(self, name=''):
        for disassembly in self:
            line_child_ids = [[0, False, {
                'goods_id': line.goods_id.id,
                'goods_qty': line.goods_qty,
            }] for line in disassembly.line_in_ids]

            line_parent_ids = [[0, False, {
                'goods_id': line.goods_id.id,
                'goods_qty': line.goods_qty,
            }] for line in disassembly.line_out_ids]

            if disassembly.bom_id:
                disassembly.bom_id.line_parent_ids.unlink()
                disassembly.bom_id.line_child_ids.unlink()

                disassembly.bom_id.write({
                    'line_parent_ids': line_parent_ids,
                    'line_child_ids': line_child_ids})
            else:
                bom_id = self.env['wh.bom'].create({
                    'name': name,
                    'type': 'disassembly',
                    'line_parent_ids': line_parent_ids,
                    'line_child_ids': line_child_ids,
                })
                disassembly.bom_id = bom_id

        return True


class WhBom(osv.osv):
    _name = 'wh.bom'
    _description = u'物料清单'

    BOM_TYPE = [
        ('assembly', u'组装单'),
        ('disassembly', u'拆卸单'),
        ('outsource', u'委外加工单'),
    ]

    name = fields.Char(u'物料清单名称',
                       help=u'组装/拆卸物料清单名称')
    type = fields.Selection(
        BOM_TYPE, u'类型', default=lambda self: self.env.context.get('type'),
        help=u'类型: 组装单、拆卸单')
    line_parent_ids = fields.One2many(
        'wh.bom.line', 'bom_id', u'组合件', domain=[('type', '=', 'parent')],
        context={'type': 'parent'}, copy=True,
        help=u'物料清单对应的组合件行')
    line_child_ids = fields.One2many(
        'wh.bom.line', 'bom_id', u'子件', domain=[('type', '=', 'child')],
        context={'type': 'child'}, copy=True,
        help=u'物料清单对应的子件行')
    active = fields.Boolean(u'启用', default=True)
    company_id = fields.Many2one(
        'res.company',
        string=u'公司',
        change_default=True,
        default=lambda self: self.env['res.company']._company_default_get())
    goods_id = fields.Many2one('goods', related='line_parent_ids.goods_id', string=u'组合商品')

    @api.one
    @api.constrains('line_parent_ids', 'line_child_ids')
    def check_parent_child_unique(self):
        """判断同一个产品不能是组合件又是子件"""
        for child_line in self.line_child_ids:
            for parent_line in self.line_parent_ids:
                if child_line.goods_id == parent_line.goods_id and child_line.attribute_id == parent_line.attribute_id:
                    raise UserError(u'组合件和子件不能相同,产品:%s' % parent_line.goods_id.name)


class WhBomLine(osv.osv):
    _name = 'wh.bom.line'
    _description = u'物料清单明细'

    BOM_LINE_TYPE = [
        ('parent', u'组合件'),
        ('child', u'子间'),
    ]

    bom_id = fields.Many2one('wh.bom', u'物料清单', ondelete='cascade',
                             help=u'子件行/组合件行对应的物料清单')
    type = fields.Selection(
        BOM_LINE_TYPE, u'类型',
        default=lambda self: self.env.context.get('type'),
        help=u'类型: 组合件、子间')
    goods_id = fields.Many2one('goods', u'商品', default=1, ondelete='restrict',
                               help=u'子件行/组合件行上的商品')
    goods_qty = fields.Float(
        u'数量', digits=dp.get_precision('Quantity'),
        default=1.0,
        help=u'子件行/组合件行上的商品数量')
    attribute_id = fields.Many2one('attribute', u'属性', ondelete='restrict')
    company_id = fields.Many2one(
        'res.company',
        string=u'公司',
        change_default=True,
        default=lambda self: self.env['res.company']._company_default_get())

    @api.one
    @api.constrains('goods_qty')
    def check_goods_qty(self):
        """验证商品数量大于0"""
        if self.goods_qty <= 0:
            raise UserError(u'商品 %s 的数量必须大于0' % self.goods_id.name)