osbzr/gooderp_addons

View on GitHub
warehouse/models/warehouse_move_line.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
import odoo.addons.decimal_precision as dp
from utils import safe_division
from jinja2 import Environment, PackageLoader
from odoo import models, fields, api
from odoo.exceptions import UserError
from odoo.tools import float_compare

env = Environment(loader=PackageLoader(
    'odoo.addons.warehouse', 'html'), autoescape=True)


class WhMoveLine(models.Model):
    _name = 'wh.move.line'
    _description = u'移库单明细'
    _order = 'lot'

    _rec_name = 'note'

    MOVE_LINE_TYPE = [
        ('out', u'出库'),
        ('in', u'入库'),
        ('internal', u'内部调拨'),
    ]

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

    ORIGIN_EXPLAIN = {
        ('wh.assembly', 'out'): u'组装单子件',
        ('wh.assembly', 'in'): u'组装单组合件',
        ('wh.disassembly', 'out'): u'拆卸单组合件',
        ('wh.disassembly', 'in'): u'拆卸单子件',
        ('wh.internal', True): u'调拨出库',
        ('wh.internal', False): u'调拨入库',
        'wh.out.inventory': u'盘亏',
        'wh.out.others': u'其他出库',
        'wh.in.inventory': u'盘盈',
        'wh.in.others': u'其他入库',
        'buy.receipt.sell': u'采购入库',
        'buy.receipt.return': u'采购退货',
        'sell.delivery.sell': u'销售出库',
        'sell.delivery.return': u'销售退货',
    }

    @api.one
    @api.depends('goods_qty', 'price_taxed', 'discount_amount', 'tax_rate')
    def _compute_all_amount(self):
        '''当订单行的数量、含税单价、折扣额、税率改变时,改变金额、税额、价税合计'''
        if self.tax_rate > 100:
            raise UserError(u'税率不能输入超过100的数')
        if self.tax_rate < 0:
            raise UserError(u'税率不能输入负数')
        self.subtotal = self.price_taxed * self.goods_qty - self.discount_amount  # 价税合计
        self.tax_amount = self.subtotal / \
            (100 + self.tax_rate) * self.tax_rate  # 税额
        self.amount = self.subtotal - self.tax_amount  # 金额

    @api.onchange('price', 'tax_rate')
    def onchange_price(self):
        '''当订单行的不含税单价改变时,改变含税单价'''
        price = self.price_taxed / (1 + self.tax_rate * 0.01)  # 不含税单价
        decimal = self.env.ref('core.decimal_price')
        if float_compare(price, self.price, precision_digits=decimal.digits) != 0:
            self.price_taxed = self.price * (1 + self.tax_rate * 0.01)

    @api.one
    @api.depends('goods_id')
    def _compute_using_attribute(self):
        self.using_attribute = self.goods_id.attribute_ids and True or False

    @api.one
    @api.depends('move_id.warehouse_id')
    def _get_line_warehouse(self):
        self.warehouse_id = self.move_id.warehouse_id.id
        if (self.move_id.origin == 'wh.assembly' or self.move_id.origin == 'wh.disassembly' or self.move_id.origin == 'outsource') and self.type == 'in':
            self.warehouse_id = self.env.ref(
                'warehouse.warehouse_production').id

    @api.one
    @api.depends('move_id.warehouse_dest_id')
    def _get_line_warehouse_dest(self):
        self.warehouse_dest_id = self.move_id.warehouse_dest_id.id
        if (self.move_id.origin == 'wh.assembly' or self.move_id.origin == 'wh.disassembly' or self.move_id.origin == 'outsource') and self.type == 'out':
            self.warehouse_dest_id = self.env.ref(
                'warehouse.warehouse_production').id

    @api.one
    @api.depends('goods_id')
    def _compute_uom_uos(self):
        if self.goods_id:
            self.uom_id = self.goods_id.uom_id
            self.uos_id = self.goods_id.uos_id

    @api.one
    @api.depends('goods_qty', 'goods_id')
    def _get_goods_uos_qty(self):
        if self.goods_id and self.goods_qty:
            self.goods_uos_qty = self.goods_qty / self.goods_id.conversion
        else:
            self.goods_uos_qty = 0

    @api.one
    def _inverse_goods_qty(self):
        self.goods_qty = self.goods_uos_qty * self.goods_id.conversion

    @api.depends('goods_id', 'goods_qty')
    def compute_line_net_weight(self):
        for move_line in self:
            move_line.line_net_weight = move_line.goods_id.net_weight * move_line.goods_qty

    move_id = fields.Many2one('wh.move', string=u'移库单', ondelete='cascade',
                              help=u'出库/入库/移库单行对应的移库单')
    date = fields.Date(u'完成日期', copy=False,
                       help=u'单据完成日期')
    cost_time = fields.Datetime(u'确认时间', copy=False,
                                help=u'单据确认时间')
    type = fields.Selection(MOVE_LINE_TYPE,
                            u'类型',
                            required=True,
                            default=lambda self: self.env.context.get('type'),
                            help=u'类型:出库、入库 或者 内部调拨')
    state = fields.Selection(MOVE_LINE_STATE, u'状态', copy=False, default='draft',
                             index=True,
                             help=u'状态标识,新建时状态为草稿;确认后状态为已完成')
    goods_id = fields.Many2one('goods', string=u'商品', required=True,
                               index=True, ondelete='restrict',
                               help=u'该单据行对应的商品')
    using_attribute = fields.Boolean(compute='_compute_using_attribute', string=u'使用属性',
                                     help=u'该单据行对应的商品是否存在属性,存在True否则False')
    attribute_id = fields.Many2one('attribute', u'属性', ondelete='restrict',
                                   help=u'该单据行对应的商品的属性')
    using_batch = fields.Boolean(related='goods_id.using_batch', string=u'批号管理',
                                 readonly=True,
                                 help=u'该单据行对应的商品是否使用批号管理')
    force_batch_one = fields.Boolean(related='goods_id.force_batch_one', string=u'每批号数量为1',
                                     readonly=True,
                                     help=u'该单据行对应的商品是否每批号数量为1,是True否则False')
    lot = fields.Char(u'批号',
                      help=u'该单据行对应的商品的批号,一般是入库单行')
    lot_id = fields.Many2one('wh.move.line', u'批号',
                             help=u'该单据行对应的商品的批号,一般是出库单行')
    lot_qty = fields.Float(related='lot_id.qty_remaining', string=u'批号数量',
                           digits=dp.get_precision('Quantity'),
                           help=u'该单据行对应的商品批号的商品剩余数量')
    lot_uos_qty = fields.Float(u'批号辅助数量',
                               digits=dp.get_precision('Quantity'),
                               help=u'该单据行对应的商品的批号辅助数量')
    location_id = fields.Many2one('location', ondelete='restrict', string='库位')
    production_date = fields.Date(u'生产日期', default=fields.Date.context_today,
                                  help=u'商品的生产日期')
    shelf_life = fields.Integer(u'保质期(天)',
                                help=u'商品的保质期(天)')
    valid_date = fields.Date(u'有效期至',
                             help=u'商品的有效期')
    uom_id = fields.Many2one('uom', string=u'单位', ondelete='restrict', compute=_compute_uom_uos,
                             help=u'商品的计量单位', store=True)
    uos_id = fields.Many2one('uom', string=u'辅助单位', ondelete='restrict', compute=_compute_uom_uos,
                             readonly=True,  help=u'商品的辅助单位', store=True)
    warehouse_id = fields.Many2one('warehouse', u'调出仓库',
                                   ondelete='restrict',
                                   store=True,
                                   compute=_get_line_warehouse,
                                   help=u'单据的来源仓库')
    warehouse_dest_id = fields.Many2one('warehouse', u'调入仓库',
                                        ondelete='restrict',
                                        store=True,
                                        compute=_get_line_warehouse_dest,
                                        help=u'单据的目的仓库')
    goods_qty = fields.Float(u'数量',
                             digits=dp.get_precision('Quantity'),
                             default=1,
                             required=True,
                             help=u'商品的数量')
    goods_uos_qty = fields.Float(u'辅助数量', digits=dp.get_precision('Quantity'),
                                 compute=_get_goods_uos_qty, inverse=_inverse_goods_qty, store=True,
                                 help=u'商品的辅助数量')

    price = fields.Float(u'单价',
                         store=True,
                         digits=dp.get_precision('Price'),
                         help=u'商品的单价')
    price_taxed = fields.Float(u'含税单价',
                               digits=dp.get_precision('Price'),
                               help=u'商品的含税单价')
    discount_rate = fields.Float(u'折扣率%',
                                 help=u'单据的折扣率%')
    discount_amount = fields.Float(u'折扣额',
                                   digits=dp.get_precision('Amount'),
                                   help=u'单据的折扣额')
    amount = fields.Float(u'金额', compute=_compute_all_amount, store=True,
                          digits=dp.get_precision('Amount'),
                          help=u'单据的金额,计算得来')
    tax_rate = fields.Float(u'税率(%)',
                            help=u'单据的税率(%)')
    tax_amount = fields.Float(u'税额', compute=_compute_all_amount, store=True,
                              digits=dp.get_precision('Amount'),
                              help=u'单据的税额,有单价×数量×税率计算得来')
    subtotal = fields.Float(u'价税合计', compute=_compute_all_amount, store=True,
                            digits=dp.get_precision('Amount'),
                            help=u'价税合计,有不含税金额+税额计算得来')
    note = fields.Text(u'备注',
                       help=u'可以为该单据添加一些需要的标识信息')
    cost_unit = fields.Float(u'单位成本', digits=dp.get_precision('Price'),
                             help=u'入库/出库单位成本')
    cost = fields.Float(u'成本', compute='_compute_cost', inverse='_inverse_cost',
                        digits=dp.get_precision('Amount'), store=True,
                        help=u'入库/出库成本')
    line_net_weight = fields.Float(
        string=u'净重小计', compute=compute_line_net_weight, store=True)
    expiration_date = fields.Date(u'过保日',
                                  help=u'商品保质期截止日期')
    company_id = fields.Many2one(
        'res.company',
        string=u'公司',
        change_default=True,
        default=lambda self: self.env['res.company']._company_default_get())
    scrap = fields.Boolean(u'报废')
    share_cost = fields.Float(u'采购费用',
                              digits=dp.get_precision('Amount'),
                              help=u'点击分摊按钮或确认时将采购费用进行分摊得出的费用')

    @api.model
    def create(self, vals):
        new_id = super(WhMoveLine, self).create(vals)
        # 只针对入库单行
        if new_id.type != 'out' and not new_id.location_id:
            # 有库存的产品
            qty_now = self.move_id.check_goods_qty(
                new_id.goods_id, new_id.attribute_id, new_id.warehouse_dest_id)[0]
            if qty_now:
                # 建议将产品上架到现有库位上
                new_id.location_id = new_id.env['location'].search([('goods_id', '=', new_id.goods_id.id),
                                                                    ('attribute_id', '=',
                                                                     new_id.attribute_id and new_id.attribute_id.id or False),
                                                                    ('warehouse_id', '=', new_id.warehouse_dest_id.id)],
                                                                   limit=1)
        return new_id

    @api.one
    @api.depends('cost_unit', 'price', 'goods_qty', 'discount_amount', 'share_cost')
    def _compute_cost(self):
        if self.env.context.get('type') == 'in' and self.goods_id:
            if self.price:
                self.cost = self.price * self.goods_qty - self.discount_amount + self.share_cost
            elif self.cost_unit:
                self.cost = self.cost_unit * self.goods_qty - self.discount_amount + self.share_cost

    @api.one
    def _inverse_cost(self):
        self.cost_unit = safe_division(self.cost, self.goods_qty)

    def get_origin_explain(self):
        self.ensure_one()
        if self.move_id.origin in ('wh.assembly', 'wh.disassembly'):
            return self.ORIGIN_EXPLAIN.get((self.move_id.origin, self.type))
        elif self.move_id.origin == 'wh.internal':
            return self.ORIGIN_EXPLAIN.get((self.move_id.origin, self.env.context.get('internal_out', False)))
        elif self.move_id.origin in self.ORIGIN_EXPLAIN.keys():
            return self.ORIGIN_EXPLAIN.get(self.move_id.origin)

        return ''

    @api.model
    def default_get(self, fields):
        res = super(WhMoveLine, self).default_get(fields)
        if self.env.context.get('goods_id') and self.env.context.get('warehouse_id'):
            res.update({
                'goods_id': self.env.context.get('goods_id'),
                'warehouse_id': self.env.context.get('warehouse_id')
            })

        return res

    def get_real_cost_unit(self):
        self.ensure_one()
        return safe_division(self.cost, self.goods_qty)

    @api.multi
    def name_get(self):
        res = []
        for line in self:
            if self.env.context.get('match'):
                res.append((line.id, '%s-%s->%s(%s, %s%s)' %
                            (line.move_id.name, line.warehouse_id.name, line.warehouse_dest_id.name,
                             line.goods_id.name, str(line.goods_qty), line.uom_id.name)))
            else:
                res.append((line.id, line.lot))
        return res

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        ''' 批号下拉的时候显示批次和剩余数量 '''
        result = []
        domain = []
        if args:
            domain = args
        if name:
            domain.append(('lot', operator, name))
        records = self.search(domain, limit=limit)
        for line in records:
            result.append((line.id, u'%s %s 余 %s' % (
                line.lot, line.warehouse_dest_id.name, line.qty_remaining)))
        return result

    def check_availability(self):
        if self.warehouse_dest_id == self.warehouse_id:
            # 如果是 商品库位转移生成的内部移库,则不用约束调入仓和调出仓是否相同;否则需要约束
            if not (self.move_id.origin == 'wh.internal' and not self.location_id == False):
                raise UserError(u'调出仓库不可以和调入仓库一样')
        # 检查属性或批号是否填充,防止无权限人员不填就可以保存
        if self.using_attribute and not self.attribute_id:
            raise UserError(u'请输入商品:%s 的属性' % self.goods_id.name)
        if self.using_batch:
            if self.type == 'in' and not self.lot:
                raise UserError(u'请输入商品:%s 的批号' % self.goods_id.name)
            if self.type in ['out', 'internal'] and not self.lot_id:
                raise UserError(u'请选择商品:%s 的批号' % self.goods_id.name)

    def prev_action_done(self):
        pass

    @api.multi
    def action_done(self):
        for line in self:
            line.check_availability()
            line.prev_action_done()
            line.write({
                'state': 'done',
                'date': line.move_id.date,
                'cost_time': fields.Datetime.now(self),
            })
            if line.type in ('in', 'internal'):
                locations = self.env['location'].search([('warehouse_id', '=', line.warehouse_dest_id.id)])
                if locations and not line.location_id:
                    raise UserError(u'调入仓库 %s 进行了库位管理,请在明细行输入库位' % line.warehouse_dest_id.name)
                if line.location_id:
                    line.location_id.write(
                        {'attribute_id': line.attribute_id.id, 'goods_id': line.goods_id.id})

            if line.type == 'in' and line.scrap:
                if not self.env.user.company_id.wh_scrap_id:
                    raise UserError(u'请在公司上输入废品库')
                dic = {
                    'type': 'internal',
                    'goods_id': line.goods_id.id,
                    'uom_id': line.uom_id.id,
                    'attribute_id': line.attribute_id.id,
                    'goods_qty': line.goods_qty,
                    'warehouse_id': line.warehouse_dest_id.id,
                    'warehouse_dest_id': self.env.user.company_id.wh_scrap_id.id
                }
                if line.lot:
                    dic.update({'lot_id': line.id})
                wh_internal = self.env['wh.internal'].search([('ref', '=', line.move_id.name)])
                if not wh_internal:
                    value = {
                        'ref': line.move_id.name,
                        'date': fields.Datetime.now(self),
                        'warehouse_id': line.warehouse_dest_id.id,
                        'warehouse_dest_id': self.env.user.company_id.wh_scrap_id.id,
                        'line_out_ids': [(0, 0, dic)],
                    }
                    self.env['wh.internal'].create(value)
                else:
                    dic['move_id'] = wh_internal.move_id.id
                    self.env['wh.move.line'].create(dic)

    def check_cancel(self):
        pass

    def prev_action_draft(self):
        pass

    @api.multi
    def action_draft(self):
        for line in self:
            line.check_cancel()
            line.prev_action_draft()
            line.write({
                'state': 'draft',
                'date': False,
            })

    @api.one
    def compute_lot_compatible(self):
        if self.warehouse_id and self.lot_id and self.lot_id.warehouse_dest_id != self.warehouse_id:
            self.lot_id = False

        if self.goods_id and self.lot_id and self.lot_id.goods_id != self.goods_id:
            self.lot_id = False

    def compute_lot_domain(self):
        warehouse_id = self.env.context.get('default_warehouse_id')
        lot_domain = [('goods_id', '=', self.goods_id.id), ('state', '=', 'done'),
                      ('lot', '!=', False), ('qty_remaining', '>', 0),
                      ('warehouse_dest_id.type', '=', 'stock')]

        if warehouse_id:
            lot_domain.append(('warehouse_dest_id', '=', warehouse_id))

        if self.attribute_id:
            lot_domain.append(('attribute_id', '=', self.attribute_id.id))

        return lot_domain

    @api.one
    def compute_suggested_cost(self):
        if self.env.context.get('type') == 'out' and self.goods_id and self.warehouse_id and self.goods_qty:
            cost, cost_unit = self.goods_id.get_suggested_cost_by_warehouse(
                self.warehouse_id, self.goods_qty, self.lot_id, self.attribute_id)

            self.cost_unit = cost_unit

        if self.env.context.get('type') == 'in' and self.goods_id:
            # 如果商品上设置了税率,则按商品上的计算,否则按公司上的进项税率计算
            tax_rate = self.goods_id.tax_rate or self.env.user.company_id.import_tax_rate
            self.cost_unit = self.goods_id.cost / (1 + tax_rate * 0.01)

    @api.multi
    @api.onchange('goods_id')
    def onchange_goods_id(self):
        if self.goods_id:
            self.uom_id = self.goods_id.uom_id
            self.uos_id = self.goods_id.uos_id
            self.attribute_id = False

            partner_id = self.env.context.get('default_partner')
            partner = self.env['partner'].browse(partner_id)
            if self.type == 'in':
                self.tax_rate = self.goods_id.get_tax_rate(self.goods_id, partner, 'buy')
            if self.type == 'out':
                self.tax_rate = self.goods_id.get_tax_rate(self.goods_id, partner, 'sell')

            if self.goods_id.using_batch and self.goods_id.force_batch_one:
                self.goods_qty = 1
                self.goods_uos_qty = self.goods_id.anti_conversion_unit(
                    self.goods_qty)
            else:
                self.goods_qty = self.goods_id.conversion_unit(
                    self.goods_uos_qty or 1)
        else:
            return

        self.compute_suggested_cost()
        self.compute_lot_compatible()

        return {'domain': {'lot_id': self.compute_lot_domain()}}

    @api.multi
    @api.onchange('warehouse_id')
    def onchange_warehouse_id(self):
        self.compute_suggested_cost()
        self.compute_lot_domain()
        self.compute_lot_compatible()

        return {'domain': {'lot_id': self.compute_lot_domain()}}

    @api.multi
    @api.onchange('attribute_id')
    def onchange_attribute_id(self):
        self.compute_suggested_cost()
        return {'domain': {'lot_id': self.compute_lot_domain()}}

    @api.onchange('goods_qty')
    def onchange_goods_qty(self):
        self.compute_suggested_cost()

    @api.onchange('goods_uos_qty')
    def onchange_goods_uos_qty(self):
        if self.goods_id:
            self.goods_qty = self.goods_id.conversion_unit(self.goods_uos_qty)
        self.compute_suggested_cost()

    @api.onchange('lot_id')
    def onchange_lot_id(self):
        if self.lot_id:
            self.lot_qty = self.lot_id.qty_remaining
            self.lot_uos_qty = self.goods_id.anti_conversion_unit(self.lot_qty)

            if self.env.context.get('type') in ['internal', 'out']:
                self.lot = self.lot_id.lot

    @api.onchange('goods_qty', 'price_taxed', 'discount_rate')
    def onchange_discount_rate(self):
        """当数量、单价或优惠率发生变化时,优惠金额发生变化"""
        self.price = self.price_taxed / (1 + self.tax_rate * 0.01)
        self.discount_amount = self.goods_qty * self.price * self.discount_rate * 0.01

    @api.multi
    @api.onchange('discount_amount')
    def onchange_discount_amount(self):
        """当优惠金额发生变化时,重新取默认的单位成本,以便计算实际的单位成本"""
        self.compute_suggested_cost()

    @api.one
    @api.constrains('goods_qty')
    def check_goods_qty(self):
        """序列号管理的商品数量必须为1"""
        if self.force_batch_one and self.goods_qty > 1:
            raise UserError(u'商品 %s 进行了序列号管理,数量必须为1' % self.goods_id.name)