Skip to content

[19.0][IMP] contract UX ❤️#1427

Draft
bosd wants to merge 338 commits into
OCA:19.0from
bosd:19.0-imp-contract
Draft

[19.0][IMP] contract UX ❤️#1427
bosd wants to merge 338 commits into
OCA:19.0from
bosd:19.0-imp-contract

Conversation

@bosd
Copy link
Copy Markdown
Contributor

@bosd bosd commented May 7, 2026

Based upon #1312
To merged after that one.

Some bugfixes and UX polishments

  • [FIX] Fill last_date_invoiced
  • [IMP] Harden the cron job, no more silent failures.
  • Disable selection of archived products
  • Update icon to new odoo style
  • Activate activity in list view and the view itself
  • PDF proper filename
  • Place items in info block on pdf , so layouts such as bubble render correctly
  • Form view align headers with odoo styling
  • Use of various widgets, users, adress on form and list view
  • add demo data
  • Improve filtering and grouping (Future contracts, last date invoiced)
  • Improve kanban view (on EE rendered as empty cards)
  • Restore Print button on portal view
  • Improve Form view, reorder items so they align with odoo's SO form view
  • Contract tag, auto assign random color and use color widget
image

sergio-teruel and others added 30 commits October 1, 2025 13:54
There were an error in previous query for moving only contracts with the mark checked,
but it's also more logic to move them, but remain them disabled.
Currently translated at 22.2% (47 of 212 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/hr/
…emplate

Fix this use-case:

If the contract journal is not set on the contract template the contract is created
without a journal when confirming the sale order
… with duplicated name

- Don't execute onchange after invoice creation
  Using that approach (that is the current one in core)  has a lot of side effects and
  performance bottlenecks. You can read odoo/odoo#40156 for summarizing them.
  This also improves the handling of the values of payment term an fiscal position
  for using the partner ones if not set.
- Tests with duplicated name
  So they are not executed at all. Detected by chance looking for a test for the other PR.
- rename misnamed methods
- clarify _get_recurring_next_date
  First compute the next period end date,
  then derive the next invoice date from the next
  period stard and end date.
- handle max_date_end in _get_recurring_next_date
  This concentrates all next date calculation
  logic in one place, and will allow further simplifications.
- add next period start/end fields
  Add two computed field showing the next period
  start and end date. This improve the UX and will
  enable further simplifications in the code.
- refactor _get_period_to_invoice
  Move the part of the logic that compute the next
  period depending on the chosen next invoice date
  to _get_next_period_date_end.
Currently translated at 40.1% (85 of 212 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fr/
- REF: Refactor _update_recurring_next_date
  Reuse the logic that is now fully located in _get_recurring_next_date.
- REF: re-add _compute_first_recurring_next_date
  for backward compatibility
- FIX: add missing dependency in computed field
- REF: remove one monthlylastday special case
  get_relative_delta now works the same for all recurring rules.
  Move the special case handling to _init_last_date_invoiced
  which is used only for migration.
- IMP: support pre-paid for monthlylastday
  monthlylastday is (almost) not a special case anymore \o/.
  montlylastday is simply a montly period where the
  periods are aligned on month boundaries.
  The last bit of special casing is that postpaid generates
  invoice the day after the last dasy of the period, except
  for monthlylastday where the invoice is generated on the
  last day of the period. This last exception will disappear
  when we put the offset under user control.
  This is a breaking change because the post-paid/pre-paid
  mode becomes relevant for monthlylastday invoicing.
  The field becomes visible in the UI. Code that generate
  monthlylastday contract lines must now correctly set
  the pre-paid/post-paid mode too. Some tests have had
  to be adapted to reflect that.
- REF: make recurring_invoicing_offset a computed field
  In preparation to making it user modifiable.
- REF: make get_next_period_date_end public
  Make it public because it is the core logic of the module.
  Also, clarify that recurring_invoicing_type
  and recurring_invoicing_offset are needed only when
  we want the next period to be computed from a
  user chosen next invoice date.
- REF: rename _get_recurring_next_date as get_next_invoice_date
  It is easier to understand. Also make it public.
…combinations for next invoicing period + simplify _get_period_to_invoice
Currently translated at 100.0% (216 of 216 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/pt_BR/
- Add failing test for next invoice date before the last date invoiced
- raise an error when next invoice date before the last date invoiced
- Add note field to contract
- add new option: create_new_line_at_contract_line_renew
  Add a company config option to decide whether to create or to extend contract
  line at renew action
- extend contract line at renewal
- improve code: unify methods argument _renew_create_line and _renew_extend_line
Currently translated at 91.9% (204 of 222 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/it/
…ontract line stop + stop update recurring_next_date
If you have contracts in several companies, cron will create all of them, but
property fields will be populated with incorrect data as the taken company is
the main from the cron user (usually admin).
Currently translated at 38.1% (85 of 223 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fr/
Currently translated at 0.9% (2 of 223 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/es_CL/
Currently translated at 99.6% (222 of 223 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/es/
Currently translated at 38.6% (86 of 223 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fr/
I have detected a method that was created as redundant and with the same
technique used when preparing the line values, so better to have
everything together in the same method instead of having it spread.
It happen that a company has to trigger the invoicing action to generate invoices before
the scheduled date (to print and prepare invoices documents, check invoices, etc.).
This requires technical access for end users with the risk that this represents.

This commit adds a new wizard to run the invoicing action for a given date with a helper
to see and check the contract that will be invoiced. When the manual action is called,
the system displays all created invoices.

[12.0][IMP] - log the manual invoice action in contract chatter

[IMP] - Add alink to the invoice in contract message at manual invoicing

[IMP] - Improve code

[FIX] - log message for invoice creation only when there is an invoice

[IMP] - split the manual invoice menu into to menus sale & purhcase

[IMP] - hide invoice button if there is nothing to invoice
Currently translated at 91.0% (213 of 234 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/it/
Currently translated at 96.6% (226 of 234 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/pt_BR/
Currently translated at 100.0% (234 of 234 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fi/
Translated using Weblate (Portuguese)

Currently translated at 99.6% (233 of 234 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/pt/
Currently translated at 100.0% (234 of 234 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fi/
Currently translated at 100.0% (254 of 254 strings)

Translation: contract-12.0/contract-12.0-contract
Translate-URL: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract/fr/
@bosd bosd force-pushed the 19.0-imp-contract branch from ce773ef to 3d05d15 Compare May 7, 2026 22:37
BhaveshHeliconia and others added 7 commits May 8, 2026 10:29
The field is declared on contract.recurring.mixin and inherited by both
contract.line and contract.contract, but only the line variant is ever
written (in _update_last_date_invoiced, called from
_prepare_recurring_invoices_values on contract_lines). On contract.contract
the field is therefore always empty even after invoices are generated,
which also silently breaks _compute_next_period_date_start /
_compute_next_period_date_end on the contract record (they always fall
back to date_start).

Override the field on contract.contract as a stored compute aggregating
the max of contract_line_ids.last_date_invoiced, ignoring cancelled lines
and pure section/note rows. Mirrors the existing patterns for
recurring_next_date (min) and date_end (max).

Also expose the field as an optional column in the contract list view and
add a "Last Invoice" group-by filter in the search view, mirroring the
existing "Next Invoice" entries.
List view:
- Decorate finished contracts (date_end < today) muted, upcoming
  contracts (date_start > today) in blue.
- Show responsible with many2one_avatar_user, date_end as remaining_days,
  recurring_next_date as visible columns; date_start, journal_id and
  last_date_invoiced as opt-in optional columns.
- Add an activities column using the standard list_activity widget.

Form view:
- Render the responsible (user_id) with many2one_avatar_user.

Search view:
- Add a "My Contracts" filter and a user_id search field.
- Add Responsible and Journal group-bys.
- Drop the duplicate empty <separator/> and frame the In-progress /
  Finished and Archived filters between separators, matching the
  invoice search-view pattern.
The contract module had no kanban view defined, so users got Odoo's
default fallback rendering with only the contract name. Add a real
kanban view modelled after the invoice kanban: partner name (bold) and
tags on top, contract name in the middle, code and remaining days to
the next invoice in muted text, activity icon and responsible avatar
in the footer. The card menu carries a color picker plus Open,
Duplicate, Archive / Restore and Delete actions. The card itself uses
highlight_color="color" so the user-chosen color renders as a left
sidebar, mirroring project tasks.

Add a color integer field on contract.contract to back the color
picker.

Switch both customer and supplier action view_modes to
list,kanban,activity,form so the new kanban view becomes available and
the auto-generated activity view (from mail.activity.mixin) is reachable
from the breadcrumb.
The search view exposes both partner_id ("Associated Partner") and
commercial_partner_id ("Commercial Entity") as group-bys, but the
distinction is opaque to users. Rename to "Contact" and "Company": the
partner_id stores the actual contact selected (which may be a child
contact at a company), while commercial_partner_id resolves the parent
commercial entity used for cross-contract roll-ups. Same wording works
for both sale and purchase contract types.
product_id on contract.template.line had no domain, so archived
products remained selectable from the contract form. The customer and
supplier line views also overrode the field domain to filter by
sale_ok/purchase_ok, replacing any inherited active filter.

Add a default domain on the field excluding archived products, and
extend the customer and supplier domains to keep filtering by active
in addition to sale_ok / purchase_ok.
Replace the old generic OCA icon with a contract-specific icon, and
ship the SVG source alongside the PNG following OCA practice. This
fixes the broken icon on the community appstore listing and gives the
module a recognisable badge in activity headers, the apps list and the
breadcrumb.

Drop the unused legacy contract_icon.svg under static/src/img and
point the portal "Show Contracts" entry at the new SVG so the portal
home and the rest of Odoo show the same icon.
@bosd bosd force-pushed the 19.0-imp-contract branch from 3d05d15 to 811976b Compare May 8, 2026 07:25
@bosd bosd changed the title [19.0][IMP] contract [19.0][IMP] contract UX ❤️ May 8, 2026
@bosd bosd force-pushed the 19.0-imp-contract branch from 811976b to 457c2a0 Compare May 8, 2026 07:38
bosd added 5 commits May 8, 2026 09:54
Form view (contract.contract):
- Promote the contract name to <h1> for visual parity with sale
  orders / quotations.
- Render partner_id with res_partner_many2one for the chip-style
  display with avatar.
- Move Responsible (user_id) and Company out of the main header into
  a "Sales" group on the Other Information notebook page, where they
  belong with the rest of the back-office metadata.

Form view (contract.template):
- Same <h1> name treatment for the template form.
- Show the standard Archived ribbon when active is False.

List view (contract.contract):
- Add contract_template_id as an optional column.

Search view (contract.contract):
- Add an "Upcoming" filter for contracts whose date_start is in the
  future. Tighten "In progress" so a future-dated contract no longer
  matches both filters.

Search view (contract.template):
- Add an Archived filter, mirroring the contract search view, now
  that templates can be archived.

Note field is converted from Text to sanitised Html so users can
format their contract notes the same way they can on a sale order.
…tract

The contract.line model already has a date_start <= date_end constraint
(_check_start_end_dates in contract_line.py), but no equivalent existed
on contract.contract itself, so setting date_end before date_start on a
contract with line_recurrence=False (or directly on the contract record)
silently succeeded.

Add an api.constrains check on contract.contract that mirrors the
line-level constraint.
Filename:
- Add print_report_name to the report action so the downloaded file
  is "Contract - {name}.pdf" instead of the generic "report.pdf".

Layout (mimicking the sale order / quotation report):
- Drop the "Partner:" caption from the address block; the chosen
  contact widget already labels itself.
- Render the contract name as a large h2 heading at the top of the
  page.
- Add an "informations" bubble row carrying Reference, Date Start,
  Date End, Responsible and Payment Terms instead of cramming them
  into a single column.
- Promote the "Recurring Items", "Modifications" and "Notes" headings
  to h4 to match the visual hierarchy.
- Render the (now Html) note via t-field so saved formatting is
  preserved, and skip the section entirely when the note is empty.
Bring the portal contract page closer to the sale-order portal layout:

- Replace the small <h5> contract title with an <h2 class="quote_header_1">
  matching the SO portal heading style.
- Add a sidebar with Download and Print buttons so portal users can
  easily get a copy of the contract PDF.
- Surface Payment Terms in the general information block; previously
  they were not shown.
Ship demo records that exercise the various contract states and the
recurring-line description markers (#START#, #END#, #INVOICEMONTHNAME#),
so a fresh demo install immediately illustrates how the module behaves:

- Three tags (Premium, Renewal Required, Internal) showcasing the
  many2many tags widget with colours.
- A "Monthly Subscription Template" demonstrating how to set up a
  contract template with a section, a recurring product line using
  the description markers and a note line.
- Three contracts:
  - Demo Running Contract: started 6 months ago, runs another 6,
    monthly post-paid, tagged Premium.
  - Demo Expired Contract: ran for a year and ended last month,
    tagged Renewal Required.
  - Demo Upcoming Contract: starts next month, monthly pre-paid,
    instantiated from the template, multi-tagged.
@bosd
Copy link
Copy Markdown
Contributor Author

bosd commented May 8, 2026

info block pdf renders correctly now in bubule layout
image

@bosd bosd force-pushed the 19.0-imp-contract branch from 457c2a0 to 2e6ab46 Compare May 8, 2026 08:05
@bosd
Copy link
Copy Markdown
Contributor Author

bosd commented May 8, 2026

image

Apply correct headings tags, address widget.
Updated smartbutton icon, so it is aligned with what odoo core uses.

bosd added 2 commits May 10, 2026 11:47
…re flag

Make the recurring-invoice cron resilient to bad data on a single contract
without giving up batch performance.

Strategy:
* Fast path - one batched ``account.move.create`` call per company,
  identical to the upstream behaviour and performance.
* On failure - fall back to per-contract processing inside savepoints,
  so one bad contract does not block its company batch and SQL-level
  errors do not poison the transaction for the remaining contracts.

Failure visibility:
* New stored fields on contract.contract:
  - invoice_generation_error (Text)
  - invoice_generation_error_date (Datetime)
  - has_invoice_generation_error (Boolean, computed/stored)
* Search filter "Invoice Generation Failed" on the contract list.
* Orange warning triangle button in the list view that opens the form
  for inspection; row is also tinted with decoration-warning.
* Warning banner on the form sheet showing the error and a Dismiss
  button.
* On failure: chatter post + TODO activity assigned to the contract's
  responsible user; both de-duplicated so consecutive failures do not
  spam.
* On the next successful run: error fields are cleared and the activity
  is auto-resolved.

Tests cover:
* Single bad contract does not block healthy ones in the same company
* Error fields and activity are populated on failure
* Successful retry clears the flag and resolves the activity
* No duplicate chatter / activity on consecutive failures
* Healthy batches still hit the single-call fast path
* Manual Dismiss button clears the flag without running the cron
* Drop unused ``contracts`` local in
  ``test_cron_isolates_failing_contract`` (ruff F841)
* Apply ruff-format pass over the new test cases and the form-view
  banner indentation
* Use ``_logger.error(..., exc_info=exc)`` instead of
  ``_logger.exception`` in ``_record_invoice_generation_error``. The
  helper is also called outside an active ``except`` block (e.g. when
  tests seed the failure flag directly), which made
  ``_logger.exception`` log a useless ``NoneType: None`` line.
  ``exc_info=exc`` logs the traceback when one is attached to the
  exception and a clean line otherwise.
@bosd
Copy link
Copy Markdown
Contributor Author

bosd commented May 10, 2026

Robust recurring-invoice cron — client context

Pushed two commits (a8c88773, e33a2676) that harden the recurring-invoice cron and surface failures per contract in the UI.

The problem

We hit this on a multi-company production deployment with a few hundred active recurring contracts. One contract had a bad data entry that made _prepare_invoice raise during the cron run. Because the upstream cron processes all of a company's contracts in a single batched account.move.create([...]), the SQL transaction rolled back the whole batch — no contracts at all got invoiced for that company on that day.

The painful part for the client wasn't just the missed invoicing run, it was finding the bad apple. There's no per-contract error trail. The cron log shows one stack trace (without naming the offending contract clearly) and there's no in-UI marker. Their bookkeeper had to scan hundreds of contracts manually to figure out which one was tripping the cron.

What this PR adds

  • Two-tier execution:
    • Fast path — one batched account.move.create([...]) per company (identical to the previous behaviour and performance).
    • On failure — fall back to per-contract processing inside cr.savepoint() so a single contract with bad data does not block its company batch and SQL-level errors don't poison the transaction for the rest.
  • Per-contract failure trail:
    • New stored fields invoice_generation_error (Text), invoice_generation_error_date (Datetime), has_invoice_generation_error (Boolean).
    • Invoice Generation Failed filter in the contract search view — one click and you have the bad apples.
    • Orange fa-exclamation-triangle icon at the start of the row in the contract list, plus decoration-warning row tint, so the failed contracts are visible at a glance even without filtering.
    • Form-view warning banner with the error message, date, and a Dismiss button so the responsible user can clear it after fixing.
    • Chatter post + TODO activity assigned to the contract's responsible user. Both are de-duplicated so consecutive cron runs against persistent bad data don't spam.
  • Auto-clear on recovery: the next successful cron run for that contract clears the error fields and resolves the activity.

Why per-contract savepoints (not just a per-contract try/except)

We started with a plain try/except per contract and the client was happy. But it only catches Python-level errors before the cursor flushes. If the failure is SQL-level (FK violation, unique constraint, check constraint), PostgreSQL marks the transaction aborted; every subsequent contract in the loop then fails with current transaction is aborted. The cr.savepoint() context manager issues SAVEPOINT / ROLLBACK TO SAVEPOINT, isolating each contract's writes properly.

Tests

Six new test cases in test_contract.py:

  • one bad contract is isolated; the healthy ones still get invoiced
  • error fields and activity are populated on failure
  • a successful retry clears the flag and resolves the activity
  • no duplicate chatter / activity on consecutive failures
  • healthy batches still hit the single-call fast path (verified by spying on _recurring_create_invoice)
  • Dismiss action clears the flag without running the cron

Full contract suite: 75 tests, 0 failures, 0 errors locally. Pre-commit: clean.

bosd added 8 commits May 10, 2026 12:07
The CI ``checklog-odoo`` step fails the build on any ERROR line in the
test log. ``test_cron_clears_error_on_recovery`` and
``test_action_clear_invoice_generation_error`` deliberately call
``_record_invoice_generation_error`` to seed a flagged contract, which
emits a legitimate ERROR log line. Wrap those seeding calls in
``mute_logger`` so the test path stays silent.

Tests that exercise the cron itself already use ``mute_logger``; this
just covers the two helpers that seed the flag directly.
Odoo core PR #248401 introduced a new JS widget
(product_label_section_and_note_field) that depends on the
translated_product_name field. Since contract.line uses this widget
but does not define the field, opening a contract form crashes with
KeyError: 'translated_product_name'.

Add a computed Text field that mirrors the pattern used by
sale.order.line in Odoo core: it returns the product display_name
translated into the contract partner's language.
This PR introduces schema changes (color, last_date_invoiced compute,
the three invoice_generation_error fields), view replacements (form
header / list view / search filter), and new constraints. Deploy
runners that gate odoo -u on a manifest version change would
otherwise leave the database on the previous schema and crash on
first access with errors like:

  psycopg2.errors.UndefinedColumn: column contract_contract.color does not exist

Bump the minor version so the next deploy auto-runs the module
update.
Without an explicit string=, Odoo auto-derives the column header
from the field name. On stored compute fields that redefine a
mixin-level field, the auto-derived label can be confusing or even
collide with another field's label depending on locale and the field
order in which the registry is built. Production users saw the
contract list-view column for last_date_invoiced rendered as
'Last Updated On' (the default label of write_date).

Set string="Date of Last Invoice" on the field on both
contract.recurring.mixin and the contract.contract override so
the label is deterministic across the form, list, search, and
group-by views. Mirrors the style of the sibling
recurring_next_date field ("Date of Next Invoice").

Bump manifest 19.0.1.1.0 -> 19.0.1.1.1 so deploy runners pick up the
new label.
The redesigned portal contract page (commit bbf9fa8) used
``data-snippet="s_timeline"`` and ``data-snippet="s_card"``
attributes on the modification timeline. These attributes are how
the website module discovers snippets to wire JS Interactions to.
On a portal page (no website builder) the Interaction's
``setup()`` runs but the DOM children it expects to find via
``this.el.querySelector(...)`` are absent, so it crashes with:

  TypeError: can't access property 'dataset',
  this.el.querySelector(...) is null

Drop the ``data-snippet`` attributes. The visual classes
(``s_timeline``, ``s_timeline_card``, etc.) stay so the
layout is unchanged. The Interaction is no longer wired up, so it
can't fail.

Manifest 19.0.1.1.1 -> 19.0.1.1.2.
Bring the contract tag administration in line with Odoo's other tag
models (res.partner.category, crm.tag, …):

- Drop the separate form view and switch the list to editable="bottom",
  so administrators manage tags inline without a popup.
- Render the color column with widget="color_picker", which makes it
  obvious which colour the tag uses and lets users change it in place.
- Pick a random colour (1-11) by default when a tag is created, so new
  tags don't all collapse onto colour 0 in the kanban / many2many_tags
  widgets.
- Drop the form view_mode on the action since the form view no longer
  exists.
The ContractContract.group_id field is a Many2one to
account.analytic.account, computed from the lines' analytic
distribution. The historical 'Group' label dates from a much older
version where contracts were grouped by analytic account, and the
field name still carries the legacy concept. To anyone reading a
contract today the label is opaque: 'Group of what?'

Rename the label to 'Analytic Account', matching what every other
place in Odoo calls the same model. Add a help string so the
relationship to the line-level analytic distribution is documented.

No schema or behaviour change -- pure UX cleanup. Manifest
19.0.1.1.2 -> 19.0.1.1.3 so opaas re-runs -u and the new label
lands in the database.
The "Other Information" tab on the contract form had two outer
``<group>`` blocks. The first wrapped a single inner "Sales" group
(rendered on the left half, with the right half empty); the second
was a bare top-level ``<group>`` containing ``code``, ``group_id``
and ``currency_id`` -- which Odoo's form layout renders as a single
full-width column, so each of those fields was visibly wider than
the Sales group fields above and broke the visual rhythm of the
tab.

Merge them into one outer ``<group>`` with two inner groups
(``Sales`` + ``Administration``), so the whole tab renders as a
consistent 2-column grid and the second column lines up next to
the first.

No model change, no functional change -- pure layout fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.