|  | from odoo import models, fields, api
from odoo.exceptions import UserError
class WhMove(models.Model):
    _name = 'wh.move'
    _description = '移库单'
    MOVE_STATE = [
        ('draft', '草稿'),
        ('done', '已完成'),
        ('cancel', '已作废'),]
    @api.depends('line_out_ids', 'line_in_ids')
    def _compute_total_qty(self):
        for wm in self:
            goods_total = 0
            if wm.line_in_ids:
                # 入库商品总数
                goods_total = sum(line.goods_qty for line in wm.line_in_ids)
            elif wm.line_out_ids:
                # 出库商品总数
                goods_total = sum(line.goods_qty for line in wm.line_out_ids)
            wm.total_qty = goods_total
    @api.model
    def _get_default_warehouse_impl(self):
        if self.env.context.get('warehouse_type', 'stock'):
            return self.env['warehouse'].get_warehouse_by_type(
                self.env.context.get('warehouse_type', 'stock'))
    @api.model
    def _get_default_warehouse_dest_impl(self):
        if self.env.context.get('warehouse_dest_type', 'stock'):
            return self.env['warehouse'].get_warehouse_by_type(
                self.env.context.get('warehouse_dest_type', 'stock'))
    @api.model
    def _get_default_warehouse(self):
        '''获取调出仓库'''
        return self._get_default_warehouse_impl()
    @api.model
    def _get_default_warehouse_dest(self):
        '''获取调入仓库'''
        return self._get_default_warehouse_dest_impl()
    origin = fields.Char('移库类型', required=True,
                         help='移库类型')
    name = fields.Char('单据编号', copy=False, default='/',
                       help='单据编号,创建时会自动生成')
    ref = fields.Char('外部单号')
    state = fields.Selection(MOVE_STATE, '状态', copy=False, default='draft',
                             index=True,
                             help='移库单状态标识,新建时状态为草稿;确认后状态为已确认')
    partner_id = fields.Many2one('partner', '业务伙伴', ondelete='restrict',
                                 help='该单据对应的业务伙伴')
    date = fields.Date('单据日期', required=True, copy=False, default=fields.Date.context_today,
                       help='单据创建日期,默认为当前天')
    warehouse_id = fields.Many2one('warehouse', '调出仓库',
                                   ondelete='restrict',
                                   required=True,
                                   domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
                                   default=_get_default_warehouse,
                                   help='移库单的来源仓库')
    warehouse_dest_id = fields.Many2one('warehouse', '调入仓库',
                                        ondelete='restrict',
                                        required=True,
                                        domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
                                        default=_get_default_warehouse_dest,
                                        help='移库单的目的仓库')
    approve_uid = fields.Many2one('res.users', '确认人',
                                  copy=False, ondelete='restrict',
                                  help='移库单的确认人')
    approve_date = fields.Datetime('确认日期', copy=False)
    line_ids = fields.One2many('wh.move.line', 'move_id', '出入库明细', copy=False)
    line_out_ids = fields.One2many('wh.move.line', 'move_id', '出库明细',
                                   domain=[
                                       ('type', 'in', ['out', 'internal'])],
                                   copy=True,
                                   help='出库类型的移库单对应的出库明细')
    line_in_ids = fields.One2many('wh.move.line', 'move_id', '入库明细',
                                  domain=[('type', '=', 'in')],
                                  context={'type': 'in'}, copy=True,
                                  help='入库类型的移库单对应的入库明细')
    in_goods_id = fields.Many2one('goods', string='入库商品',
                                  related='line_in_ids.goods_id')
    out_goods_id = fields.Many2one('goods', string='出库商品',
                                   related='line_out_ids.goods_id')
    auxiliary_id = fields.Many2one(
        'auxiliary.financing', '辅助核算', help='辅助核算是对账务处理的一种补充,即实现更广泛的账务处理,\
        以适应企业管理和决策的需要.辅助核算一般通过核算项目来实现', ondelete='restrict')
    note = fields.Text('备注',
                       copy=False,
                       help='可以为该单据添加一些需要的标识信息')
    total_qty = fields.Float('商品总数', compute=_compute_total_qty,
                             digits='Quantity', store=True,
                             help='该移库单的入/出库明细行包含的商品总数')
    user_id = fields.Many2one(
        'res.users',
        '经办人',
        ondelete='restrict',
        default=lambda self: self.env.user,
        help='单据经办人'
    )
    express_type = fields.Char(string='承运商')
    express_code = fields.Char('快递单号', copy=False)
    company_id = fields.Many2one(
        'res.company',
        string='公司',
        change_default=True,
        default=lambda self: self.env.company)
    qc_result = fields.Binary('质检报告',
                              help='点击上传质检报告')
    finance_category_id = fields.Many2one(
        'core.category',
        string='收发类别',
        ondelete='restrict',
        domain=[('type', '=', 'finance'), ('note', '!=', '由系统创建')],
        context={'type': 'finance'},
        help='生成凭证时从此字段上取商品科目的对方科目',
    )
    details = fields.Html('明细',compute='_compute_details')
    @api.depends('line_in_ids', 'line_out_ids')
    def _compute_details(self):
        for v in self:
            vl = {'col':[],'val':[]}
            vl['col'] = ['商品','数量']
            for l in v.line_in_ids:
                vl['val'].append([l.goods_id.name,l.goods_qty])
            for l in v.line_out_ids:
                vl['val'].append([l.goods_id.name,l.goods_qty])
            v.details = v.company_id._get_html_table(vl)
    def scan_barcode_move_line_operation(self, line, conversion):
        """
        在原移库明细行中更新数量和辅助数量,不创建新行
        :return:
        """
        line.goods_qty += 1
        line.goods_uos_qty = line.goods_qty / conversion
        return True
    def scan_barcode_inventory_line_operation(self, line, conversion):
        '''盘点单明细行数量增加'''
        line.inventory_qty += 1
        line.inventory_uos_qty = line.inventory_qty / conversion
        line.difference_qty += 1
        line.difference_uos_qty = line.difference_qty / conversion
        return True
    def scan_barcode_move_in_out_operation(self, move, att, conversion, goods, val):
        """
        对仓库各种移库单据上扫码的统一处理
        :return: 是否创建新的明细行
        """
        create_line = False
        loop_field = 'line_in_ids' if val['type'] == 'in' else 'line_out_ids'
        for line in move[loop_field]:
            line.cost_unit = (line.goods_id.price if val['type'] in ['out', 'internal']
                              else line.goods_id.cost)  # 其他出入库单 、内部调拨单
            line.price_taxed = (line.goods_id.price if val['type'] == 'out'
                                else line.goods_id.cost)  # 采购或销售单据
            # 如果商品属性或商品上存在条码,且明细行上已经存在该商品,则数量累加
            if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
                create_line = self.scan_barcode_move_line_operation(
                    line, conversion)
        return create_line
    def scan_barcode_inventory_operation(self, move, att, conversion, goods, val):
        '''盘点单扫码操作'''
        create_line = False
        for line in move.line_ids:
            # 如果商品属性上存在条码 或 商品上存在条码
            if (att and line.attribute_id == att) or (goods and line.goods_id == goods):
                create_line = self.scan_barcode_inventory_line_operation(
                    line, conversion)
        return create_line
    def scan_barcode_each_model_operation(self, model_name, order_id, att, goods, conversion):
        val = {}
        create_line = False  # 是否创建新的明细行
        order = self.env[model_name].browse(order_id)
        if model_name in ['wh.out', 'wh.in', 'wh.internal']:
            move = order.move_id
        # 在其他出库单上扫描条码
        if model_name == 'wh.out':
            val['type'] = 'out'
        # 在其他入库单上扫描条码
        if model_name == 'wh.in':
            val['type'] = 'in'
        # 销售出入库单的二维码
        if model_name == 'sell.delivery':
            move = order.sell_move_id
            val['type'] = order.is_return and 'in' or 'out'
        # 采购出入库单的二维码
        if model_name == 'buy.receipt':
            move = order.buy_move_id
            val['type'] = order.is_return and 'out' or 'in'
        # 调拔单的扫描条码
        if model_name == 'wh.internal':
            val['type'] = 'internal'
        if model_name != 'wh.inventory':
            create_line = self.scan_barcode_move_in_out_operation(
                move, att, conversion, goods, val)
        # 盘点单的扫码
        if model_name == 'wh.inventory':
            move = order
            create_line = self.scan_barcode_inventory_operation(
                move, att, conversion, goods, val)
        return move, create_line, val
    
    def check_goods_qty(self, goods, attribute, warehouse):
        '''SQL来取指定商品,属性,仓库,的当前剩余数量'''
        if attribute:
            change_conditions = "AND line.attribute_id = %s" % attribute.id
        elif goods:
            change_conditions = "AND line.goods_id = %s" % goods.id
        else:
            change_conditions = "AND 1 = 0"
        self.env.cr.execute('''
                       SELECT sum(line.qty_remaining) as qty
                       FROM wh_move_line line
                       WHERE line.warehouse_dest_id = %s
                             AND line.state = 'done'
                             %s
                   ''' % (warehouse.id, change_conditions,))
        return self.env.cr.fetchone()
    def prepare_move_line_data(self, att, val, goods, move):
        """
        准备移库单明细数据
        :return: 字典
        """
        # 若传入的商品属性 att 上条码存在则取属性对应的商品,否则取传入的商品 goods
        goods = att and att.goods_id or goods
        goods_id = goods.id
        uos_id = goods.uos_id.id
        uom_id = goods.uom_id.id
        tax_rate = goods.tax_rate
        attribute_id = att and att.id or False
        conversion = goods.conversion
        # 采购入库取成本价,销售退货取销售价;采购退货取成本价,销售发货取销售价
        price_taxed = move._name == 'buy.receipt' and goods.cost or goods.price
        cost_unit = val.get('type') != 'in' and 0 or goods.cost
        val.update({
            'goods_id': goods_id,
            'attribute_id': attribute_id,
            'warehouse_id': move.warehouse_id.id,
            'uos_id': uos_id,
            'uom_id': uom_id,
        })
        if move._name != 'wh.inventory':
            val.update({
                'warehouse_dest_id': move.warehouse_dest_id.id,
                'goods_uos_qty': 1.0 / conversion,
                'goods_qty': 1,
                'price_taxed': price_taxed,
                'tax_rate': tax_rate,
                'cost_unit': cost_unit,
                'move_id': move.id})
        else:
            val.update({
                'inventory_uos_qty': 1.0 / conversion,
                'inventory_qty': 1,
                'real_uos_qty': 0,
                'real_qty': 0,
                'difference_uos_qty': 1.0 / conversion,
                'difference_qty': 1,
                'inventory_id': move.id})
        return val
    @api.model
    def check_barcode(self, model_name, order_id, att, goods):
        pass
    @api.model
    def scan_barcode(self, model_name, barcode, order_id):
        """
        扫描条码
        :param model_name: 模型名
        :param barcode: 条码
        :param order_id: 单据id
        :return:
        """
        att = self.env['attribute'].search([('ean', '=', barcode)])
        goods = self.env['goods'].search([('barcode', '=', barcode)])
        line_model = (model_name == 'wh.inventory' and 'wh.inventory.line'
                      or 'wh.move.line')
        if not att and not goods:
            raise UserError('条码为  %s 的商品不存在' % (barcode))
        else:
            self.check_barcode(model_name, order_id, att, goods)
            conversion = att and att.goods_id.conversion or goods.conversion
            move, create_line, val = self.scan_barcode_each_model_operation(
                model_name, order_id, att, goods, conversion)
            if not create_line:
                self.env[line_model].create(
                    self.prepare_move_line_data(att, val, goods, move))
    def check_qc_result(self):
        """
        检验质检报告是否上传
        :return:
        """
        qc_rule = self.env['qc.rule'].search([
            ('move_type', '=', self.origin),
            ('warehouse_id', '=', self.warehouse_id.id),
            ('warehouse_dest_id', '=', self.warehouse_dest_id.id)])
        if qc_rule and not self.qc_result:
            raise UserError('请先上传质检报告')
    def prev_approve_order(self):
        """
        确认单据之前所做的处理
        :return:
        """
        for order in self:
            if not order.line_out_ids and not order.line_in_ids:
                raise UserError('单据的明细行不可以为空')
            order.check_qc_result()
    
    def approve_order(self):
        """
        确认单据
        :return:
        """
        for order in self:
            order.prev_approve_order()
            order.line_out_ids.action_done()
            order.line_in_ids.action_done()
        # 每次移库完成,清空库位上商品数量为0的商品和属性(不合逻辑的数据)
        for loc in self.env['location'].search([('save_qty', '=', 0),
                                                ('goods_id', '!=', False)
                                                ]):
            if not loc.current_qty:
                continue    # pragma: no cover
        return self.write({
            'approve_uid': self.env.uid,
            'approve_date': fields.Datetime.now(self),
        })
    def prev_cancel_approved_order(self):
        pass
    
    def cancel_approved_order(self):
        """
        撤销确认单据
        :return:
        """
        for order in self:
            order.prev_cancel_approved_order()
            order.line_out_ids.action_draft()
            order.line_in_ids.action_draft()
        return self.write({
            'approve_uid': False,
            'approve_date': False,
        })
    def write(self, vals):
        """
        作废明细行
        """
        if vals.get('state', False) == 'cancel':
            for order in self:
                order.line_out_ids.action_cancel()
                order.line_in_ids.action_cancel()
        return super(WhMove, self).write(vals)
    def create_zero_wh_in(self, wh_in, model_name):
        """
        创建一个缺货向导
        :param wh_in: 单据实例
        :param model_name: 单据模型
        :return:
        """
        all_line_message = ""
        today = fields.Datetime.now()
        line_in_ids = []
        goods_list = []
        for line in wh_in.line_out_ids:
            if line.goods_id.no_stock:
                continue
            result = self.check_goods_qty(
                line.goods_id, line.attribute_id, wh_in.warehouse_id)
            result = result[0] or 0
            if line.goods_qty > result and not line.lot_id and not self.env.context.get('wh_in_line_ids'):
                # 在销售出库时如果临时缺货,自动生成一张盘盈入库单
                if (line.goods_id.id, line.attribute_id.id) in goods_list:
                    continue
                goods_list.append((line.goods_id.id, line.attribute_id.id))
                all_line_message += '商品 %s ' % line.goods_id.name
                if line.attribute_id:
                    all_line_message += ' 型号%s' % line.attribute_id.name
                line_in_ids.append((0, 0, {
                    'goods_id': line.goods_id.id,
                    'attribute_id': line.attribute_id.id,
                    'goods_uos_qty': 0,
                    'uos_id': line.uos_id.id,
                    'goods_qty': 0,
                    'uom_id': line.uom_id.id,
                    'cost_unit': line.goods_id.cost,
                    'state': 'done',
                    'date': today}))
                all_line_message += u" 当前库存量不足,继续出售请点击确定,并及时盘点库存\n"
            if line.goods_qty <= 0 or line.price_taxed < 0:
                raise UserError('商品 %s 的数量和含税单价不能小于0。' % line.goods_id.name)
        if line_in_ids:
            vals = {
                'type': 'inventory',
                'warehouse_id': self.env.ref('warehouse.warehouse_inventory').id,
                'warehouse_dest_id': wh_in.warehouse_id.id,
                'state': 'done',
                'date': today,
                'line_in_ids': line_in_ids}
            return self.env[model_name].with_context(
                {'active_model': model_name}
            ).open_dialog('goods_inventory', {
                'message': all_line_message,
                'args': [vals],
            })
 |