-
-
Notifications
You must be signed in to change notification settings - Fork 794
[ADD] product_abc_classification_finance: Classification by cost and sale price #2002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 16.0
Are you sure you want to change the base?
Conversation
Adding classification by Cost/Sale Price/Sale Margin
|
Hello @lmignon , @lmignon , @rousseldenis , |
lmignon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AJamal13 Thank you for your proposal. is all the code in github? There's nothing in the proposed code to fill in the new model that have been added.
Thank you for your response, @lmignon |
|
@AJamal13 Sorry, the most important file was collapsed by default into the diff. Nevertheless can you add unittest plz to ensure the quality of the new addon. |
Sure, will do |
Introduces demo data for finance-based ABC classification, including new XML records and manifest updates. Adds a comprehensive test suite for finance profile logic, validation, and history record creation. Also includes code cleanup, improved formatting, and minor view XML attribute adjustments for consistency.
Added tests to validate that finance profile levels must total 100%, ensure products with negative margin are excluded from ABC ranking for 'sale_margin' profiles, and verify that _get_finance_data_query returns correct SQL for all profile types.
Added tests to validate that finance profile levels must total 100%, ensure products with negative margin are excluded from ABC ranking for 'sale_margin' profiles, and verify that _get_finance_data_query returns correct SQL for all profile types. Add demo data and tests for finance ABC classification Introduces demo data for finance-based ABC classification, including new XML records and manifest updates. Adds a comprehensive test suite for finance profile logic, validation, and history record creation. Also includes code cleanup, improved formatting, and minor view XML attribute adjustments for consistency. fix checks
This reverts commit e1d7e1e.
|
@lmignon |
|
Hello ! Thank you ! |
@quentinDupont Thank you for your response and for the tip, |
| ), | ||
| ( | ||
| "sale_margin", | ||
| "Based on the Sale Margin of delivered sale order line by product", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AJamal13 AFAIK the sale_margin addon provides you the expected margin if all the products sold into the SO are delivered.... If you need to know the real margin based on the delivered qties for your SOL, you need to use the OCA addon sale_margin_delivered and the margin_delivered field provided by this addon. IMO, you should only keep the 2 profiles 'cost' and 'sale_price' in this addon without extra dependency on sale_margin and create a new addon with the extra dependency required in the case where the classification is based on margins.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lmignon sorry re-requested by mistake
You're welcome |
|
@lmignon Thank you for your response and advise. I excluded the margin from the module, and might add it later. |
| ], | ||
|
|
||
| "installable": True, | ||
| "application": False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AJamal13 you should add yourself as the official maintainers 😏
| finance_data = self._finance_init_collected_data_instance() | ||
| finance_data.product = ProductProduct.browse(product_id) | ||
| finance_data.purchase_price = float( | ||
| getattr(finance_data.product.product_tmpl_id, "standard_price", 0.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use getattr ?
| # Always set purchase_price (standard cost) from product.template | ||
| tmpl = finance_data.product.product_tmpl_id | ||
| finance_data.purchase_price = float( | ||
| getattr(tmpl, "standard_price", 0.0) or 0.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use getattr ?
| ProductClassification = self.env["abc.classification.product.level"] | ||
|
|
||
| for profile in to_compute: | ||
| # Get finance data per product (list of FinanceSaleData), plus total for percentage computation | ||
| finance_data_list, total_value = profile._finance_get_data() | ||
| existing_level_ids_to_remove = profile._get_existing_level_ids() | ||
| level_percentage = profile._build_ordered_level_cumulative_percentage() | ||
| if not level_percentage: | ||
| continue | ||
| level, percentage = level_percentage.pop(0) | ||
| previous_data = None | ||
| total_products = len(finance_data_list) | ||
| percentage_products = (100.0 / total_products) if total_products else 0.0 | ||
| # Pick the correct value field for this profile type | ||
| if profile.profile_type == "cost": | ||
| value_field = "total_cost" | ||
| elif profile.profile_type == "sale_price": | ||
| value_field = "total_sales" | ||
| else: | ||
| raise UserError( | ||
| _(f"Unknown finance profile_type: {profile.profile_type}") | ||
| ) | ||
|
|
||
| for i, finance_data in enumerate(finance_data_list): | ||
| finance_data.total_products = total_products | ||
| finance_data.percentage_products = percentage_products | ||
| finance_data.cumulated_percentage_products = ( | ||
| finance_data.percentage_products | ||
| if i == 0 | ||
| else ( | ||
| finance_data.percentage_products | ||
| + previous_data.cumulated_percentage_products | ||
| ) | ||
| ) | ||
| # Compute percentages and cumulative percentages for the products | ||
| value = getattr(finance_data, value_field, 0.0) or 0.0 | ||
| finance_data.percentage = ( | ||
| (100.0 * value / total_value) if total_value else 0.0 | ||
| ) | ||
| finance_data.cumulated_percentage = ( | ||
| finance_data.percentage | ||
| if i == 0 | ||
| else (finance_data.percentage + previous_data.cumulated_percentage) | ||
| ) | ||
| # Allow for floating point imprecision: round to 2 decimals and allow up to 101 | ||
| if float_round(finance_data.cumulated_percentage, 2) > 100.01: | ||
| raise UserError( | ||
| _( | ||
| "Cumulative percentage greater than 100 (actual: %.4f)." | ||
| % finance_data.cumulated_percentage | ||
| ) | ||
| ) | ||
| finance_data.sum_cumulated_percentages = ( | ||
| finance_data.cumulated_percentage | ||
| + finance_data.cumulated_percentage_products | ||
| ) | ||
| # Compute ABC classification for the products based on the | ||
| # sum of cumulated percentages | ||
| if ( | ||
| finance_data.sum_cumulated_percentages > percentage | ||
| and len(level_percentage) > 0 | ||
| ): | ||
| level, percentage = level_percentage.pop(0) | ||
| product = finance_data.product | ||
| levels = product.abc_classification_product_level_ids | ||
| product_abc_classification = levels.filtered( | ||
| lambda p, prof=profile: p.profile_id == prof | ||
| ) | ||
| finance_data.computed_level = level | ||
| if product_abc_classification: | ||
| existing_level_ids_to_remove.remove(product_abc_classification.id) | ||
| if product_abc_classification.level_id != level: | ||
| vals = profile._finance_data_to_vals(finance_data, create=False) | ||
| product_abc_classification.write(vals) | ||
| else: | ||
| vals = profile._finance_data_to_vals(finance_data, create=True) | ||
| product_abc_classification = ProductClassification.create(vals) | ||
| finance_data.product_level = product_abc_classification | ||
| previous_data = finance_data | ||
| if finance_data_list: | ||
| profile._finance_log_history(finance_data_list) | ||
| profile._purge_obsolete_level_values(existing_level_ids_to_remove) | ||
| return res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this code should be placed into a dedicated method to ease readability and modularity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the advice, but in this code i used the method from product_abc_classification_sale_stock as a reference while just adjusting some details, wouldn't it work just fine as it is ?
|
@lmignon thank you for your advices, |
|
Thank for all your work @AJamal13
But it's also a lot of work and if you don't see the point, I won't block for this too. Can you fix pre-commit plz? |
|
@lmignon thanks for the advice, Done. |
lmignon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for all your work. Even if I think we could still improve the quality and modularity you already invested a lot of effort to align your initial proposal with the expected OCA quality. The code LGTM, let's move forward 😏
jbaudoux
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to drop the commits where you merged the mainstream 16.0 branch in your branch. You should rebase on 16.0 instead.
And squash all your commits in a single commit
Thank you! |
Ok, will do. |
|
@AJamal13 Thanks for this. As already said, your commits should be cleaned to follow a little bit conventions (they should look like And you can read the whole page to help you contributing. Thanks Like this: |
| and not rec.warehouse_id | ||
| ): | ||
| raise ValidationError( | ||
| _("You must specify a warehouse for {profile_name}").format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use translation function arguments instead of format()
| value_field = "total_sales" | ||
| else: | ||
| raise UserError( | ||
| _(f"Unknown finance profile_type: {profile.profile_type}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
| profile._purge_obsolete_level_values(existing_level_ids_to_remove) | ||
| return res | ||
|
|
||
| def _finance_log_history(self, finance_data_list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this ??? And why passing through csv implementation as you bypass the ORM?
|
@rousseldenis thank you for your advices, will apply changes and request your review. |

This commit introduces a new module inheriting from product_abc_classification_sale_stock to include additional profile types for classifying products by cost or sale price,.