diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index 2d4749b..8384a52 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -137,9 +137,10 @@ def register():
open_position.OpenPosition,
open_position.OpenPositionReport,
open_position.OpenPositionContext,
- optional.OptionalScenario,
- fee.Fee,
- fee.FeeLots,
+ optional.OptionalScenario,
+ fee.Fee,
+ fee.FeeRule,
+ fee.FeeLots,
purchase.FeeLots,
valuation.Valuation,
valuation.ValuationLine,
diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py
index 0e30740..c2fa3fc 100755
--- a/modules/purchase_trade/fee.py
+++ b/modules/purchase_trade/fee.py
@@ -35,7 +35,9 @@ class Fee(ModelSQL,ModelView):
"Fee"
__name__ = 'fee.fee'
- line = fields.Many2One('purchase.line',"Line")
+ line = fields.Many2One('purchase.line',"Line")
+ source_rule = fields.Many2One('fee.rule', "Source Rule", readonly=True)
+ generated_by_rule = fields.Boolean("Generated by Rule", readonly=True)
shipment_in = fields.Many2One('stock.shipment.in')
shipment_out = fields.Many2One('stock.shipment.out')
shipment_internal = fields.Many2One('stock.shipment.internal')
@@ -99,9 +101,13 @@ class Fee(ModelSQL,ModelView):
fee_date = fields.Date("Date")
@classmethod
- def default_fee_date(cls):
- Date = Pool().get('ir.date')
- return Date.today()
+ def default_fee_date(cls):
+ Date = Pool().get('ir.date')
+ return Date.today()
+
+ @classmethod
+ def default_generated_by_rule(cls):
+ return False
@classmethod
def default_qt_state(cls):
@@ -971,7 +977,177 @@ class Fee(ModelSQL,ModelView):
description=description,
lines=[move_line,move_line_],
)
-
+
+class FeeRule(ModelSQL, ModelView):
+ "Fee Rule"
+ __name__ = 'fee.rule'
+
+ name = fields.Char("Name", required=True)
+ active = fields.Boolean("Active")
+ sequence = fields.Integer("Sequence")
+ apply_on = fields.Selection([
+ ('purchase_line', "Purchase Line"),
+ ('sale_line', "Sale Line"),
+ ], "Apply On", required=True)
+ auto_apply = fields.Boolean("Auto Apply")
+
+ party = fields.Many2One('party.party', "Counterparty")
+ line_product = fields.Many2One('product.product', "Line Product")
+ incoterm = fields.Many2One('incoterm.incoterm', "Incoterm")
+ from_location = fields.Many2One('stock.location', "From Location")
+ to_location = fields.Many2One('stock.location', "To Location")
+ execution_plan = fields.Many2One('workflow.plan', "Execution Plan")
+
+ fee_product = fields.Many2One(
+ 'product.product', "Fee Product", required=True,
+ domain=[('type', '=', 'service')])
+ supplier = fields.Many2One('party.party', "Supplier")
+ p_r = fields.Selection([
+ ('pay', 'PAY'),
+ ('rec', 'REC'),
+ ], "P/R", required=True)
+ mode = fields.Selection([
+ ('lumpsum', 'Lump sum'),
+ ('perqt', 'Per qt'),
+ ('pprice', '% price'),
+ ('rate', '% rate'),
+ ('pcost', '% cost price'),
+ ('ppack', 'Per packing'),
+ ], "Mode", required=True)
+ price = fields.Numeric("Price", digits=(16, 4))
+ quantity = fields.Numeric("Qt", digits='unit')
+ currency = fields.Many2One('currency.currency', "Currency")
+ unit = fields.Many2One('product.uom', "Unit")
+ qt_state = fields.Many2One('lot.qt.type', "Qt State")
+ weight_type = fields.Selection([
+ ('net', 'Net'),
+ ('brut', 'Gross'),
+ ], "W. type")
+
+ @classmethod
+ def default_active(cls):
+ return True
+
+ @classmethod
+ def default_sequence(cls):
+ return 10
+
+ @classmethod
+ def default_auto_apply(cls):
+ return False
+
+ @classmethod
+ def default_p_r(cls):
+ return 'pay'
+
+ @classmethod
+ def default_mode(cls):
+ return 'lumpsum'
+
+ @classmethod
+ def default_weight_type(cls):
+ return 'brut'
+
+ @staticmethod
+ def _same_record(left, right):
+ return bool(
+ left and right
+ and getattr(left, 'id', left) == getattr(right, 'id', right))
+
+ @classmethod
+ def _rules_for_lines(cls, apply_on, auto_only=False):
+ domain = [
+ ('active', '=', True),
+ ('apply_on', '=', apply_on),
+ ]
+ if auto_only:
+ domain.append(('auto_apply', '=', True))
+ return cls.search(domain, order=[('sequence', 'ASC'), ('id', 'ASC')])
+
+ def _matches_line(self, line, apply_on):
+ contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
+ if not contract or self.apply_on != apply_on:
+ return False
+ checks = [
+ (self.party, getattr(contract, 'party', None)),
+ (self.line_product, getattr(line, 'product', None)),
+ (self.incoterm, getattr(contract, 'incoterm', None)),
+ (self.from_location, getattr(contract, 'from_location', None)),
+ (self.to_location, getattr(contract, 'to_location', None)),
+ ]
+ if any(rule_value and not self._same_record(rule_value, actual)
+ for rule_value, actual in checks):
+ return False
+ if self.execution_plan and not self._same_record(
+ self.execution_plan, getattr(contract, 'plan', None)):
+ return False
+ return True
+
+ def _existing_fee(self, line, apply_on):
+ Fee = Pool().get('fee.fee')
+ field_name = 'line' if apply_on == 'purchase_line' else 'sale_line'
+ existing = Fee.search([
+ ('source_rule', '=', self.id),
+ (field_name, '=', line.id),
+ ], limit=1)
+ return existing[0] if existing else None
+
+ def _fee_values(self, line, apply_on):
+ contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
+ values = {
+ 'source_rule': self.id,
+ 'generated_by_rule': True,
+ 'type': 'budgeted',
+ 'p_r': self.p_r,
+ 'product': self.fee_product.id,
+ 'supplier': (
+ getattr(self.supplier, 'id', None)
+ or getattr(getattr(contract, 'party', None), 'id', None)),
+ 'mode': self.mode,
+ 'price': self.price if self.price is not None else Decimal(0),
+ 'currency': (
+ getattr(self.currency, 'id', None)
+ or getattr(getattr(contract, 'currency', None), 'id', None)),
+ 'unit': (
+ getattr(self.unit, 'id', None)
+ or getattr(getattr(line, 'unit', None), 'id', None)),
+ 'quantity': (
+ self.quantity if self.quantity is not None
+ else getattr(line, 'quantity_theorical', None)
+ or getattr(line, 'quantity', None)),
+ 'qt_state': getattr(self.qt_state, 'id', None),
+ 'weight_type': self.weight_type,
+ }
+ values['line' if apply_on == 'purchase_line' else 'sale_line'] = line.id
+ return values
+
+ @classmethod
+ def apply_to_lines(cls, lines, apply_on, auto_only=False):
+ Fee = Pool().get('fee.fee')
+ rules = cls._rules_for_lines(apply_on, auto_only=auto_only)
+ created = []
+ for line in lines:
+ if not getattr(line, 'id', None):
+ continue
+ for rule in rules:
+ if not rule._matches_line(line, apply_on):
+ continue
+ if rule._existing_fee(line, apply_on):
+ continue
+ values = rule._fee_values(line, apply_on)
+ if not (
+ values.get('supplier') and values.get('currency')
+ and values.get('unit')):
+ logger.info(
+ "FEE_RULE_SKIPPED_MISSING_VALUES rule=%s line=%s",
+ rule.id, line.id)
+ continue
+ with Transaction().set_context(
+ _purchase_trade_skip_fee_rules=True):
+ created.extend(Fee.create([values]))
+ return created
+
+
class FeeLots(ModelSQL,ModelView):
"Fee lots"
diff --git a/modules/purchase_trade/fee.xml b/modules/purchase_trade/fee.xml
index eeeba06..1d6ed50 100755
--- a/modules/purchase_trade/fee.xml
+++ b/modules/purchase_trade/fee.xml
@@ -19,11 +19,36 @@ this repository contains the full copyright notices and license terms. -->