feat: implement centralized vendor payment workflow for cross-company bank journal transactions
This commit is contained in:
parent
8f90de43fd
commit
e25289e66b
15
README.md
15
README.md
@ -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**
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
36
models/account_journal.py
Normal 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
81
models/account_payment.py
Normal 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
|
||||||
29
views/account_journal_views.xml
Normal file
29
views/account_journal_views.xml
Normal 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
2
wizard/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import account_payment_register
|
||||||
99
wizard/account_payment_register.py
Normal file
99
wizard/account_payment_register.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user