Odoo 10 - 单例错误 - 无法获取订单行中特定字段的总和

Odoo 10 - Singleton Error - Cannot get sum of specific field in order lines

我目前在我的模块中实现一项功能时遇到困难,该功能计算订单行的总税额。我的模块有一个订单行部分(很像销售模块中的那个),它允许添加多个产品并在它们旁边显示它们的价格。看看我是如何添加这个功能的 。在底部,我有一个显示三个字段的小计页脚:amount_untaxedamount_taxamount_total。税收计算工作正常(存储在 amount_tax 中),但我无法在该页脚中显示税收金额。这是相关代码:

models.py

# -*- coding: utf-8 -*-

from odoo import models, fields, api
from odoo.addons import decimal_precision as dp

class mymodule(models.Model):
    _name = 'mymodule.mymodule'
    _description = 'My Module'

    currency_id = fields.Many2one('res.currency', string='Currency')

    operations = fields.One2many('mymodule.line', 'mymodule_id', 'Order Lines')

    amount_untaxed = fields.Monetary(string='Untaxed Amount', 
                                default=0,
                                compute='_compute_untaxed',
                                currency_field='currency_id')

    amount_tax = fields.Monetary(string='Taxes',
                            default=0,
                            compute='_compute_taxes',
                            currency_field='currency_id')

    amount_total = fields.Monetary(string='Total',
                                default=0,
                                compute='_compute_total',
                                currency_field='currency_id')


    # Sum all prices into untaxed total
    @api.depends('operations.price')
    def _compute_untaxed(self):
        for record in self:
            record.amount_untaxed = sum(line.price for line in record.operations)

    # Sum all prices into taxed total
    @api.depends('operations.total')
    def _compute_total(self):
        for record in self:
            record.amount_total = sum(line.total for line in record.operations)

    # Sum all taxes
    @api.depends('operations.taxes')
    def _compute_taxes(self):
        for record in self:
            record.amount_tax = sum(line.taxes for line in record.operations)

class mymodule_line(models.Model):
    _name = 'mymodule.line'
    _description = 'Order Line'

    mymodule_id = fields.Many2one('mymodule.mymodule') 

    product_id = fields.Many2one('product.template',
                                string='Product',
                                required=False) # Product name

    tax_id = fields.Many2many('account.tax', 
                            string='Taxes', 
                            store=False) # Product default taxes

    taxes = fields.Float(string='Taxes', readonly=False)

    price = fields.Float(string='Price')

    total = fields.Float(string='Total', readonly=False)

    # Display default values based on product selected
    @api.onchange('product_id')
    def _onchange_product_id(self):
        self.price = self.product_id.lst_price 
        self.tax_id = self.product_id.taxes_id 

    # Compute total and tax amount for selected product
    @api.onchange('price', 'tax_id')
    @api.depends('total', 'tax_id.amount', 'price', 'taxes')
    def _compute_total(self):
            self.taxes = (0.01 * self.tax_id.amount * self.price)      
            self.total = self.taxes + self.price

views.xml

    <record id="mymodule.form" model="ir.ui.view">
        <field name="name">My Module Form</field>
        <field name="model">mymodule.mymodule</field>
        <field name="arch" type="xml">
            <form>
                <sheet>
                    <notebook>
                        <!-- Order Lines -->
                        <page string="Operations">
                            <group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
                                <field name="amount_untaxed"/>
                                <field name="amount_tax"/>
                                <div class="oe_subtotal_footer_separator oe_inline o_td_label">
                                </div>
                                <field name="amount_total" nolabel="1" class="oe_subtotal_footer_separator"/>
                            </group>
                        </page>
                    </notebook>
                </sheet>
            </form>
        </field>
    </record>

有趣的是,如果我这样更改以下行,添加计算参数,税额将正确显示。

taxes = fields.Float(string='Taxes', readonly=False, compute='_compute_total')

但是,如果我在订单行中添加了多个产品,然后尝试保存记录,则会出现单例错误。

ValueError: Expected singleton: mymodule.line(346, 347)

在我的 _compute_total 函数上方添加 @api.one 装饰器然后允许我保存记录,但是一旦保存,税收计算就会消失。

这是我的小计页脚应该是什么样子的图片以供参考:Subtotal Footer

感谢您的帮助!

让我们来看看这在 Odoo 核心中是如何完成的,例如在 sale module:

_name = "sale.order"

@api.depends('order_line.price_total')
def _amount_all(self):
    """
    Compute the total amounts of the SO.
    """
    for order in self:
        amount_untaxed = amount_tax = 0.0
        for line in order.order_line:
            amount_untaxed += line.price_subtotal
            # FORWARDPORT UP TO 10.0
            if order.company_id.tax_calculation_rounding_method == 'round_globally':
                price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
                taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id)
                amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', []))
            else:
                amount_tax += line.price_tax
        order.update({
            'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
            'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
            'amount_total': amount_untaxed + amount_tax,
        })

amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always')
amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always')

首先,请注意所有字段都可以在一个方法中计算,这比多次循环相同的记录要高效一些。

其次,注意字段定义都是readonly=True。默认情况下,计算字段为 readonly,不应手动创建 readonly=False这可能就是您的字段未按预期显示的原因。

如果您想让计算域可编辑,您需要为该域设置一个 inverse method。您似乎不希望此字段可编辑,因为它应该在您的每个 record.operations.

上进行管理

这只是您设置中可能存在缺陷的一方面。再次查看销售模块,但这次查看 order lines are set up:

_name = 'sale.order.line'

@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
def _compute_amount(self):
    """
    Compute the amounts of the SO line.
    """
    for line in self:
        price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
        taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id)
        line.update({
            'price_tax': taxes['total_included'] - taxes['total_excluded'],
            'price_total': taxes['total_included'],
            'price_subtotal': taxes['total_excluded'],
        })

price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True)
price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True)
price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True)

请注意,这些字段中的每一个都会被计算,而您的字段是由 onchange 方法驱动的。此外,这些字段是 readonly=True(默认情况下所有 computed 字段),这可以防止它们被手动更改。


ValueError: Expected singleton: mymodule.line(346, 347)

Adding the @api.one decorator above my _compute_total function then allows me to save the record, but the tax calculation disappears once it is saved.

@api.depends('operations.total')
def _compute_total(self):
    for record in self:

@api.one 装饰器添加到上述方法是不合逻辑的,因为它用于指定您的方法应该只用于计算单个记录(而不是记录集)。您的方法已经设置为处理多条记录(for record in self 意味着可能会有多条记录)因此您应该使用 @api.multi 装饰器(如果有的话)。


综上所述,我强烈建议重新审视您的模型设计以反映 sale 模块中使用的模型,因为它们在设计时考虑了效率和数据安全性。

我想补充一些 travisw 的回答没有描述的内容:您正在组合使用 api.changeapi.depends 装饰器。这可以解释奇怪的行为,比如单例错误,而不是用额外的 api.one.

保存记录

api.onchange 仅适用于单例,我想这就是为什么您使用 self 而不是循环自身来编写方法的原因。 api.depends 也适用于单例,但在许多情况下不限制 Odoo,它将与一组记录一起使用。

我建议你像这样划分你的方法:

@api.onchange('price', 'tax_id')
def onchange_total(self):
    self._compute_total()

@api.depends('total', 'tax_id.amount', 'price', 'taxes')
def compute_total(self):
    self._compute_total()

@api.multi
def _compute_total(self):
    for line in self:
        line.taxes = (0.01 * line.tax_id.amount * line.price)
        line.total = line.taxes + line.price