From af20c0150e7c45f37171dc47e1089326c02eb3af Mon Sep 17 00:00:00 2001 From: bosd <5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me> Date: Thu, 11 Jun 2026 09:02:38 +0200 Subject: [PATCH 1/5] [IMP] contract: compute and display last_date_invoiced on contracts Aggregate the latest invoiced date from the contract lines into a stored computed field on contract.contract, label it explicitly in the views, and bump the manifest version. --- contract/__manifest__.py | 2 +- contract/models/contract.py | 23 ++++++++++++++++++ contract/models/contract_recurring_mixin.py | 1 + contract/tests/test_contract.py | 27 +++++++++++++++++++++ contract/views/contract.xml | 7 ++++++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 644a317788..4b0fc0675c 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -11,7 +11,7 @@ { "name": "Recurring - Contracts Management", - "version": "19.0.1.0.0", + "version": "19.0.1.1.3", "category": "Contract Management", "license": "AGPL-3", "author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)", diff --git a/contract/models/contract.py b/contract/models/contract.py index 8008e24fa9..7ad95dd0b0 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -124,6 +124,13 @@ class ContractContract(models.Model): # === Dates === date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False) + last_date_invoiced = fields.Date( + string="Date of Last Invoice", + compute="_compute_last_date_invoiced", + store=True, + readonly=True, + copy=False, + ) # === Compute Methods === @@ -210,6 +217,21 @@ def _compute_date_end(self): if date_end and all(date_end): contract.date_end = max(date_end) + @api.depends( + "contract_line_ids.last_date_invoiced", + "contract_line_ids.is_canceled", + ) + def _compute_last_date_invoiced(self): + for contract in self: + dates = contract.contract_line_ids.filtered( + lambda line: ( + line.last_date_invoiced + and not line.is_canceled + and (not line.display_type or line.is_recurring_note) + ) + ).mapped("last_date_invoiced") + contract.last_date_invoiced = max(dates) if dates else False + def _inverse_partner_id(self): for rec in self: if not rec.invoice_partner_id: @@ -655,6 +677,7 @@ def _recurring_create_invoice(self, date_ref=False): self._add_contract_origin(moves) self._invoice_followers(moves) self._compute_recurring_next_date() + self._compute_last_date_invoiced() return moves @api.model diff --git a/contract/models/contract_recurring_mixin.py b/contract/models/contract_recurring_mixin.py index 88b1659fb3..34dedfd1b0 100644 --- a/contract/models/contract_recurring_mixin.py +++ b/contract/models/contract_recurring_mixin.py @@ -70,6 +70,7 @@ class ContractRecurringMixin(models.AbstractModel): # Define when and how invoices should be issued within the recurrence. last_date_invoiced = fields.Date( + string="Date of Last Invoice", readonly=True, copy=False, ) diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index c42de5ad82..12e3cca2bd 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -533,6 +533,33 @@ def test_last_invoice_pre_paid(self): "Should not create a new invoice after the last one", ) + def test_contract_last_date_invoiced(self): + """contract.contract.last_date_invoiced aggregates max of line values, + excluding cancelled lines and pure section/note rows.""" + self.assertFalse(self.acct_line.last_date_invoiced) + self.assertFalse(self.contract.last_date_invoiced) + self.acct_line.date_start = "2018-01-01" + self.acct_line.recurring_invoicing_type = "post-paid" + self.acct_line.date_end = "2018-03-15" + self.contract.recurring_create_invoice() + self.assertEqual(self.acct_line.last_date_invoiced, to_date("2018-01-31")) + self.assertEqual(self.contract.last_date_invoiced, to_date("2018-01-31")) + line2 = self.env["contract.line"].create( + dict(self.line_vals, recurring_next_date="2018-04-15") + ) + line2.last_date_invoiced = "2018-03-31" + self.assertEqual(self.contract.last_date_invoiced, to_date("2018-03-31")) + line2.is_canceled = True + self.assertEqual(self.contract.last_date_invoiced, to_date("2018-01-31")) + self.env["contract.line"].create( + { + "contract_id": self.contract.id, + "display_type": "line_section", + "name": "Header", + } + ) + self.assertEqual(self.contract.last_date_invoiced, to_date("2018-01-31")) + def test_onchange_partner_id(self): self.contract._onchange_partner_id() self.assertEqual( diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 50dab8bac4..97d2c383fe 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -416,6 +416,7 @@ widget="many2many_tags" options="{'color_field': 'color'}" /> + @@ -469,6 +470,12 @@ domain="[('recurring_next_date', '!=', False)]" context="{'group_by':'recurring_next_date'}" /> + Date: Thu, 11 Jun 2026 09:02:54 +0200 Subject: [PATCH 2/5] [IMP] contract: revamp form, list, search and kanban views Add a kanban view and enable the activity view, improve the list and search view UX, add a partner search group-by, refine the form layout (Other Information in a single two-column grid, 'Analytic Account' label) and streamline the contract.tag UX. --- contract/models/contract.py | 12 ++- contract/models/contract_tag.py | 9 +- contract/models/contract_template.py | 1 + contract/views/contract.xml | 151 ++++++++++++++++++++++----- contract/views/contract_tag.xml | 21 +--- contract/views/contract_template.xml | 16 ++- 6 files changed, 159 insertions(+), 51 deletions(-) diff --git a/contract/models/contract.py b/contract/models/contract.py index 7ad95dd0b0..687a30deb7 100644 --- a/contract/models/contract.py +++ b/contract/models/contract.py @@ -39,15 +39,23 @@ class ContractContract(models.Model): default=lambda self: self.env.user, ) group_id = fields.Many2one( - string="Group", + string="Analytic Account", comodel_name="account.analytic.account", compute="_compute_group_id", store=True, readonly=False, ondelete="restrict", + help=( + "Analytic account shared by every line of this contract. " + "Computed from the lines' analytic distribution when all lines " + "point at the same account; left empty when the lines use " + "different accounts. Stored compute, editable: clearing or " + "setting it manually overrides the computed value." + ), ) tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags") - note = fields.Text(string="Notes") + color = fields.Integer(string="Color Index") + note = fields.Html(string="Notes", sanitize=True) # === Partner and Commercial Info === partner_id = fields.Many2one( diff --git a/contract/models/contract_tag.py b/contract/models/contract_tag.py index 9cd98aeaa7..60673d5bb6 100644 --- a/contract/models/contract_tag.py +++ b/contract/models/contract_tag.py @@ -1,6 +1,8 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from random import randint + from odoo import fields, models @@ -8,8 +10,13 @@ class ContractTag(models.Model): _name = "contract.tag" _description = "Contract Tag" + def _get_default_color(self): + return randint(1, 11) + name = fields.Char(required=True) company_id = fields.Many2one( "res.company", string="Company", default=lambda self: self.env.company ) - color = fields.Integer("Color Index", default=0) + color = fields.Integer( + string="Color Index", default=lambda self: self._get_default_color() + ) diff --git a/contract/models/contract_template.py b/contract/models/contract_template.py index b886071e26..dcc7878f50 100644 --- a/contract/models/contract_template.py +++ b/contract/models/contract_template.py @@ -20,6 +20,7 @@ class ContractTemplate(models.Model): # === Basic Info === + active = fields.Boolean(default=True) name = fields.Char(required=True) partner_id = fields.Many2one( comodel_name="res.partner", diff --git a/contract/views/contract.xml b/contract/views/contract.xml index 97d2c383fe..1d8b4da140 100644 --- a/contract/views/contract.xml +++ b/contract/views/contract.xml @@ -28,7 +28,7 @@