crea mandatos SEPA desde pedidos del TPV
This commit is contained in:
parent
a754df3914
commit
62e0e8a92c
7 changed files with 262 additions and 15 deletions
33
pos_account_payment_order_sepa/README.md
Normal file
33
pos_account_payment_order_sepa/README.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# POS Account Payment Order SEPA
|
||||
|
||||
Adds a contextual action **"Collect by SEPA direct debit"** on the POS orders
|
||||
list/form (Point of Sale → Orders).
|
||||
|
||||
## Purpose
|
||||
|
||||
When a POS order is paid with a *"Customer Account"* payment method
|
||||
(`split_transactions = True`), Odoo does not register a real collection: it
|
||||
leaves the amount as an **open receivable** for the customer inside the
|
||||
session's journal entry. This module lets you collect those receivables in
|
||||
bulk through a **SEPA direct debit payment order** (debit order), without
|
||||
re-invoicing the already-issued (simplified) tickets.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Make sure the affected customers have a **valid SEPA mandate** (see
|
||||
`account_banking_mandate_batch`).
|
||||
2. Go to **Point of Sale → Orders**, filter the orders paid on account, select
|
||||
them and run **Action → Collect by SEPA direct debit**.
|
||||
3. The action creates the **payment lines** inside a draft inbound SEPA
|
||||
payment order, grouped by payment mode. Orders without an open receivable,
|
||||
without a partner, already queued, or whose partner has no valid mandate are
|
||||
**skipped and reported**.
|
||||
4. Open **Invoicing → Customers → Payment Orders**, confirm it and **generate
|
||||
the SEPA file (pain.008)**.
|
||||
|
||||
## Payment mode resolution
|
||||
|
||||
The inbound SEPA payment mode is taken from the partner's
|
||||
`customer_payment_mode_id` when it is a valid `sepa_direct_debit` inbound mode;
|
||||
otherwise the first inbound `sepa_direct_debit` payment mode is used as
|
||||
fallback.
|
||||
1
pos_account_payment_order_sepa/__init__.py
Normal file
1
pos_account_payment_order_sepa/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
17
pos_account_payment_order_sepa/__manifest__.py
Normal file
17
pos_account_payment_order_sepa/__manifest__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "POS Account Payment Order SEPA",
|
||||
"summary": "Contextual action on POS orders to collect 'Customer Account' "
|
||||
"receivables through a SEPA direct debit payment order.",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "AGPL-3",
|
||||
"author": "Custom",
|
||||
"depends": [
|
||||
"account_banking_sepa_direct_debit",
|
||||
"point_of_sale",
|
||||
],
|
||||
"data": [
|
||||
"data/server_action.xml",
|
||||
],
|
||||
}
|
||||
133
pos_account_payment_order_sepa/data/server_action.xml
Normal file
133
pos_account_payment_order_sepa/data/server_action.xml
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="action_pos_collect_sepa" model="ir.actions.server">
|
||||
<field name="name">Collect by SEPA direct debit</field>
|
||||
<field name="model_id" ref="point_of_sale.model_pos_order" />
|
||||
<field name="binding_model_id" ref="point_of_sale.model_pos_order" />
|
||||
<field name="binding_view_types">list,form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code"><![CDATA[
|
||||
# Collect the still-open "Customer Account" receivables generated by the
|
||||
# selected POS orders through a SEPA direct debit payment order (debit
|
||||
# order). For each order we take the receivable journal item of its session
|
||||
# move, keep the unreconciled ones whose partner has a valid SEPA mandate,
|
||||
# and create the payment lines inside a single draft inbound payment order.
|
||||
# The SEPA XML (pain.008) is generated afterwards from that order by the user.
|
||||
#
|
||||
# Orders are skipped (and reported) when: no accounting move yet, no open
|
||||
# receivable line, no partner, the line is already in a payment order, or the
|
||||
# partner has no valid SEPA mandate. Refunds/credit lines are ignored.
|
||||
|
||||
Mode = env["account.payment.mode"]
|
||||
Order = env["account.payment.order"]
|
||||
Mandate = env["account.banking.mandate"]
|
||||
|
||||
# --- Resolve the inbound SEPA payment mode (partner mode, with fallback) ---
|
||||
def sepa_mode_for(partner):
|
||||
mode = partner.customer_payment_mode_id
|
||||
if (
|
||||
mode
|
||||
and mode.payment_order_ok
|
||||
and mode.payment_method_id.code == "sepa_direct_debit"
|
||||
and mode.payment_method_id.payment_type == "inbound"
|
||||
):
|
||||
return mode
|
||||
return False
|
||||
|
||||
fallback_mode = Mode.search(
|
||||
[
|
||||
("payment_order_ok", "=", True),
|
||||
("payment_method_id.code", "=", "sepa_direct_debit"),
|
||||
("payment_method_id.payment_type", "=", "inbound"),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
# --- Classify the selected orders ---
|
||||
lines_by_mode = {}
|
||||
skipped = {"no_move": 0, "no_line": 0, "no_partner": 0, "already": 0, "no_mandate": 0, "no_mode": 0}
|
||||
|
||||
for order in records:
|
||||
move = order.account_move
|
||||
if not move:
|
||||
skipped["no_move"] += 1
|
||||
continue
|
||||
rec_lines = move.line_ids.filtered(
|
||||
lambda l: l.account_id.account_type == "asset_receivable"
|
||||
and not l.reconciled
|
||||
and l.amount_residual > 0
|
||||
)
|
||||
if not rec_lines:
|
||||
skipped["no_line"] += 1
|
||||
continue
|
||||
for line in rec_lines:
|
||||
partner = line.partner_id
|
||||
if not partner:
|
||||
skipped["no_partner"] += 1
|
||||
continue
|
||||
if env["account.payment.line"].search_count(
|
||||
[("move_line_id", "=", line.id), ("order_id.state", "!=", "cancel")]
|
||||
):
|
||||
skipped["already"] += 1
|
||||
continue
|
||||
if not Mandate.search_count(
|
||||
[("partner_id", "=", partner.commercial_partner_id.id), ("state", "=", "valid")]
|
||||
):
|
||||
skipped["no_mandate"] += 1
|
||||
continue
|
||||
mode = sepa_mode_for(partner) or fallback_mode
|
||||
if not mode:
|
||||
skipped["no_mode"] += 1
|
||||
continue
|
||||
lines_by_mode.setdefault(mode.id, env["account.move.line"])
|
||||
lines_by_mode[mode.id] |= line
|
||||
|
||||
# --- Create one draft payment order per payment mode and add the lines ---
|
||||
created_lines = 0
|
||||
order_names = []
|
||||
for mode_id, move_lines in lines_by_mode.items():
|
||||
mode = Mode.browse(mode_id)
|
||||
payment_order = Order.search(
|
||||
[("payment_mode_id", "=", mode_id), ("state", "=", "draft")], limit=1
|
||||
)
|
||||
if not payment_order:
|
||||
payment_order = Order.create({"payment_mode_id": mode_id})
|
||||
move_lines.create_payment_line_from_move_line(payment_order)
|
||||
created_lines += len(move_lines)
|
||||
order_names.append(payment_order.name or "Draft")
|
||||
|
||||
# --- Report ---
|
||||
message = (
|
||||
"Payment lines created: %(created)s\n"
|
||||
"Payment order(s): %(orders)s\n\n"
|
||||
"Skipped:\n"
|
||||
" no accounting move: %(no_move)s\n"
|
||||
" no open receivable line: %(no_line)s\n"
|
||||
" no partner: %(no_partner)s\n"
|
||||
" already in a payment order: %(already)s\n"
|
||||
" no valid SEPA mandate: %(no_mandate)s\n"
|
||||
" no SEPA payment mode: %(no_mode)s"
|
||||
) % {
|
||||
"created": created_lines,
|
||||
"orders": ", ".join(order_names) or "-",
|
||||
"no_move": skipped["no_move"],
|
||||
"no_line": skipped["no_line"],
|
||||
"no_partner": skipped["no_partner"],
|
||||
"already": skipped["already"],
|
||||
"no_mandate": skipped["no_mandate"],
|
||||
"no_mode": skipped["no_mode"],
|
||||
}
|
||||
|
||||
action = {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": "SEPA collection",
|
||||
"message": message,
|
||||
"type": "success" if created_lines else "warning",
|
||||
"sticky": True,
|
||||
},
|
||||
}
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue