diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index f7c201e..20ad0af 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -175,6 +175,7 @@ def register(): purchase.AssayUnit, purchase.PayableRule, purchase.PenaltyRule, + purchase.PenaltyRuleTier, purchase.ConcentrateTerm, backtoback.Backtoback, dimension.AnalyticDimension, diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 6a315a9..83d77ee 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -778,23 +778,83 @@ class PayableRule(ModelSQL, ModelView): ('grade_minus', 'Grade minus deduction'), ('min_of_both', 'Min(% of grade, grade - deduction)'), ], "Method") - min_payable = fields.Numeric("Floor (min payable)") # ex: le "min -1" du Cu + min_payable = fields.Numeric("Floor (min payable)") + + def compute_payable_quantity(self, grade): + """ + Retourne la quantité payable dans l'unité du grade. + grade : Decimal (ex: Decimal('26.862')) + """ + grade = Decimal(str(grade)) + + if self.payable_method == 'percent': + result = grade * self.payable_percent / Decimal(100) + + elif self.payable_method == 'grade_minus': + result = grade - self.deduction_value + + elif self.payable_method == 'min_of_both': + by_percent = grade * self.payable_percent / Decimal(100) + by_deduction = grade - self.deduction_value + result = min(by_percent, by_deduction) + + if self.min_payable is not None: + result = max(result, self.min_payable) + + return result + +class PenaltyRuleTier(ModelSQL, ModelView): + "Penalty Rule Tier" + __name__ = 'penalty.rule.tier' + rule = fields.Many2One('penalty.rule', "Rule", ondelete='CASCADE') + threshold_from = fields.Numeric("From") + threshold_to = fields.Numeric("To") # None = pas de plafond + threshold_unit = fields.Many2One('assay.unit', "Unit") + deduction_per_unit = fields.Numeric("Deduction / unit") + penalty_value = fields.Numeric("Penalty Value (USD/DMT)") + + def compute_tier_penalty(self, grade, dry_weight_dmt): + """ + Retourne la pénalité USD pour ce palier uniquement. + grade : Decimal – teneur brute de l'élément + dry_weight_dmt: Decimal – poids sec en DMT + """ + grade = Decimal(str(grade)) + dry_weight_dmt = Decimal(str(dry_weight_dmt)) + + # Grade en dessous du seuil bas → ce palier ne s'applique pas + if grade <= self.threshold_from: + return Decimal(0) + + # Excès au-dessus du seuil bas, plafonné au seuil haut si existant + excess_top = grade if self.threshold_to is None else min(grade, self.threshold_to) + excess = excess_top - self.threshold_from + + # USD/DMT × DMT + return (excess * self.penalty_value * dry_weight_dmt).quantize(Decimal('0.01')) + class PenaltyRule(ModelSQL, ModelView): "Penalty Rule" __name__ = 'penalty.rule' - _rec_name = 'name' - name = fields.Char("Name") element = fields.Many2One('assay.element', "Element") + tiers = fields.One2Many('penalty.rule.tier', 'rule', "Tiers") - threshold = fields.Numeric("Treshold") - threshold_unit = fields.Many2One('assay.unit',"Unit") + def compute_penalty(self, grade, dry_weight_dmt): + """ + Retourne la pénalité totale USD en cumulant tous les paliers traversés. + grade : Decimal – teneur brute de l'élément + dry_weight_dmt: Decimal – poids sec en DMT + """ + grade = Decimal(str(grade)) + dry_weight_dmt = Decimal(str(dry_weight_dmt)) - step = fields.Numeric("Step") - penalty_value = fields.Numeric("Penalty Value") - currency = fields.Many2One('currency.currency',"Curr") - unit = fields.Many2One('product.uom',"Unit") + total = Decimal(0) + for tier in self.tiers: + total += tier.compute_tier_penalty(grade, dry_weight_dmt) + + return total.quantize(Decimal('0.01')) class ConcentrateTerm(ModelSQL, ModelView): "Concentrate Term" diff --git a/modules/purchase_trade/purchase.xml b/modules/purchase_trade/purchase.xml index 120f673..5668384 100755 --- a/modules/purchase_trade/purchase.xml +++ b/modules/purchase_trade/purchase.xml @@ -211,6 +211,22 @@ this repository contains the full copyright notices and license terms. --> payable_rule_form + + penalty.rule + form + penalty_rule_form + + + penalty.rule + tree + penalty_rule_tree + + + penalty.rule.tier + tree + penalty_rule_tier_tree + + +