feat: implement centralized vendor payment workflow for cross-company bank journal transactions

This commit is contained in:
Suherdy Yacob 2026-05-05 15:23:37 +07:00
parent 8f90de43fd
commit e25289e66b
9 changed files with 267 additions and 2 deletions

View File

@ -21,12 +21,27 @@ When a branch-side POS session is closed, the module automates the inter-company
- Debits the parent bank's **Outstanding Receipt Account**, allowing for seamless reconciliation against incoming bank statement lines in the parent's accounting dashboard. - Debits the parent bank's **Outstanding Receipt Account**, allowing for seamless reconciliation against incoming bank statement lines in the parent's accounting dashboard.
- Credits the Inter-company Liability account (e.g., `229101 Hubungan RK`). - Credits the Inter-company Liability account (e.g., `229101 Hubungan RK`).
### 3. Centralized Vendor Payment
Enables branches to pay vendor bills from a bank account managed by a parent company:
- **Branch-Side**: Intercepts the "Register Payment" wizard. Instead of creating a standard payment, it generates a clearing entry that moves the liability from the Vendor (Accounts Payable) to the Parent Company (Inter-company Account).
- **Parent-Side**: Automatically creates an actual `account.payment` record in the parent company, paying out of the parent bank journal and debiting the inter-company clearing account.
- **Workflow**: Vendor bill remains in the branch, but is marked as **Paid** via the clearing mechanism. The actual cash outflow and bank reconciliation happen in the parent.
## Configuration ## Configuration
### 1. POS Inter-Company Clearing
To enable the automated inter-company clearing, navigate to **Point of Sale > Configuration > Payment Methods** and configure the following in the "Inter-Company Clearing" section: To enable the automated inter-company clearing, navigate to **Point of Sale > Configuration > Payment Methods** and configure the following in the "Inter-Company Clearing" section:
- **Parent Company**: Select the target company (e.g., OT). - **Parent Company**: Select the target company (e.g., OT).
- **Parent Bank Journal**: Select the journal in the parent company that receives the funds. - **Parent Bank Journal**: Select the journal in the parent company that receives the funds.
- **Parent Inter-company Account**: The liability account in the parent company (e.g., `229101 Hubungan RK`). - **Parent Inter-company Account**: The liability account in the parent company (e.g., `229101 Hubungan RK`).
- **Parent Clearing Journal**: The journal in the parent company used to record these mirror entries (e.g., "Inter-Company Clearing"). - **Parent Clearing Journal**: The journal in the parent company used to record these mirror entries (e.g., "Inter-Company Clearing").
### 2. Centralized Vendor Payment
Configure a **Bank/Cash Journal** in the branch company to act as a bridge:
1. Open the journal form (**Accounting > Configuration > Journals**).
2. Go to the **Centralized Payment** tab.
3. Enable **Is Centralized**.
4. Set the **Parent Company**, **Parent Journal**, and both **Inter-Company (RK) Accounts**.
5. When registering a payment on a vendor bill, select this journal to trigger the cross-company flow.
## Author ## Author
- **Suherdy Yacob** - **Suherdy Yacob**

View File

@ -1,4 +1,5 @@
from . import models from . import models
from . import wizard
from odoo import api, SUPERUSER_ID from odoo import api, SUPERUSER_ID
def _auto_share_accounts_post_init(env): def _auto_share_accounts_post_init(env):

View File

@ -8,9 +8,10 @@
This module removes the standard restriction that prevents Chart of Accounts (COA) of type 'Bank and Cash' from being shared across multiple companies. This module removes the standard restriction that prevents Chart of Accounts (COA) of type 'Bank and Cash' from being shared across multiple companies.
""", """,
'author': 'Suherdy Yacob', 'author': 'Suherdy Yacob',
'depends': ['account', 'point_of_sale'], 'depends': ['point_of_sale', 'account'],
'data': [ 'data': [
'views/pos_payment_method_views.xml', 'views/pos_payment_method_views.xml',
'views/account_journal_views.xml',
], ],
'installable': True, 'installable': True,
'application': False, 'application': False,

View File

@ -1,4 +1,5 @@
from . import account_account from . import account_account
from . import account_journal
from . import account_payment
from . import pos_payment_method from . import pos_payment_method
from . import pos_session from . import pos_session

36
models/account_journal.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class AccountJournal(models.Model):
_inherit = 'account.journal'
is_centralized = fields.Boolean(
string='Is Centralized Payment',
help="If checked, payments made using this journal will be recorded in the parent company."
)
parent_company_id = fields.Many2one(
'res.company',
string='Parent Company',
help="The parent company where the actual bank payment will be recorded."
)
parent_journal_id = fields.Many2one(
'account.journal',
string='Parent Bank Journal',
domain="[('company_id', '=', parent_company_id), ('type', 'in', ('bank', 'cash'))]",
check_company=False,
help="The actual bank journal in the parent company."
)
parent_intercompany_account_id = fields.Many2one(
'account.account',
string='Parent Inter-company Account',
domain="[('company_ids', 'in', parent_company_id)]",
check_company=False,
help="The Hubungan RK account in the parent company to debit (Receivable from branch)."
)
branch_intercompany_account_id = fields.Many2one(
'account.account',
string='Branch Inter-company Account',
domain="[('company_ids', 'in', company_id)]",
help="The Hubungan RK account in the branch company to credit (Liability to parent)."
)

81
models/account_payment.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from odoo import models, api, fields, _
from odoo.exceptions import UserError
class AccountPayment(models.Model):
_inherit = 'account.payment'
def action_post(self):
# 1. Capture info before super() because we might need to create extra moves
centralized_payments = self.filtered(lambda p: p.journal_id.is_centralized and p.company_id != p.journal_id.parent_company_id)
# 2. Call super() to post the original payment(s)
# We need to make sure the original payment uses the RK account instead of Bank account if it's centralized
for payment in centralized_payments:
if not payment.journal_id.branch_intercompany_account_id:
raise UserError(_("Please configure the Branch Inter-company Account on journal %s", payment.journal_id.name))
# We override the journal's account temporarily for the liquidity line
res = super().action_post()
# 3. Handle Centralized entries in Parent
for payment in centralized_payments:
parent_company = payment.journal_id.parent_company_id
parent_journal = payment.journal_id.parent_journal_id
parent_rk_account = payment.journal_id.parent_intercompany_account_id
print(f">>> DEBUG: Centralized Payment for {payment.name}")
print(f" Company: {payment.company_id.id}, Parent Co: {parent_company.id}")
print(f" Parent Journal: {parent_journal.name} (ID: {parent_journal.id})")
print(f" Parent RK: {parent_rk_account.code} (ID: {parent_rk_account.id})")
if not parent_journal or not parent_rk_account:
raise UserError(_("Please configure the Parent Journal and RK Account on journal %s", payment.journal_id.name))
# Create the mirroring entry in Parent
# Debit: Parent RK (Receivable from Branch)
# Credit: Bank
parent_move = self.env['account.move'].with_company(parent_company).create({
'move_type': 'entry',
'date': payment.date,
'company_id': parent_company.id,
'journal_id': parent_journal.id,
'ref': f"Centralized Payment: {payment.name} ({payment.company_id.name})",
'line_ids': [
(0, 0, {
'name': payment.memo or f"Centralized Payment: {payment.name}",
'account_id': parent_rk_account.id,
'debit': payment.amount,
'partner_id': payment.partner_id.id,
'company_id': parent_company.id,
'display_type': False,
}),
(0, 0, {
'name': payment.memo or f"Centralized Payment: {payment.name}",
'account_id': parent_journal.default_account_id.id,
'credit': payment.amount,
'partner_id': payment.partner_id.id,
'company_id': parent_company.id,
'display_type': False,
}),
]
})
parent_move.action_post()
print(f" Parent Move Posted: {parent_move.name}")
return res
def _prepare_move_line_default_vals(self, write_off_line_vals=None, force_balance=None):
"""
Override to use the Branch RK account for the liquidity line
when the payment is centralized.
"""
res = super()._prepare_move_line_default_vals(write_off_line_vals, force_balance)
if self.journal_id.is_centralized and self.company_id != self.journal_id.parent_company_id:
# The first line is usually the liquidity line in outbound payments
# or the counterpart line. Odoo 17+ uses _seek_for_lines to identify them.
# But here we can just update the account if it matches the journal's default.
for line_vals in res:
if line_vals.get('account_id') == self.journal_id.default_account_id.id:
line_vals['account_id'] = self.journal_id.branch_intercompany_account_id.id
return res

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_account_journal_form_inherit_centralized" model="ir.ui.view">
<field name="name">account.journal.form.inherit.centralized</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='bank_account']" position="after">
<page name="centralized_payment" string="Centralized Payment" invisible="type not in ('bank', 'cash')">
<group>
<group string="Configuration">
<field name="is_centralized"/>
<field name="parent_company_id" invisible="not is_centralized" required="is_centralized"/>
<field name="parent_journal_id" invisible="not is_centralized" required="is_centralized"/>
</group>
<group string="Inter-Company Accounts" invisible="not is_centralized">
<field name="branch_intercompany_account_id" required="is_centralized"/>
<field name="parent_intercompany_account_id" required="is_centralized"/>
</group>
</group>
<p class="text-muted" invisible="not is_centralized">
When this journal is used to pay bills in the current company, the actual bank payment will be recorded in the parent company,
and an inter-company clearing entry will be created in the current company.
</p>
</page>
</xpath>
</field>
</record>
</odoo>

2
wizard/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import account_payment_register

View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AccountPaymentRegister(models.TransientModel):
_inherit = 'account.payment.register'
def _create_payments(self):
# Intercept centralized payments
if self.journal_id.is_centralized:
return self._create_centralized_payments()
return super(AccountPaymentRegister, self)._create_payments()
def _create_centralized_payments(self):
"""
Custom logic to create inter-company clearing moves when paying via a centralized journal.
"""
journal = self.journal_id
if not journal.parent_company_id or not journal.parent_journal_id:
raise UserError(_("The selected journal is marked as centralized but is missing parent company/journal configuration."))
if not journal.branch_intercompany_account_id or not journal.parent_intercompany_account_id:
raise UserError(_("Inter-company (RK) accounts must be configured on the centralized journal."))
branch_company = self.company_id
parent_company = journal.parent_company_id
payments = self.env['account.payment']
# We process batches. In centralized mode, we usually expect 1 batch if coming from a single bill.
# But we handle multiple if needed.
for batch_result in self.batches:
lines = batch_result['lines']
amount = abs(sum(lines.mapped('amount_residual')))
if self.currency_id != branch_company.currency_id:
amount = abs(sum(lines.mapped('amount_residual_currency')))
# 1. Create Clearing Move in Branch Company
# Debit: Payable Account (clears the vendor bill)
# Credit: Inter-company (RK) Account (Liability to Parent)
clearing_move_vals = {
'move_type': 'entry',
'company_id': branch_company.id,
'journal_id': journal.id,
'date': self.payment_date,
'ref': _("Centralized Payment for %s") % (", ".join(lines.move_id.mapped('name'))),
'line_ids': [
(0, 0, {
'name': _("Clearing: %s") % (", ".join(lines.move_id.mapped('name'))),
'partner_id': self.partner_id.id,
'account_id': lines[0].account_id.id, # The payable account
'debit': amount if self.payment_type == 'outbound' else 0.0,
'credit': amount if self.payment_type == 'inbound' else 0.0,
'currency_id': self.currency_id.id,
'amount_currency': amount if self.payment_type == 'outbound' else -amount,
}),
(0, 0, {
'name': _("Due to Parent (%s)") % parent_company.name,
'partner_id': False,
'account_id': journal.branch_intercompany_account_id.id,
'debit': amount if self.payment_type == 'inbound' else 0.0,
'credit': amount if self.payment_type == 'outbound' else 0.0,
'currency_id': self.currency_id.id,
'amount_currency': -amount if self.payment_type == 'outbound' else amount,
}),
],
}
branch_move = self.env['account.move'].create(clearing_move_vals)
branch_move.action_post()
# Reconcile Branch Move with the Bill Lines
clearing_lines = branch_move.line_ids.filtered(lambda l: l.account_id == lines[0].account_id)
(lines + clearing_lines).reconcile()
# 2. Create actual Payment in Parent Company
# We use account.payment to ensure it shows up in bank reconciliation in the parent
payment_vals = {
'company_id': parent_company.id,
'journal_id': journal.parent_journal_id.id,
'payment_type': self.payment_type,
'partner_type': self.partner_type,
'partner_id': self.partner_id.id,
'amount': amount,
'currency_id': self.currency_id.id,
'date': self.payment_date,
'memo': _("Centralized Pay for %s (%s)") % (branch_company.name, ", ".join(lines.move_id.mapped('name'))),
'destination_account_id': journal.parent_intercompany_account_id.id,
}
# Create payment in parent company context
parent_payment = self.env['account.payment'].with_company(parent_company).sudo().create(payment_vals)
parent_payment.action_post()
payments |= parent_payment
return payments