Skip to content

[ADD] pos_payment_method_fee: add module - #1608

Draft
ajfebres wants to merge 1 commit into
OCA:16.0from
BinhexTeam:16.0-add-pos_payment_method_fee
Draft

[ADD] pos_payment_method_fee: add module#1608
ajfebres wants to merge 1 commit into
OCA:16.0from
BinhexTeam:16.0-add-pos_payment_method_fee

Conversation

@ajfebres

@ajfebres ajfebres commented Sep 8, 2026

Copy link
Copy Markdown

@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.

@OCA-git-bot OCA-git-bot added series:16.0 mod:pos_payment_method_fee Module pos_payment_method_fee labels Sep 8, 2026
@rrebollo

rrebollo commented Sep 8, 2026

Copy link
Copy Markdown

@ajfebres, since you're proposing a new addon, please briefly make the case for it in the PR description.

@rrebollo rrebollo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider my suggestions

Comment on lines +50 to +59
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",
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Why don't make _post_immediate also works over recordsets?

Comment on lines +97 to +118
"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,
},
),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why context_today?

# ------------------------------------------------------------------
# Posting engine
# ------------------------------------------------------------------
def _prepare_account_move_vals(self, amount, fee_rule, label):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on line 135 you just stablished state = "posted", can U make it in a sinlge place?

Comment on lines +158 to +188
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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some common ground here. I think you could make it DRYer (Don't repeat yourself)

@ajfebres
ajfebres force-pushed the 16.0-add-pos_payment_method_fee branch 4 times, most recently from 7720c91 to 35ffaa7 Compare September 9, 2026 09:38
@ajfebres
ajfebres force-pushed the 16.0-add-pos_payment_method_fee branch from 35ffaa7 to 276c70e Compare September 9, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:pos_payment_method_fee Module pos_payment_method_fee series:16.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants