[ADD] pos_payment_method_fee: add module - #1608
Conversation
|
@ajfebres, since you're proposing a new addon, please briefly make the case for it in the PR description. |
| fee_line = FeeLine.create( | ||
| { | ||
| "payment_id": payment.id, | ||
| "fee_rule_id": fee_rule.id, | ||
| "amount_base": payment.amount, | ||
| "fee_amount": fee_amount, | ||
| "session_id": session.id, | ||
| "state": "draft", | ||
| } | ||
| ) |
There was a problem hiding this comment.
For performance reasons, I think you could create the records in batch. First, iterate to build the payloads, and then create them all at once.
There was a problem hiding this comment.
I´m iterating directly over the active rules to create the fee one by one. What you're proposing isn't bad, but given the posting_policy == "immediate" condition, creating a recordset would require iterating again to filter based on that condition. So, it's better to do it just once.
You're proposing something like:
| fee_line = FeeLine.create( | |
| { | |
| "payment_id": payment.id, | |
| "fee_rule_id": fee_rule.id, | |
| "amount_base": payment.amount, | |
| "fee_amount": fee_amount, | |
| "session_id": session.id, | |
| "state": "draft", | |
| } | |
| ) | |
| lines = [] | |
| for fee_rule in payment_method.fee_ids.filtered(lambda f: f.active): | |
| fee_amount = fee_rule._compute_fee_amount(payment.amount) | |
| if float_is_zero(fee_amount, precision_digits=precision): | |
| continue | |
| lines.append( | |
| { | |
| "payment_id": payment.id, | |
| "fee_rule_id": fee_rule.id, | |
| "amount_base": payment.amount, | |
| "fee_amount": fee_amount, | |
| "session_id": session.id, | |
| "state": "draft", | |
| } | |
| ) | |
| fee_lines = FeeLine.create(lines) | |
| immediate_fee_lines = fee_lines.filtered(lambda line: line.fee_rule_id.posting_policy == "immediate") | |
| immediate_fee_lines._post_immediate() |
There was a problem hiding this comment.
I see. Why don't make _post_immediate also works over recordsets?
| "line_ids": [ | ||
| ( | ||
| 0, | ||
| 0, | ||
| { | ||
| "name": label, | ||
| "account_id": debit_account.id, | ||
| "debit": amount, | ||
| "credit": 0.0, | ||
| }, | ||
| ), | ||
| ( | ||
| 0, | ||
| 0, | ||
| { | ||
| "name": label, | ||
| "account_id": credit_account.id, | ||
| "debit": 0.0, | ||
| "credit": amount, | ||
| }, | ||
| ), | ||
| ], |
There was a problem hiding this comment.
you could use fields.Command instead of the old fashion triple tuplas for relational fields asignation.
| return { | ||
| "journal_id": journal.id, | ||
| "company_id": company.id, | ||
| "date": fields.Date.context_today(self), |
| # ------------------------------------------------------------------ | ||
| # Posting engine | ||
| # ------------------------------------------------------------------ | ||
| def _prepare_account_move_vals(self, amount, fee_rule, label): |
There was a problem hiding this comment.
| def _prepare_account_move_vals(self, amount, fee_rule, label): | |
| @api.model | |
| def _prepare_account_move_vals(self, amount, fee_rule, label): |
| ], | ||
| } | ||
|
|
||
| def _post_fee_amount(self, amount, fee_rule, label): |
There was a problem hiding this comment.
| def _post_fee_amount(self, amount, fee_rule, label): | |
| @api.model | |
| def _post_fee_amount(self, amount, fee_rule, label): |
| self.ensure_one() | ||
| precision = self.currency_id.decimal_places if self.currency_id else 2 | ||
| if float_is_zero(self.fee_amount, precision_digits=precision): | ||
| self.write({"state": "posted"}) |
There was a problem hiding this comment.
| self.write({"state": "posted"}) | |
| self.state = posted |
| move_line = move.line_ids.filtered( | ||
| lambda l: l.account_id == self.fee_rule_id.account_id | ||
| )[:1] | ||
| self.write({"account_move_line_id": move_line.id, "state": "posted"}) |
There was a problem hiding this comment.
on line 135 you just stablished state = "posted", can U make it in a sinlge place?
| def _post_grouped_fees(self): | ||
| """Group homogeneous fees (same fee rule) into a single journal | ||
| entry, as required for high transaction volume retail scenarios.""" | ||
| groups = {} | ||
| for line in self: | ||
| groups.setdefault(line.fee_rule_id.id, self.browse()) | ||
| groups[line.fee_rule_id.id] |= line | ||
| for _fee_rule_id, lines in groups.items(): | ||
| fee_rule = lines.fee_rule_id[:1] | ||
| currency = lines[:1].currency_id | ||
| precision = currency.decimal_places if currency else 2 | ||
| total = sum(lines.mapped("fee_amount")) | ||
| if float_is_zero(total, precision_digits=precision): | ||
| lines.write({"state": "posted"}) | ||
| continue | ||
| session = lines.session_id[:1] | ||
| label = _("POS Fees (grouped) - %(fee_rule)s - Session %(session)s") % { | ||
| "fee_rule": fee_rule.name, | ||
| "session": session.name, | ||
| } | ||
| move = lines._post_fee_amount(total, fee_rule, label) | ||
| move_line = move.line_ids.filtered( | ||
| lambda l: l.account_id == fee_rule.account_id | ||
| )[:1] | ||
| lines.write({"account_move_line_id": move_line.id, "state": "posted"}) | ||
|
|
||
| def _post_detailed_fees(self): | ||
| """Post one journal entry per fee line, preserving full per | ||
| transaction traceability (audit friendly).""" | ||
| for line in self: | ||
| line._post_immediate() |
There was a problem hiding this comment.
There is some common ground here. I think you could make it DRYer (Don't repeat yourself)
7720c91 to
35ffaa7
Compare
35ffaa7 to
276c70e
Compare
@BinhexTeam
🚀 Feature: POS Payment Method Fees
🎯 Purpose of this Module
This PR introduces a modular framework designed to manage and automate transactional fees for POS payment methods. It ensures full compatibility with Odoo 16 Community while aligning with OCA quality standards.
🛑 The Need (What problem does it solve?)
In retail and restaurant environments, operating with electronic payment terminals frequently generates financial costs, such as percentage commissions or fixed charges per transaction.
Standard Odoo 16 Community lacks a native mechanism to model these transactional fees, calculate multiple costs per operation, or generate specific accounting lines for them.
Because of this limitation, businesses suffer from a loss of financial visibility, distorted operating margins, and are often forced to resort to manual accounting adjustments or external parallel reconciliations.
✅ The Solution
This module solves the problem by decoupling the financial logic from the core POS, providing real operative visibility of transactional costs. Key features include:
⚙️ Flexible Fee Configuration: Allows assigning multiple fees per payment method, supporting both fixed amounts and percentage-based calculations.
🧾 Seamless Accounting Automation: Automatically generates the financial accounting lines for the costs without altering the original customer ticket, taxes, or total sales.
🔀 Advanced Posting Strategies: Offers different accounting strategies, such as "Immediate Posting" for maximum auditing granularity, or "Session Close Posting" (with grouped or detailed modes) to optimize performance and reduce accounting volume.
🔍 Full Traceability (Fee Ledger): Introduces a dedicated traceability layer (
pos.payment.fee.line) responsible for persisting the calculation, enabling deep auditing, and decoupling the accounting process.🛡️ Core Protection: Designed with an architecture that minimizes the impact on the Odoo core, mitigating risks with deferred posting and configurable grouping to ensure smooth POS closing performance.