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
错误原因分析
- 代码笔误:
create()方法中存在语法错误selyour textf,导致方法调用失败,引发move_type传递错误 - 逻辑顺序错误:先调用
super().create()创建记录,再修改vals_list,此时修改不会对已创建的发票行生效,计算逻辑未正确应用 - Taxes获取冗余:通过
set(taxes.ids)转换再重新浏览的方式获取taxes,可能导致上下文丢失或无效的tax记录 - 空值未处理:当
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
相关产品推荐
相关产品推荐

