Skip to content

[18.0][ADD] sale_order_add_byproduct#3760

Open
bosd wants to merge 5 commits intoOCA:18.0from
bosd:18.0-sale_order_add_byproduct
Open

[18.0][ADD] sale_order_add_byproduct#3760
bosd wants to merge 5 commits intoOCA:18.0from
bosd:18.0-sale_order_add_byproduct

Conversation

@bosd
Copy link
Copy Markdown
Contributor

@bosd bosd commented Jun 16, 2025

This module is designed for contract manufacturing scenarios where the
client provides raw materials or a base product for processing. Unlike
standard manufacturing where the output quantity of finished goods is
precisely known, this use case involves a process where the exact yield
of by-products is uncertain until after the manufacturing operation
is complete.

Scenario

A contract manufacturer processes materials supplied by a client. During
this process, in addition to any primary output, a variable quantity of
by-products is inevitably generated. The client is obligated to purchase
these by-products from the contract manufacturer.

@bosd bosd force-pushed the 18.0-sale_order_add_byproduct branch 3 times, most recently from d56a75e to f961ee9 Compare June 16, 2025 08:41
@bosd bosd force-pushed the 18.0-sale_order_add_byproduct branch 6 times, most recently from 9581476 to fedbd8a Compare September 15, 2025 10:22
@rousseldenis rousseldenis added this to the 18.0 milestone Oct 7, 2025
continue
quantity_produced = sum(byproduct_move.move_line_ids.mapped("quantity"))

if not byproduct_move.product_id.sale_ok:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This has to be put at the beginning of the loop to avoid too much conditions evaluations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Moreover, I would have put this in a separate function like _get_byproduct_moves_to_add() or somethng like that.

@bosd bosd force-pushed the 18.0-sale_order_add_byproduct branch 2 times, most recently from 0ce29bc to 7ed5507 Compare October 17, 2025 23:29
@bosd
Copy link
Copy Markdown
Contributor Author

bosd commented Oct 18, 2025

Thanks for the review.
CI is still red, but that seems to come from a different module.

bosd added 5 commits November 10, 2025 10:29
- Add res_company model with byproduct_note_template field
- Update res_config_settings to use compute/inverse methods for company-specific settings
- Update mrp_production to use company-specific template with fallback to global config
- Add proper fallback logic: company setting -> global config -> default value
- Maintain backward compatibility with existing global configurations
- Include _get_byproduct_moves_to_add() method for cleaner code structure
- Apply proper float_compare() usage as per review
- Update __init__.py to import new company model
- Change _logger.info() calls to _logger.debug() for internal processing messages
- Maintain appropriate logging level for diagnostic information
- Keep debug level for internal processing logs to reduce noise in production
- Apply to all logging calls in the _add_byproducts_to_sale_order method
@bosd bosd force-pushed the 18.0-sale_order_add_byproduct branch from 7ed5507 to 4d83fc5 Compare November 10, 2025 09:29
Copy link
Copy Markdown

@Dranyel-Bosd Dranyel-Bosd left a comment

Choose a reason for hiding this comment

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

LGTM

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: REQUEST_CHANGES

Thank you for the contribution -- the business case is well-documented and the test coverage is solid. However, there are several correctness issues and OCA convention gaps that should be addressed before merge.

Critical Findings

# File Severity Finding
1 sale_order_line.py Bug _action_launch_stock_rule override breaks mixed recordsets -- returns True for the entire batch when ANY line is a byproduct, silently skipping procurement for legitimate lines
2 sale_order_line.py Bug create override may double-trigger procurement for non-byproduct lines added to confirmed SOs (Odoo already handles this)
3 mrp_production.py Dead code Duplicate SO search at L42-45 (origin.startswith("S") branch does the exact same query as L38-40)
4 mrp_production.py Security Note template uses Python str.format() with user-controlled template string from ir.config_parameter -- could expose object attributes via {0.__class__} style injection
5 sale_order_line.py OCA Missing license header
6 res_config_settings.py Convention Reviewer rousseldenis already flagged: prefer config_parameter= attribute or a proper company-level field with related= instead of manual compute/inverse

Suggestions

# File Finding
7 __manifest__.py Remove commented-out "demo" key
8 Tests Excessive invalidate_all() + re-browse suggests misunderstanding of ORM cache -- consider simplifying
9 mrp_production.py bom_param argument in helper is accepted but never used -- remove or implement


# Call super only on lines that should be procured
if lines_not_to_procure:
return True
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 (critical): This logic is inverted for mixed recordsets. If self contains 5 lines and 1 is a byproduct, this returns True immediately and skips procurement for the other 4 lines.

The correct pattern is to exclude byproduct lines and call super() on the remaining lines:

def _action_launch_stock_rule(self, previous_product_uom_qty=False):
    lines_to_procure = self.filtered(lambda line: not line.is_mrp_byproduct_line)
    if not lines_to_procure:
        return True
    return super(SaleOrderLine, lines_to_procure)._action_launch_stock_rule(
        previous_product_uom_qty=previous_product_uom_qty
    )

for line in lines:
if line.order_id.state == "sale" and not line.is_mrp_byproduct_line:
line._action_launch_stock_rule()
return lines
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: Overriding create to manually call _action_launch_stock_rule() for non-byproduct lines is risky. Odoo's standard sale.order.line already triggers procurement on line creation for confirmed SOs via its own mechanism. This override may cause double procurement (two MOs created for one SO line).

I believe the _action_launch_stock_rule override above (once fixed) is sufficient to prevent byproduct lines from triggering procurement. This entire create override should likely be removed.

If it IS needed, please add a test that demonstrates the scenario it solves -- currently no test covers the create-triggered procurement path specifically.

if not sale_order and self.origin.startswith("S"):
sale_order = self.env["sale.order"].search(
[("name", "=", self.origin)], limit=1
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead code: This if branch (self.origin.startswith("S")) executes the exact same search as lines 38-40 above:

sale_order = self.env["sale.order"].search(
    [("name", "=", self.origin)], limit=1
)

This duplicate search can never find anything the first one missed. Remove this branch entirely.

if config_param:
note = config_param.format(
product_name=product_to_add.name, mo_name=self.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.

Security concern: Using str.format() with a user-controlled template string is a known Python injection vector. A malicious admin could craft a template like {product_name.__class__.__mro__} to leak internal Python object information.

Consider using safe_substitute from string.Template, or restrict the format to known placeholders via a simple .replace() approach:

note = config_param.replace("{product_name}", product_to_add.name).replace("{mo_name}", self.name)

@@ -0,0 +1,35 @@
from odoo import fields, models
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OCA convention: Missing license header. All Python files in OCA modules must have the copyright and license header:

# Copyright 2025 OBS Solutions BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

"license": "AGPL-3",
"depends": ["mrp", "sale_management", "sale_stock"],
"data": ["data/config_parameter_data.xml", "views/res_config_settings_views.xml"],
# "demo": ["demo/demo_data.xml"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cleanup: Remove the commented-out "demo" line. OCA guidelines discourage committed commented-out code.


byproduct_note_template = fields.Char(
string="By-product Note Template",
help="Template for the note on sale order lines created for by-products. "
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As rousseldenis already noted: since you now have a res.company field, the settings field should use related="company_id.byproduct_note_template" rather than manual _compute / _inverse methods. This is the standard OCA pattern for company-level settings and removes ~15 lines of code.

@rousseldenis
Copy link
Copy Markdown
Contributor

@bosd Could you improve your commit messages and squash some of them if needed. Thanks

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.

4 participants