Skip to content

[17.0][MIG] sale_order_merge#3904

Open
edescalona wants to merge 19 commits intoOCA:17.0from
BinhexTeam:17.0-mig-sale_order_merge
Open

[17.0][MIG] sale_order_merge#3904
edescalona wants to merge 19 commits intoOCA:17.0from
BinhexTeam:17.0-mig-sale_order_merge

Conversation

@edescalona
Copy link
Copy Markdown
Contributor

@edescalona edescalona commented Sep 18, 2025

@BinhexTeam

This PR was based on [8.0] sale_order_merge, so several changes were made.

The merge criteria are defined as follows:

  • Same customer, shipping address, warehouse, and company.
  • Orders must be in "Draft" status.
  • You can also merge confirmed orders if you enable the Merge Order Confirmation setting.

ping @StefanRijnhart @gurneyalex

@edescalona edescalona marked this pull request as ready for review September 18, 2025 23:44
@edescalona edescalona marked this pull request as draft September 19, 2025 14:31
@edescalona edescalona force-pushed the 17.0-mig-sale_order_merge branch from 1b8fb0e to 8be9119 Compare September 20, 2025 02:05
@edescalona edescalona marked this pull request as ready for review September 23, 2025 12:55
@rousseldenis
Copy link
Copy Markdown
Contributor

/ocabot migration sale_order_merge

Copy link
Copy Markdown
Contributor

@rousseldenis rousseldenis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM even if I'm wondering if compute method for 'merge_with' could be improved

@edescalona
Copy link
Copy Markdown
Contributor Author

Hi @rousseldenis , maybe it can be improved, but well, try to remove only what doesn't work in the target version.

Comment thread sale_order_merge/models/sale_order.py
Comment thread sale_order_merge/wizards/wizard_sale_order_merge.py Outdated
@edescalona
Copy link
Copy Markdown
Contributor Author

Hi @rousseldenis @hildickethan , I'm going to make the changes you mentioned to me to adapt them to the guidelines. Thank you for your feedback.

@edescalona
Copy link
Copy Markdown
Contributor Author

Hi @rousseldenis @hildickethan , I'm covering the tests, I'll do squash at the end.

@edescalona edescalona force-pushed the 17.0-mig-sale_order_merge branch 2 times, most recently from b5dd279 to c7e047e Compare September 29, 2025 20:46
@edescalona edescalona changed the title [17.0][MIG] sale_order_merge [17.0][MIG][IMP] sale_order_merge Sep 29, 2025
@edescalona edescalona changed the title [17.0][MIG][IMP] sale_order_merge [17.0][MIG] sale_order_merge Sep 29, 2025
@edescalona edescalona force-pushed the 17.0-mig-sale_order_merge branch 4 times, most recently from aaa1f42 to 3642728 Compare October 3, 2025 15:29
@edescalona edescalona force-pushed the 17.0-mig-sale_order_merge branch from 3642728 to 541787e Compare October 3, 2025 15:59
@edescalona
Copy link
Copy Markdown
Contributor Author

ping @rousseldenis @hildickethan

@hildickethan
Copy link
Copy Markdown
Member

Hi @rousseldenis @hildickethan , I'm going to make the changes you mentioned to me to adapt them to the guidelines. Thank you for your feedback.

@edescalona what happened to this? Was there some issue with migrating the fields?

@edescalona
Copy link
Copy Markdown
Contributor Author

ping @hildickethan @rousseldenis

Copy link
Copy Markdown

@alexey-pelykh alexey-pelykh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: [17.0][MIG] sale_order_merge

Thanks for the migration effort -- this is a useful module. I reviewed the code for Odoo 17 API compatibility, OCA conventions, and correctness. CI is green on tests (OCB and Odoo) and pre-commit, which is good. A few items below.


Bug: wrong order name in merge chatter message

wizard_sale_order_merge.py, merge() method, line 162

The cancelled orders post a message referencing themselves rather than the target order:

order.message_post(body=_("Merged into %(order)s") % {"order": order.name})

order.name here is the name of the order being cancelled. Should be self.order_id.name (the merge target).

Additionally at line 165, self.to_merge.mapped("name") includes the target itself. Consider using orders.mapped("name") (where orders = self.to_merge - self.order_id, already computed above).


Deprecated key: view_type in action dicts

view_type was removed from ir.actions.act_window in Odoo 13. Two occurrences:

  1. sale_order.py:192 -- action_button_merge() return dict
  2. wizard_sale_order_merge.py:130 -- open_sale() return dict

Please remove the "view_type": "form" lines from both.


Unresolved review feedback: field naming conventions

@hildickethan raised this earlier and it has not been addressed:

  • merge_with (Many2many on sale.order) -- OCA convention is to suffix relational fields: consider merge_with_ids or mergeable_order_ids
  • to_merge (Many2many on wizard) -- consider to_merge_ids
  • mergeable (Many2many on wizard) -- consider mergeable_ids

The order_id M2O already follows the convention correctly.


_compute_merge_with performs a search per record

The compute method runs self.search([("merge_with", "=", sale.id)]) inside a for sale in self loop. For list views with many orders this could be expensive. Since the field is non-stored, consider whether merge_ok alone is sufficient to gate the UI button and the full merge_with set can be resolved lazily only when the merge button is clicked.


_get_orders_selected uses _read_group as a per-record iterator

_read_group with groupby=["id"] produces one group per record, which is equivalent to iterating self. A simpler direct iteration (for order in self: ...) would improve readability and avoid relying on the id pseudo-field's return type from _read_group.


merge_moves -- invoice line reassignment bypasses recomputation

Line 59 writes move_id directly on invoice_line_ids to move them between account.move records. This bypasses the accounting engine's recomputation of tax lines, sequences, and balanced-entry checks. It only applies to draft invoices which limits risk, but verifying the merged invoice renders correctly after this operation (and potentially calling _onchange_invoice_line_ids() or _compute_amount() on the full move) would be safer.


Minor / style

  • License header year: The copyright headers carry only 2016 (from the 8.0 original). Consider adding the current year and the migrating author per OCA conventions.
  • _merge_order_by_states reads ir.config_parameter via sudo() on every call, and it is called multiple times per flow. Consider caching per-request.
  • Missing @api.depends on _compute_merge_with: non-stored computed fields without @api.depends are recomputed on every access. Adding an explicit empty depends or a docstring comment would help future maintainers understand the intent.

Overall the module is functional and well-tested. The merge message bug and the deprecated view_type keys are the most important items to fix before merge. The naming convention feedback from the earlier review also remains open.

self.merge_order_lines()
for order in orders:
order.with_context(disable_cancel_warning=True).action_cancel()
order.message_post(body=_("Merged into %(order)s") % {"order": order.name})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: order.name is the name of the order being cancelled, not the merge target. This should be self.order_id.name:

Suggested change
order.message_post(body=_("Merged into %(order)s") % {"order": order.name})
order.message_post(body=_("Merged into %(order)s") % {"order": self.order_id.name})

order.message_post(body=_("Merged into %(order)s") % {"order": order.name})
self.order_id.message_post(
body=_("Order(s) %(orders)s merged into this one")
% {"orders": ",".join(self.to_merge.mapped("name"))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.to_merge includes the target order (self.order_id), so the message lists the target among "merged into this one". Consider using orders (already defined above as self.to_merge - self.order_id):

Suggested change
% {"orders": ",".join(self.to_merge.mapped("name"))}
% {"orders": ",".join(orders.mapped("name"))}

self.ensure_one()
return {
"name": _("Merged sale order"),
"view_type": "form",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view_type was removed from ir.actions.act_window in Odoo 13. Please remove this line.

)
return {
"name": _("Merge sale orders"),
"view_type": "form",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view_type was removed from ir.actions.act_window in Odoo 13. Please remove this line.

Copy link
Copy Markdown

@alexey-pelykh alexey-pelykh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for migrating this module to 17.0. I have reviewed the code and found several issues that should be addressed.

Summary:

  • One bug in the merge message (wrong order name referenced)
  • Stale view_type keys from old Odoo versions that should be removed for a clean 17.0 migration
  • Translation files reference old 8.0 model paths that no longer exist
  • Potential data integrity concern in picking merge logic
  • Performance concern in _compute_merge_with

See inline comments for details.

any(order.state in ("sent", "draft") for order in orders)
and self.order_id.state == "sale"
):
self._confirm_orders_by_order_target(orders)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: order.name here refers to the source order being iterated, not the target order. This message is posted on the source order and should say it was merged into the target, so this should be self.order_id.name:

order.message_post(body=_("Merged into %(order)s") % {"order": self.order_id.name})

merge_ids = self.search(domain)
if len(self) > 1:
(self - merge_ids)._validate_selected(order)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view_type was removed in Odoo 17.0 (it was deprecated since ~14.0). This key is silently ignored but should be removed for a clean migration. Same applies to the open_sale method in the wizard.

("company_id", "=", self.company_id.id),
("currency_id", "=", self.currency_id.id),
("state", "in", self._merge_order_by_states()),
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The op and arg parameters are ignored. This means the search method only works correctly for the specific ("merge_with", "=", sale.id) usage, but any other operator (e.g. !=, in) would return wrong results silently. Consider at minimum validating that op == "=" and raising NotImplementedError for unsupported operators, or better yet, handling common operators.

self.env["ir.config_parameter"]
.sudo()
.get_param("sale_order_merge.merge_order_confirm", False)
):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This compute method performs a self.search(...) call for each record in the recordset, which is an N+1 pattern. For a list view with many orders this could be slow. Consider whether this can be optimized, e.g. by batching the search or restructuring the computation. At minimum, adding a note about the performance implications would be helpful.

if self._picking_can_merge(picking):
key = self._get_picking_map_key(picking)
if key not in pick_map:
pick_map[key] = self.env["stock.picking"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When merging pickings, move_line_ids (stock.move.line) are reassigned to the target picking, but the corresponding move_ids (stock.move) are not moved. This can leave stock.move records pointing to the source picking, which will then be missing its move lines. The stock.move records should also be reassigned to the target picking before unlinking the source pickings (which doesn't seem to happen either -- source pickings with no moves/move_lines are left dangling).

name="name"
>res.config.settings.view.form.inherit.sale_order_merge</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inherits base.res_config_settings_view_form which places the setting in the General Settings page. Since this is a sale-related setting, it should inherit sale.res_config_settings_view_form and use an appropriate xpath to place it in the Sales settings section. The xpath //setting[@id='no_edit_order'] would need to be adjusted accordingly.

target = pickings[0]
if len(pickings) > 1:
pickings -= target
pickings.mapped("move_line_ids").write({"picking_id": target.id})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other action dict: remove the view_type key, which was removed in Odoo 17.0.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants