osbzr/gooderp_addons

View on GitHub
warehouse/models/warehouse_move.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-

from odoo import models, fields, api
from odoo.exceptions import UserError


class WhMove(models.Model):
    _name = 'wh.move'
    _description = u'移库单'

    MOVE_STATE = [
        ('draft', u'草稿'),
        ('done', u'已完成'),
        ('cancel', u'已作废'),]

    @api.one
    @api.depends('line_out_ids', 'line_in_ids')
    def _compute_total_qty(self):
        goods_total = 0
        if self.line_in_ids:
            # 入库商品总数
            goods_total = sum(line.goods_qty for line in self.line_in_ids)
        elif self.line_out_ids:
            # 出库商品总数
            goods_total = sum(line.goods_qty for line in self.line_out_ids)
        self.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(u'移库类型', required=True,
                         help=u'移库类型')
    name = fields.Char(u'单据编号', copy=False, default='/',
                       help=u'单据编号,创建时会自动生成')
    ref = fields.Char(u'外部单号')
    state = fields.Selection(MOVE_STATE, u'状态', copy=False, default='draft',
                             index=True,
                             help=u'移库单状态标识,新建时状态为草稿;确认后状态为已确认',
                             track_visibility='onchange')
    partner_id = fields.Many2one('partner', u'业务伙伴', ondelete='restrict',
                                 help=u'该单据对应的业务伙伴')
    date = fields.Date(u'单据日期', required=True, copy=False, default=fields.Date.context_today,
                       help=u'单据创建日期,默认为当前天')
    warehouse_id = fields.Many2one('warehouse', u'调出仓库',
                                   ondelete='restrict',
                                   required=True,
                                   readonly=True,
                                   domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
                                   states={'draft': [('readonly', False)]},
                                   default=_get_default_warehouse,
                                   help=u'移库单的来源仓库')
    warehouse_dest_id = fields.Many2one('warehouse', u'调入仓库',
                                        ondelete='restrict',
                                        required=True,
                                        readonly=False,
                                        domain="['|',('user_ids','=',False),('user_ids','in',uid)]",
                                        states={'done': [('readonly', True)]},
                                        default=_get_default_warehouse_dest,
                                        help=u'移库单的目的仓库')
    approve_uid = fields.Many2one('res.users', u'确认人',
                                  copy=False, ondelete='restrict',
                                  help=u'移库单的确认人')
    approve_date = fields.Datetime(u'确认日期', copy=False)
    line_out_ids = fields.One2many('wh.move.line', 'move_id', u'出库明细',
                                   domain=[
                                       ('type', 'in', ['out', 'internal'])],
                                   copy=True,
                                   help=u'出库类型的移库单对应的出库明细')
    line_in_ids = fields.One2many('wh.move.line', 'move_id', u'入库明细',
                                  domain=[('type', '=', 'in')],
                                  context={'type': 'in'}, copy=True,
                                  help=u'入库类型的移库单对应的入库明细')
    note = fields.Text(u'备注',
                       copy=False,
                       help=u'可以为该单据添加一些需要的标识信息')
    total_qty = fields.Integer(u'商品总数', compute=_compute_total_qty, store=True,
                               help=u'该移库单的入/出库明细行包含的商品总数')
    user_id = fields.Many2one(
        'res.users',
        u'经办人',
        ondelete='restrict',
        states={'done': [('readonly', True)]},
        default=lambda self: self.env.user,
        help=u'单据经办人',
        track_visibility='onchange'
    )
    express_type = fields.Char(string='承运商')
    express_code = fields.Char(u'快递单号', copy=False)
    company_id = fields.Many2one(
        'res.company',
        string=u'公司',
        change_default=True,
        default=lambda self: self.env['res.company']._company_default_get())
    qc_result = fields.Binary(u'质检报告',
                              help=u'点击上传质检报告')
    finance_category_id = fields.Many2one(
        'core.category',
        string=u'收发类别',
        ondelete='restrict',
        states={'done': [('readonly', True)]},
        domain=[('type', '=', 'finance')],
        context={'type': 'finance'},
        help=u'生成凭证时从此字段上取商品科目的对方科目',
    )
    all_line_done = fields.Boolean(u'出库行都完成', compute='compute_all_line_done', store=True)

    @api.one
    @api.depends('line_out_ids.state')
    def compute_all_line_done(self):
        """如果所有出库行的状态都为done,则置为True,以便控制发料和出库按钮显隐"""
        if all(line.state == 'done' for line in self.line_out_ids):
            self.all_line_done = True

    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
            val['type'] = 'out'
            create_line = self.scan_barcode_inventory_operation(
                move, att, conversion, goods, val)

        return move, create_line, val

    @api.multi
    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['type'] == 'out' and 0 or goods.cost / \
            (1 + tax_rate * 0.01)

        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(u'条码为  %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(u'请先上传质检报告')

    def prev_approve_order(self):
        """
        确认单据之前所做的处理
        :return:
        """
        for order in self:
            if not order.line_out_ids and not order.line_in_ids:
                raise UserError(u'单据的明细行不可以为空')
            order.check_qc_result()

    @api.multi
    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

    @api.multi
    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,
        })

    @api.multi
    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)

    @api.multi
    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 += u'商品 %s ' % line.goods_id.name
                if line.attribute_id:
                    all_line_message += u' 型号%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 / (1 + line.goods_id.tax_rate * 0.01),
                    'state': 'done',
                    'date': today}))
                all_line_message += u" 当前库存量不足,继续出售请点击确定,并及时盘点库存\n"

            if line.goods_qty <= 0 or line.price_taxed < 0:
                raise UserError(u'商品 %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].open_dialog('goods_inventory', {
                'message': all_line_message,
                'args': [vals],
            })