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
+
+