[MIG] sale_order_invoice_untaxed_amount: migrate to 18.0

This commit is contained in:
Stéphan Sainléger
2026-07-23 12:18:37 +02:00
parent 4c51357735
commit c06b8437ac
12 changed files with 430 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
*.*~
*pyc

View File

@@ -0,0 +1,65 @@
# sale_order_invoice_untaxed_amount
Display the invoiced and uninvoiced untaxed total in the sale order.
This module adds two stored computed fields on the sale order:
- **Invoiced Untaxed Amount**: the sum of the untaxed amounts of all
non-cancelled invoices linked to the sale order.
- **Uninvoiced Untaxed Amount**: the remaining untaxed amount still to be
invoiced (`amount_untaxed - invoiced_untaxed_amount`, never negative).
Both fields are displayed:
- In the **sale order form**, below the tax totals block.
- In the **sale order list view**, as optional columns (hidden by default)
with column totals.
## Installation
Use Odoo normal module installation procedure to install
`sale_order_invoice_untaxed_amount`.
## Configuration
This module does not require any configuration.
## Usage
1. Open or create a sale order.
2. Confirm the order and create one or more invoices from it.
3. The **Invoiced Untaxed Amount** and **Uninvoiced Untaxed Amount** are
displayed below the totals block of the sale order form.
4. In the sale orders list view, enable the optional columns **Invoiced
Untaxed Amount** and **Uninvoiced Untaxed Amount** to compare invoiced and
remaining untaxed amounts across orders.
The amounts are recomputed automatically whenever the order state, its
invoices, or the invoices state change.
## Known issues / Roadmap
None yet.
## Bug Tracker
Bugs are tracked on [our issues website](https://github.com/elabore-coop/sale-tools/issues).
In case of trouble, please check there if your issue has already been
reported. If you spotted it first, help us smashing it by providing a
detailed and welcomed feedback.
## Credits
### Contributors
- Stéphan Sainléger
### Funders
The development of this module has been financially supported by:
- Elabore (https://elabore.coop)
### Maintainer
This module is maintained by Elabore.

View File

@@ -0,0 +1,3 @@
from . import models

View File

@@ -0,0 +1,37 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "sale_order_invoice_untaxed_amount",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger",
"license": "AGPL-3",
"category": "Tools",
"summary": "Display the invoiced and uninvoiced untaxed total in the sale order",
# any module necessary for this one to work correctly
"depends": [
"account",
"sale",
],
"qweb": [
# "static/src/xml/*.xml",
],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/sale_order_view.xml",
],
# only loaded in demonstration mode
"demo": [],
"js": [],
"css": [],
"installable": True,
# Install this module automatically if all dependency have been previously
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}

View File

@@ -0,0 +1 @@
This directory should contain the *.po for Odoo translation.

View File

@@ -0,0 +1,56 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_order_invoice_untaxed_amount
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-02 08:32+0000\n"
"PO-Revision-Date: 2022-12-02 08:32+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__id
msgid "ID"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__invoiced_untaxed_amount
msgid "Invoiced Untaxed Amount"
msgstr "Facturé HT"
#. module: sale_order_invoice_untaxed_amount
#: model_terms:ir.ui.view,arch_db:sale_order_invoice_untaxed_amount.view_order_tree_invoiced_untaxed_amount
msgid "Invoiced Untaxed Total"
msgstr "Facturé HT Total"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model,name:sale_order_invoice_untaxed_amount.model_sale_order
msgid "Sales Order"
msgstr "Bon de commande"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__uninvoiced_untaxed_amount
msgid "Uninvoiced Untaxed Amount"
msgstr "Non-facturé HT"
#. module: sale_order_invoice_untaxed_amount
#: model_terms:ir.ui.view,arch_db:sale_order_invoice_untaxed_amount.view_order_tree_invoiced_untaxed_amount
msgid "Uninvoiced Untaxed Total"
msgstr "Non-facturé HT Total"

View File

@@ -0,0 +1,56 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_order_invoice_untaxed_amount
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-02 08:33+0000\n"
"PO-Revision-Date: 2022-12-02 08:33+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__display_name
msgid "Display Name"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__id
msgid "ID"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__invoiced_untaxed_amount
msgid "Invoiced Untaxed Amount"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model_terms:ir.ui.view,arch_db:sale_order_invoice_untaxed_amount.view_order_tree_invoiced_untaxed_amount
msgid "Invoiced Untaxed Total"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order____last_update
msgid "Last Modified on"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model,name:sale_order_invoice_untaxed_amount.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model:ir.model.fields,field_description:sale_order_invoice_untaxed_amount.field_sale_order__uninvoiced_untaxed_amount
msgid "Uninvoiced Untaxed Amount"
msgstr ""
#. module: sale_order_invoice_untaxed_amount
#: model_terms:ir.ui.view,arch_db:sale_order_invoice_untaxed_amount.view_order_tree_invoiced_untaxed_amount
msgid "Uninvoiced Untaxed Total"
msgstr ""

View File

@@ -0,0 +1 @@
from . import sale_order

View File

@@ -0,0 +1,43 @@
# Copyright (C) 2021 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = "sale.order"
invoiced_untaxed_amount = fields.Monetary(
string="Invoiced Untaxed Amount",
compute="_compute_invoice_untaxed_amount",
store=True,
)
uninvoiced_untaxed_amount = fields.Monetary(
string="Uninvoiced Untaxed Amount",
compute="_compute_invoice_untaxed_amount",
store=True,
)
@api.depends(
"state",
"invoice_ids",
"invoice_ids.amount_untaxed_signed",
"amount_total",
"invoice_ids.state",
)
def _compute_invoice_untaxed_amount(self):
for rec in self:
if rec.state != "cancel" and rec.invoice_ids:
rec.invoiced_untaxed_amount = 0.0
for invoice in rec.invoice_ids:
if invoice.state != "cancel":
rec.invoiced_untaxed_amount += invoice.amount_untaxed_signed
rec.uninvoiced_untaxed_amount = max(0, rec.amount_untaxed - rec.invoiced_untaxed_amount)
else:
rec.invoiced_untaxed_amount = 0.0
if rec.state in ["draft", "sent", "cancel"]:
rec.uninvoiced_untaxed_amount = 0.0
else:
rec.uninvoiced_untaxed_amount = rec.amount_untaxed

View File

@@ -0,0 +1 @@
from . import test_sale_order_invoice_untaxed_amount

View File

@@ -0,0 +1,134 @@
# Copyright (C) 2021 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
from odoo.tests import common
class TestSaleOrderInvoiceUntaxedAmount(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Partners
cls.res_partner_1 = cls.env["res.partner"].create({"name": "Wood Corner"})
# Products
cls.product_1 = cls.env["product.product"].create(
{
"name": "Desk Combination",
"type": "consu",
"invoice_policy": "order",
}
)
cls.product_2 = cls.env["product.product"].create(
{
"name": "Conference Chair",
"type": "consu",
"invoice_policy": "order",
}
)
cls.product_3 = cls.env["product.product"].create(
{
"name": "Repair Services",
"type": "service",
"invoice_policy": "order",
}
)
# Sale Order
cls.tax = cls.env["account.tax"].create(
{"name": "Tax 21", "type_tax_use": "sale", "amount": 21}
)
cls.sale_order_1 = cls.env["sale.order"].create(
{"partner_id": cls.res_partner_1.id}
)
cls.order_line_1 = cls.env["sale.order.line"].create(
{
"order_id": cls.sale_order_1.id,
"product_id": cls.product_1.id,
"product_uom": cls.product_1.uom_id.id,
"product_uom_qty": 10.0,
"price_unit": 10.0,
"tax_id": [(6, 0, cls.tax.ids)],
}
)
cls.order_line_2 = cls.env["sale.order.line"].create(
{
"order_id": cls.sale_order_1.id,
"product_id": cls.product_2.id,
"product_uom": cls.product_2.uom_id.id,
"product_uom_qty": 25.0,
"price_unit": 4.0,
"tax_id": [(6, 0, cls.tax.ids)],
}
)
cls.order_line_3 = cls.env["sale.order.line"].create(
{
"order_id": cls.sale_order_1.id,
"product_id": cls.product_3.id,
"product_uom": cls.product_3.uom_id.id,
"product_uom_qty": 20.0,
"price_unit": 5.0,
"tax_id": [(6, 0, cls.tax.ids)],
}
)
# Total untaxed amount: 10*10 + 25*4 + 20*5 = 100 + 100 + 100 = 300
def test_untaxed_amount_without_invoice(self):
"""No invoice yet: nothing invoiced, everything uninvoiced."""
self.assertEqual(self.sale_order_1.amount_untaxed, 300.0)
self.assertEqual(
self.sale_order_1.invoiced_untaxed_amount,
0.0,
"Invoiced Untaxed Amount should be 0.0 without invoice",
)
# Order still in draft: uninvoiced amount is forced to 0.0
self.assertEqual(
self.sale_order_1.uninvoiced_untaxed_amount,
0.0,
"Uninvoiced Untaxed Amount should be 0.0 while in draft",
)
def test_untaxed_amount_with_down_payment(self):
"""A fixed down payment of 100 is invoiced."""
context_payment = {
"active_ids": [self.sale_order_1.id],
"active_id": self.sale_order_1.id,
}
payment = (
self.env["sale.advance.payment.inv"]
.with_context(context_payment)
.create({"advance_payment_method": "fixed", "fixed_amount": 100})
)
payment.create_invoices()
# The fixed down payment (100) is a tax-included amount.
# With a 21% tax, the invoiced untaxed amount is 100 / 1.21 = 82.64.
self.assertAlmostEqual(
self.sale_order_1.invoiced_untaxed_amount,
82.64,
places=2,
msg="Invoiced Untaxed Amount should be 82.64 (100 tax included / 1.21)",
)
self.assertAlmostEqual(
self.sale_order_1.uninvoiced_untaxed_amount,
217.36,
places=2,
msg="Uninvoiced Untaxed Amount should be 217.36 (300 - 82.64)",
)
def test_untaxed_amount_fully_invoiced(self):
"""The whole order is invoiced: nothing remains uninvoiced."""
self.sale_order_1.action_confirm()
self.sale_order_1._create_invoices(final=True)
self.assertEqual(
self.sale_order_1.invoiced_untaxed_amount,
300.0,
"Invoiced Untaxed Amount should equal the full untaxed total",
)
self.assertEqual(
self.sale_order_1.uninvoiced_untaxed_amount,
0.0,
"Uninvoiced Untaxed Amount should be 0 once fully invoiced",
)

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_order_form_invoiced_untaxed_amount" model="ir.ui.view">
<field name="name">sale.order.form.invoiced.untaxed.amount</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<group name="sale_total" position="after">
<group name="invoiced_untaxed" class="oe_subtotal_footer order-2 order-lg-2" colspan="2">
<field name="invoiced_untaxed_amount" />
<field name="uninvoiced_untaxed_amount" />
</group>
</group>
</field>
</record>
<record id="view_order_tree_invoiced_untaxed_amount" model="ir.ui.view">
<field name="name">sale.order.list.invoiced.untaxed.amount</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_tax']" position="after">
<field name="invoiced_untaxed_amount" sum="Invoiced Untaxed Total" optional="hide" />
<field
name="uninvoiced_untaxed_amount"
sum="Uninvoiced Untaxed Total"
optional="hide"
/>
</xpath>
</field>
</record>
</odoo>