0
votes

I am currently having difficulty implementing a feature in my module that totals the amount of taxes from order lines. My module has an order line section (much like that in the sales module), which allows several products to be added and their prices to be displayed next to them. See how I added this feature here. At the bottom, I have a subtotal footer which displays three fields: amount_untaxed, amount_tax, and amount_total. The tax calculation works correctly (stored in amount_tax), but I cannot display the amount of taxes in that footer. Here is the relevant code:

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>

What is interesting is if I change the following line as such, adding the compute argument, the amount of taxes will display properly.

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

However, if I add more than one product in the order lines and then try to save the record, I get a singleton error.

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.

Here is an image of what my subtotal footer should look like for reference: Subtotal Footer

I appreciate your help!

2
Following link will be helpful to you in future. odedrabhavesh.blogspot.in/2017/02/…Bhavesh Odedra

2 Answers

2
votes

Let's take a look at how this is done in Odoo core, for example in the 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')

Firstly, note that all of the fields can be computed in a single method, which can be a little more efficient than looping through the same records multiple times.

Second, notice that the field definitios are all readonly=True. A computed field is readonly by default and should not be manually made readonly=False. This may be why your field isn't displaying as you expect.

If you want to have a computed field be editable, you need to set an inverse method for the field(s). It seems you would not want to have this field be editable because it should be managed on each of your record.operations.


This is just one aspect of your set up that may have flaws. Again, take a look at the sale module, but this time look at how the 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)

Notice that each of these fields are also computed, whereas yours are driven by onchange methods. Additionally, these fields are readonly=True (as all computed fields are by default) which prevents them from being manually changed.


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:

Adding the @api.one decorator to your above method is illogical because it's used to specify that your method should only be used to compute a single record (rather than a recordset). Your method is already set up to handle multiple records (for record in self implies there will possibly be multiple records) so you should be using the @api.multi decorator if anything.


All that being said, I would highly suggest revisiting your model designs to mirror those used in the sale module because they are designed with efficiency and data security in mind.

0
votes

I would like to add something which isn't depicted by travisw's answer: You're using api.change and api.depends decorators in combination. That would explain the weird behaviour like the singleton error and not saving the records with additional api.one.

api.onchange will only work with singletons, i guess that's why you wrote your method using self instead of looping on self. api.depends will work with singletons, too, but without limiting Odoo in many situations, it will be used with a set of records.

I advise you to divide your method like:

@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