Skip to content

[19.0] sale_management: converted sale.order.option lines are counted in the order total #5970

Description

@melitonio

Summary

openupgrade_scripts/scripts/sale_management/19.0.1.0/post-migration.py converts sale.order.option records into sale.order.line records with is_optional=True. The resulting lines are not treated as optional by Odoo 19, so they are added to the order total.

On a real database this silently increased the total of a draft quotation by ~12%.

Why the converted lines are not optional

In 19.0, is_optional is a property of a section, not of an individual line. sale_management/models/sale_order_line.py:

is_optional = fields.Boolean(
    string="Optional Line",
    copy=True,
    default=False,
)  # Whether this section's lines are optional in the portal.

def _is_line_optional(self):
    self.ensure_one()
    return (
        self.parent_id.is_optional
        or (
            self.parent_id.display_type == 'line_subsection'
            and self.parent_id.parent_id.is_optional
        )
    )

_is_line_optional() reads self.parent_id.is_optional, and parent_id is a computed, non-stored field derived from the ordering of the lines (sale/models/sale_order_line.py::_compute_parent_id, it resolves to the last section preceding the line).

The migration script creates each converted option as a bare product line:

line = SaleOrderLine.create({
    "is_optional": True,
    "order_id": order_id,
    ...
    "sequence": sequence,
    ...
})

with is_optional set on the line itself and the sequence copied from the option (frequently 0). The line therefore sorts to the top of the order, has no preceding section, parent_id is empty, _is_line_optional() returns False, and the line is treated as a regular billable line.

Steps to reproduce

  1. On 18.0, create a quotation with regular lines and at least one sale.order.option that was never added to the order (line_id IS NULL).
  2. Note sale_order.amount_total.
  3. Migrate to 19.0 with OpenUpgrade.
  4. amount_total has grown by the value of the option.

Observed

Migrating a production database (15.0 → 16 → 17 → 18 → 19, three sale.order.option rows in total):

Quotation State Before After Delta
A draft 494,000,000 554,375,000 +60,375,000
B cancel 180,525,103 191,335,103 +10,810,000

The sum of the non-optional lines after migration matches the pre-migration total exactly, which confirms the delta comes only from the converted options.

The migration completes with exit code 0 and no warning, so this is easy to miss.

Suggested fix

Either:

  • place the converted options under a section with is_optional=True (creating one per order if needed) and give them a sequence that puts them after it, so _compute_parent_id resolves correctly; or
  • if that is considered out of scope for the script, skip options whose line_id was NULL (they were never part of the order) and log them, rather than materialising them as billable lines.

Happy to send a PR if the maintainers have a preference between the two.

Versions

  • OpenUpgrade branch 19.0 (cloned 2026-09-05)
  • Odoo 19.0 official Docker image (19.0-20260817)
  • PostgreSQL 16

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions