You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Odoo 15中添加margin字段计算subtotal时出现move_type错误

Odoo 15 客户发票行新增Margin字段时move_type错误的解决方案

问题背景

在Odoo 15中扩展account.move.line模型,新增margin字段用于计算发票行的price_subtotal,重写create()和_get_price_total_and_subtotal_model()方法后触发move_type相关错误,原代码如下:

class AccountMoveLine(models.Model):
    _inherit = 'account.move.line'
    margin = fields.Float(string='Margin (%)', digits='Margin',default=0.0)

    @api.model
    def _get_price_total_and_subtotal_model(self, price_unit, quantity, discount, margin,          
    currency, product, partner, taxes, move_type):
    ''' This method is used to compute 'price_total' & 'price_subtotal'.

        :param price_unit:  The current price unit.
        :param quantity:    The current quantity.
        :param discount:    The current discount.
        :param currency:    The line's currency.
        :param product:     The line's product.
        :param partner:     The line's partner.
        :param taxes:       The applied taxes.
        :param move_type:   The type of the move.
        :return:            A dictionary containing 'price_subtotal' & 'price_total'.
        '''
        res = {}
        # Compute 'price_subtotal'.
        line_discount_price_unit = price_unit * (1 + (margin / 100.0) - (discount / 100.0))
        subtotal = quantity * line_discount_price_unit
        # Compute 'price_total'.
        if taxes:
            taxes_res = taxes._origin.with_context(force_sign=1).compute_all(line_discount_price_unit, quantity=quantity,currency=currency, product=product, partner=partner, is_refund=move_type in ('out_refund','in_refund'))
            res['price_subtotal'] = taxes_res['total_excluded']
            res['price_total'] = taxes_res['total_included']
        else:
            res['price_total'] = res['price_subtotal'] = subtotal
        # In case of multi currency, round before it's use for computing debit credit
        if currency:
            res = {k: currency.round(v) for k, v in res.items()}
        return res

    @api.model_create_multi
    def create(self, vals_list):
        # OVERRIDE
        lines = super(AccountMoveLine, self).create(vals_list)
        ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
        BUSINESS_FIELDS = ('price_unit', 'quantity', 'margin', 'discount', 'tax_ids')
        for vals in vals_list:
            move = self.env['account.move'].browse(vals['move_id'])
            vals.setdefault('company_currency_id', move.company_id.currency_id.id)

            currency_id = vals.get('currency_id') or move.company_id.currency_id.id
            if currency_id == move.company_id.currency_id.id:
                balance = vals.get('debit', 0.0) - vals.get('credit', 0.0)
                vals.update({
                    'currency_id': currency_id,
                    'amount_currency': balance,
                })
            else:
                vals['amount_currency'] = vals.get('amount_currency', 0.0)

            if move.is_invoice(include_receipts=True):
                currency = move.currency_id
                partner = self.env['res.partner'].browse(vals.get('partner_id'))
                taxes = self.new({'tax_ids': vals.get('tax_ids', [])}).tax_ids
                tax_ids = set(taxes.ids)
                taxes = self.env['account.tax'].browse(tax_ids)

                if any(vals.get(field) for field in ACCOUNTING_FIELDS):
                    price_subtotal = self._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')),
                        partner,
                        taxes,
                        move.move_type,
                    ).get('price_subtotal', 0.0)
                    vals.update(self._get_fields_onchange_balance_model(
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals['amount_currency'],
                        move.move_type,
                        currency,
                        taxes,
                        price_subtotal
                    ))
                    vals.update(sel`your text`f._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')),
                        partner,
                        taxes,
                        move.move_type,
                    ))
                elif any(vals.get(field) for field in BUSINESS_FIELDS):
                    vals.update(self._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')),
                        partner,
                        taxes,
                        move.move_type,
                    ))
                    vals.update(self._get_fields_onchange_subtotal_model(
                        vals['price_subtotal'],
                        move.move_type,
                        currency,
                        move.company_id,
                        move.date,
                    ))

        moves = lines.mapped('move_id')
        if self._context.get('check_move_validity', True):
            moves._check_balanced()
        moves.filtered(lambda m: m.state == 'posted')._check_fiscalyear_lock_date()
        lines.filtered(lambda l: l.parent_state == 'posted')._check_tax_lock_date()
        moves._synchronize_business_models({'line_ids'})

        return lines

错误原因分析

  1. 代码笔误:create()方法中存在语法错误selyour textf,导致方法调用失败,引发move_type传递错误
  2. 逻辑顺序错误:先调用super().create()创建记录,再修改vals_list,此时修改不会对已创建的发票行生效,计算逻辑未正确应用
  3. Taxes获取冗余:通过set(taxes.ids)转换再重新浏览的方式获取taxes,可能导致上下文丢失或无效的tax记录
  4. 空值未处理:当partner_id为空时,self.env['res.partner'].browse(vals.get('partner_id'))会创建空记录,干扰后续计算

修复方案

1. 修正语法笔误

将selyour textf替换为self,恢复正常方法调用。

2. 调整create方法逻辑顺序

在调用super().create()之前处理vals_list,确保计算后的字段值被正确传入创建逻辑。

3. 优化Taxes获取方式

直接使用self.env['account.tax'].browse(vals.get('tax_ids', []))获取tax记录,避免不必要的转换。

4. 处理空值情况

当partner_id或product_id为空时,传入空的对应模型记录而非无效浏览记录。

5. 确保move_type可靠传递

直接从move对象获取move_type,避免中间环节参数丢失。

修复后的完整代码

class AccountMoveLine(models.Model):
    _inherit = 'account.move.line'
    margin = fields.Float(string='Margin (%)', digits='Margin', default=0.0)

    @api.model
    def _get_price_total_and_subtotal_model(self, price_unit, quantity, discount, margin,          
                                            currency, product, partner, taxes, move_type):
        ''' This method is used to compute 'price_total' & 'price_subtotal'.

            :param price_unit:  The current price unit.
            :param quantity:    The current quantity.
            :param discount:    The current discount.
            :param margin:      The current margin percentage.
            :param currency:    The line's currency.
            :param product:     The line's product.
            :param partner:     The line's partner.
            :param taxes:       The applied taxes.
            :param move_type:   The type of the move.
            :return:            A dictionary containing 'price_subtotal' & 'price_total'.
            '''
        res = {}
        # Compute 'price_subtotal' with margin calculation
        line_discount_price_unit = price_unit * (1 + (margin / 100.0) - (discount / 100.0))
        subtotal = quantity * line_discount_price_unit

        # Compute 'price_total' with taxes
        if taxes:
            taxes_res = taxes._origin.with_context(force_sign=1).compute_all(
                line_discount_price_unit, 
                quantity=quantity,
                currency=currency, 
                product=product, 
                partner=partner, 
                is_refund=move_type in ('out_refund', 'in_refund')
            )
            res['price_subtotal'] = taxes_res['total_excluded']
            res['price_total'] = taxes_res['total_included']
        else:
            res['price_total'] = res['price_subtotal'] = subtotal

        # Round values for multi-currency scenarios
        if currency:
            res = {k: currency.round(v) for k, v in res.items()}
        return res

    @api.model_create_multi
    def create(self, vals_list):
        ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
        BUSINESS_FIELDS = ('price_unit', 'quantity', 'margin', 'discount', 'tax_ids')

        # Process vals_list BEFORE creating lines
        for vals in vals_list:
            move = self.env['account.move'].browse(vals['move_id'])
            vals.setdefault('company_currency_id', move.company_id.currency_id.id)

            currency_id = vals.get('currency_id') or move.company_id.currency_id.id
            if currency_id == move.company_id.currency_id.id:
                balance = vals.get('debit', 0.0) - vals.get('credit', 0.0)
                vals.update({
                    'currency_id': currency_id,
                    'amount_currency': balance,
                })
            else:
                vals['amount_currency'] = vals.get('amount_currency', 0.0)

            if move.is_invoice(include_receipts=True):
                currency = move.currency_id
                partner = self.env['res.partner'].browse(vals.get('partner_id')) if vals.get('partner_id') else self.env['res.partner']
                taxes = self.env['account.tax'].browse(vals.get('tax_ids', []))

                if any(vals.get(field) for field in ACCOUNTING_FIELDS):
                    price_subtotal = self._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')) if vals.get('product_id') else self.env['product.product'],
                        partner,
                        taxes,
                        move.move_type,
                    ).get('price_subtotal', 0.0)
                    vals.update(self._get_fields_onchange_balance_model(
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals['amount_currency'],
                        move.move_type,
                        currency,
                        taxes,
                        price_subtotal
                    ))
                    vals.update(self._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')) if vals.get('product_id') else self.env['product.product'],
                        partner,
                        taxes,
                        move.move_type,
                    ))
                elif any(vals.get(field) for field in BUSINESS_FIELDS):
                    subtotal_vals = self._get_price_total_and_subtotal_model(
                        vals.get('price_unit', 0.0),
                        vals.get('quantity', 0.0),
                        vals.get('discount', 0.0),
                        vals.get('margin', 0.0),
                        currency,
                        self.env['product.product'].browse(vals.get('product_id')) if vals.get('product_id') else self.env['product.product'],
                        partner,
                        taxes,
                        move.move_type,
                    )
                    vals.update(subtotal_vals)
                    vals.update(self._get_fields_onchange_subtotal_model(
                        vals['price_subtotal'],
                        move.move_type,
                        currency,
                        move.company_id,
                        move.date,
                    ))

        # Create lines with processed vals
        lines = super(AccountMoveLine, self).create(vals_list)

        moves = lines.mapped('move_id')
        if self._context.get('check_move_validity', True):
            moves._check_balanced()
        moves.filtered(lambda m: m.state == 'posted')._check_fiscalyear_lock_date()
        lines.filtered(lambda l: l.parent_state == 'posted')._check_tax_lock_date()
        moves._synchronize_business_models({'line_ids'})

        return lines

额外注意事项

  • 确保Margin小数精度在Odoo的小数精度配置中已正确设置
  • 测试不同发票类型(普通发票、退款发票)下的margin计算是否正常
  • 验证多币种场景下的金额四舍五入逻辑是否符合业务需求

内容的提问来源于stack exchange,提问作者Abdullah Elshoura

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 04:20:31