refactored to 17.0
This commit is contained in:
commit
aaade5784b
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Account Shared Bank Cash
|
||||
|
||||
## Overview
|
||||
|
||||
This module mirrors the parent company's Bank and Cash accounts to branch companies and provides:
|
||||
|
||||
1. **Mirrored Bank/Cash Accounts**: Automatically clones parent company bank/cash accounts and journals to branches on install
|
||||
2. **POS Inter-Company Clearing**: When a POS session closes, creates automated clearing entries between the branch and parent company
|
||||
3. **Centralized Vendor Payment**: Branch vendor bills can be paid via parent company's bank journal with inter-company clearing
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. POS Inter-Company Clearing
|
||||
|
||||
On each `POS Payment Method`:
|
||||
- **Inter-Company Clearing Account**: The RK account in the branch company
|
||||
- **Clearing Journal**: A miscellaneous journal for the clearing entry
|
||||
- **Parent Company**: The parent company for the mirror entry
|
||||
- **Parent Bank Journal**: The parent's bank journal (outstanding receipt account will be debited)
|
||||
- **Parent Hubungan RK Account**: The parent's inter-company liability account
|
||||
- **Parent Clearing Journal**: A miscellaneous journal in the parent for the mirror entry
|
||||
|
||||
### 2. Centralized Vendor Payment
|
||||
|
||||
On each `Account Journal` (Bank/Cash):
|
||||
- **Is Centralized Payment**: Enable centralized mode
|
||||
- **Parent Company**: The parent company
|
||||
- **Parent Bank Journal**: The parent's actual bank journal
|
||||
- **Parent Inter-Company Account**: The RK account in the parent (Receivable from branch)
|
||||
- **Branch Inter-Company Account**: The RK account in the branch (Liability to parent)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- **Odoo 17 Compatibility**: Uses mirror account strategy since Odoo 17 `account.account` uses single `company_id`
|
||||
- **Post-init hook**: Automatically clones parent bank/cash accounts and journals to branches
|
||||
- **Uninstall hook**: Removes mirrored accounts that have no journal items
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `point_of_sale`
|
||||
- `account`
|
||||
132
__init__.py
Normal file
132
__init__.py
Normal file
@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import wizard
|
||||
from odoo import api, SUPERUSER_ID
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _auto_share_accounts_post_init(cr, registry):
|
||||
"""
|
||||
Automatically mirror Bank & Cash accounts from the Parent Company (ID 2)
|
||||
to branch companies that don't have their own Chart of Accounts.
|
||||
|
||||
Also auto-creates Bank journals in each branch for the mirrored accounts.
|
||||
|
||||
Odoo 17 Strategy: Since account.account uses a single company_id (M2O),
|
||||
we CREATE new account records in each branch company with the same code/name
|
||||
as the parent's bank/cash accounts (instead of sharing via M2M in Odoo 19).
|
||||
"""
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
parent_company = env['res.company'].browse(2)
|
||||
if not parent_company.exists():
|
||||
_logger.warning("Parent company (ID=2) not found, skipping auto-share.")
|
||||
return
|
||||
|
||||
branch_companies = env['res.company'].search([('id', '!=', 2)])
|
||||
|
||||
# Find parent's bank/cash accounts
|
||||
parent_bank_cash_accounts = env['account.account'].search([
|
||||
('company_id', '=', parent_company.id),
|
||||
('account_type', '=', 'asset_cash'),
|
||||
])
|
||||
|
||||
if not parent_bank_cash_accounts:
|
||||
_logger.info("No bank/cash accounts found in parent company, skipping.")
|
||||
return
|
||||
|
||||
for branch in branch_companies:
|
||||
# Check if branch already has its own accounts
|
||||
existing_branch_accounts = env['account.account'].search([
|
||||
('company_id', '=', branch.id),
|
||||
('account_type', '=', 'asset_cash'),
|
||||
])
|
||||
|
||||
if existing_branch_accounts:
|
||||
_logger.info("Branch %s already has bank/cash accounts, skipping.", branch.name)
|
||||
continue
|
||||
|
||||
# Clone parent's bank/cash accounts to this branch
|
||||
for parent_acc in parent_bank_cash_accounts:
|
||||
# Check if an account with the same code already exists in this branch
|
||||
existing = env['account.account'].search([
|
||||
('company_id', '=', branch.id),
|
||||
('code', '=', parent_acc.code),
|
||||
], limit=1)
|
||||
|
||||
if not existing:
|
||||
env['account.account'].create({
|
||||
'name': parent_acc.name,
|
||||
'code': parent_acc.code,
|
||||
'account_type': parent_acc.account_type,
|
||||
'company_id': branch.id,
|
||||
'reconcile': parent_acc.reconcile,
|
||||
'currency_id': parent_acc.currency_id.id if parent_acc.currency_id else False,
|
||||
'note': "Auto-mirrored from parent company (%s)" % parent_company.name,
|
||||
})
|
||||
_logger.info(
|
||||
"Created mirror account %s (%s) in branch %s",
|
||||
parent_acc.code, parent_acc.name, branch.name
|
||||
)
|
||||
|
||||
# Auto-create bank journals in branch for mirrored accounts
|
||||
parent_bank_journals = env['account.journal'].search([
|
||||
('company_id', '=', parent_company.id),
|
||||
('type', 'in', ('bank', 'cash')),
|
||||
])
|
||||
|
||||
for parent_journal in parent_bank_journals:
|
||||
existing_journal = env['account.journal'].search([
|
||||
('company_id', '=', branch.id),
|
||||
('code', '=', parent_journal.code),
|
||||
], limit=1)
|
||||
|
||||
if not existing_journal:
|
||||
# Find the mirrored account in the branch
|
||||
branch_account = env['account.account'].search([
|
||||
('company_id', '=', branch.id),
|
||||
('code', '=', parent_journal.default_account_id.code),
|
||||
], limit=1)
|
||||
|
||||
if branch_account:
|
||||
env['account.journal'].create({
|
||||
'name': parent_journal.name,
|
||||
'code': parent_journal.code,
|
||||
'type': parent_journal.type,
|
||||
'company_id': branch.id,
|
||||
'default_account_id': branch_account.id,
|
||||
})
|
||||
_logger.info(
|
||||
"Created mirror journal %s (%s) in branch %s",
|
||||
parent_journal.code, parent_journal.name, branch.name
|
||||
)
|
||||
|
||||
|
||||
def _cleanup_shared_accounts_uninstall(cr, registry):
|
||||
"""
|
||||
Remove the auto-mirrored accounts upon uninstallation.
|
||||
Only removes accounts tagged with the auto-mirror note and having no journal items.
|
||||
"""
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
mirrored_accounts = env['account.account'].search([
|
||||
('company_id', '!=', 2),
|
||||
('note', 'ilike', 'Auto-mirrored from parent company'),
|
||||
('account_type', '=', 'asset_cash'),
|
||||
])
|
||||
|
||||
for acc in mirrored_accounts:
|
||||
# Only delete if no journal items reference this account
|
||||
has_items = env['account.move.line'].search([
|
||||
('account_id', '=', acc.id),
|
||||
], limit=1)
|
||||
if not has_items:
|
||||
_logger.info("Removing mirrored account %s from company %s", acc.code, acc.company_id.name)
|
||||
acc.unlink()
|
||||
else:
|
||||
_logger.warning(
|
||||
"Cannot remove mirrored account %s from company %s: has journal items",
|
||||
acc.code, acc.company_id.name
|
||||
)
|
||||
22
__manifest__.py
Normal file
22
__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Account Shared Bank Cash',
|
||||
'version': '17.0.1.0',
|
||||
'category': 'Accounting',
|
||||
'summary': 'Mirror parent bank/cash accounts to branches with inter-company clearing',
|
||||
'description': """
|
||||
This module mirrors the parent company's Bank and Cash accounts and journals to branch companies,
|
||||
and provides automated inter-company clearing for POS sessions and centralized vendor payments.
|
||||
""",
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': ['point_of_sale', 'account'],
|
||||
'data': [
|
||||
'views/pos_payment_method_views.xml',
|
||||
'views/account_journal_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'post_init_hook': '_auto_share_accounts_post_init',
|
||||
'uninstall_hook': '_cleanup_shared_accounts_uninstall',
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/__manifest__.cpython-312.pyc
Normal file
BIN
__pycache__/__manifest__.cpython-312.pyc
Normal file
Binary file not shown.
5
models/__init__.py
Normal file
5
models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import account_journal
|
||||
from . import account_payment
|
||||
from . import pos_payment_method
|
||||
from . import pos_session
|
||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/account_journal.cpython-312.pyc
Normal file
BIN
models/__pycache__/account_journal.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/account_payment.cpython-312.pyc
Normal file
BIN
models/__pycache__/account_payment.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/pos_payment_method.cpython-312.pyc
Normal file
BIN
models/__pycache__/pos_payment_method.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/pos_session.cpython-312.pyc
Normal file
BIN
models/__pycache__/pos_session.cpython-312.pyc
Normal file
Binary file not shown.
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',
|
||||
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',
|
||||
check_company=False,
|
||||
help="The Hubungan RK account in the branch company to credit (Liability to parent)."
|
||||
)
|
||||
87
models/account_payment.py
Normal file
87
models/account_payment.py
Normal file
@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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. Validate centralized payments before posting
|
||||
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
|
||||
)
|
||||
|
||||
# 3. Call super() to post the original payment(s)
|
||||
res = super().action_post()
|
||||
|
||||
# 4. 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
|
||||
|
||||
_logger.info(
|
||||
"Centralized Payment for %s | Company: %s -> Parent: %s | Parent Journal: %s | RK: %s",
|
||||
payment.name, payment.company_id.name, parent_company.name,
|
||||
parent_journal.name, parent_rk_account.code
|
||||
)
|
||||
|
||||
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': _("Centralized Payment: %s (%s)") % (payment.name, payment.company_id.name),
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': payment.ref or _("Centralized Payment: %s") % payment.name,
|
||||
'account_id': parent_rk_account.id,
|
||||
'debit': payment.amount,
|
||||
'partner_id': payment.partner_id.id,
|
||||
'company_id': parent_company.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': payment.ref or _("Centralized Payment: %s") % payment.name,
|
||||
'account_id': parent_journal.default_account_id.id,
|
||||
'credit': payment.amount,
|
||||
'partner_id': payment.partner_id.id,
|
||||
'company_id': parent_company.id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
parent_move.action_post()
|
||||
_logger.info("Parent Move Posted: %s", 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:
|
||||
# Update the account if it matches the journal's default (liquidity account).
|
||||
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
|
||||
54
models/pos_payment_method.py
Normal file
54
models/pos_payment_method.py
Normal file
@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class PosPaymentMethod(models.Model):
|
||||
_inherit = 'pos.payment.method'
|
||||
|
||||
# --- Branch-side clearing configuration ---
|
||||
intercompany_clearing_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Inter-Company Clearing Account',
|
||||
domain="[('account_type', 'in', ['asset_receivable', 'liability_payable', 'asset_current'])]",
|
||||
help="If specified, an automatic clearing entry will be generated when a POS session closes. "
|
||||
"This is used to transfer the balance from a shared parent bank account to an inter-company account."
|
||||
)
|
||||
|
||||
intercompany_clearing_journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Clearing Journal',
|
||||
domain="[('type', '=', 'general')]",
|
||||
help="Journal to use for the automated inter-company clearing entries."
|
||||
)
|
||||
|
||||
# --- Parent-side mirror entry configuration ---
|
||||
parent_company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Parent Company',
|
||||
help="The parent company where the mirror clearing entry will be created. "
|
||||
"Leave empty to auto-detect from the branch company's parent."
|
||||
)
|
||||
|
||||
parent_bank_journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Parent Bank Journal',
|
||||
domain="[('type', '=', 'bank')]",
|
||||
help="The bank journal in the parent company. Its outstanding receipt account will be debited "
|
||||
"in the mirror entry, allowing reconciliation with the parent's bank statement."
|
||||
)
|
||||
|
||||
parent_intercompany_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='Parent Hubungan RK Account',
|
||||
domain="[('reconcile', '=', True)]",
|
||||
help="The Hubungan RK liability account (e.g., 229101) in the parent company to credit. "
|
||||
"This represents the parent's liability to the branch."
|
||||
)
|
||||
|
||||
parent_clearing_journal_id = fields.Many2one(
|
||||
'account.journal',
|
||||
string='Parent Clearing Journal',
|
||||
domain="[('type', '=', 'general')]",
|
||||
help="Journal in the parent company to use for the mirror clearing entry."
|
||||
)
|
||||
324
models/pos_session.py
Normal file
324
models/pos_session.py
Normal file
@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, Command, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PosSession(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
def _validate_session(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
|
||||
res = super(PosSession, self)._validate_session(balancing_account, amount_to_balance, bank_payment_method_diffs)
|
||||
|
||||
# After the standard validation and account move creation, we create the inter-company clearing moves
|
||||
self._create_intercompany_clearing_moves()
|
||||
|
||||
return res
|
||||
|
||||
def _create_bank_payment_moves(self, data):
|
||||
"""Override to skip account.payment creation for intercompany payment methods."""
|
||||
intercompany_pms = self.payment_method_ids.filtered(
|
||||
lambda pm: pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id
|
||||
)
|
||||
|
||||
if not intercompany_pms:
|
||||
return super()._create_bank_payment_moves(data)
|
||||
|
||||
combine_receivables_bank = data.get('combine_receivables_bank', {})
|
||||
MoveLine = data.get('MoveLine')
|
||||
|
||||
# Split the data into intercompany and standard
|
||||
standard_combine = {}
|
||||
intercompany_combine = {}
|
||||
|
||||
for pm, amounts in combine_receivables_bank.items():
|
||||
if pm in intercompany_pms:
|
||||
intercompany_combine[pm] = amounts
|
||||
else:
|
||||
standard_combine[pm] = amounts
|
||||
|
||||
# Call super with only standard payments
|
||||
data['combine_receivables_bank'] = standard_combine
|
||||
res_data = super()._create_bank_payment_moves(data)
|
||||
|
||||
# Restore original data
|
||||
data['combine_receivables_bank'] = combine_receivables_bank
|
||||
|
||||
# Manually handle intercompany ones: create the line in main move but skip account.payment
|
||||
for pm, amounts in intercompany_combine.items():
|
||||
combine_receivable_line = MoveLine.create(
|
||||
self._get_combine_receivable_vals(pm, amounts['amount'], amounts['amount_converted'])
|
||||
)
|
||||
res_data['payment_method_to_receivable_lines'][pm] = combine_receivable_line
|
||||
|
||||
return res_data
|
||||
|
||||
def _create_intercompany_clearing_moves(self):
|
||||
for session in self:
|
||||
if session.state != 'closed' or not session.move_id:
|
||||
continue
|
||||
|
||||
# Dictionary to accumulate amounts per payment method
|
||||
clearing_amounts = {}
|
||||
|
||||
# Find all orders and payments for this session
|
||||
orders = session.order_ids
|
||||
|
||||
for order in orders:
|
||||
for payment in order.payment_ids:
|
||||
pm = payment.payment_method_id
|
||||
if pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id:
|
||||
if pm not in clearing_amounts:
|
||||
clearing_amounts[pm] = 0.0
|
||||
clearing_amounts[pm] += payment.amount
|
||||
|
||||
# Group PMs by their clearing journal
|
||||
journal_to_pms = {}
|
||||
for pm, amount in clearing_amounts.items():
|
||||
if session.currency_id.is_zero(amount):
|
||||
continue
|
||||
journal = pm.intercompany_clearing_journal_id
|
||||
if journal not in journal_to_pms:
|
||||
journal_to_pms[journal] = []
|
||||
journal_to_pms[journal].append(pm)
|
||||
|
||||
for clearing_journal, pms in journal_to_pms.items():
|
||||
aggregated_data = {} # Key: (receivable_account, intercompany_account)
|
||||
pm_level_data = [] # For parent mirror entries
|
||||
|
||||
for pm in pms:
|
||||
amount = clearing_amounts[pm]
|
||||
receivable_account = self._get_receivable_account(pm)
|
||||
if not receivable_account:
|
||||
continue
|
||||
|
||||
intercompany_account = pm.intercompany_clearing_account_id
|
||||
|
||||
# Convert amount to company currency if needed
|
||||
amount_company_curr = amount
|
||||
if session.currency_id != session.company_id.currency_id:
|
||||
amount_company_curr = session.currency_id._convert(
|
||||
amount, session.company_id.currency_id, session.company_id,
|
||||
session.stop_at or fields.Date.context_today(session)
|
||||
)
|
||||
|
||||
# Store PM level data for parent mirror
|
||||
pm_level_data.append({
|
||||
'pm': pm,
|
||||
'amount': amount,
|
||||
'amount_company_curr': amount_company_curr,
|
||||
})
|
||||
|
||||
# Aggregate for branch move
|
||||
key = (receivable_account, intercompany_account)
|
||||
if key not in aggregated_data:
|
||||
aggregated_data[key] = {
|
||||
'total_amount': 0.0,
|
||||
'total_company_curr': 0.0,
|
||||
'pms': [],
|
||||
'receivable_account': receivable_account,
|
||||
'intercompany_account': intercompany_account,
|
||||
}
|
||||
aggregated_data[key]['total_amount'] += amount
|
||||
aggregated_data[key]['total_company_curr'] += amount_company_curr
|
||||
aggregated_data[key]['pms'].append(pm)
|
||||
|
||||
if not aggregated_data:
|
||||
continue
|
||||
|
||||
line_ids = []
|
||||
for key, agg_data in aggregated_data.items():
|
||||
# CREDIT: Total AR in Transit
|
||||
line_ids.append(Command.create({
|
||||
'name': _("Total Clearing - %s") % session.name,
|
||||
'account_id': agg_data['receivable_account'].id,
|
||||
'credit': agg_data['total_company_curr'],
|
||||
'debit': 0.0,
|
||||
'currency_id': session.currency_id.id,
|
||||
'amount_currency': -agg_data['total_amount'],
|
||||
}))
|
||||
|
||||
# DEBIT: Total Hubungan RK
|
||||
line_ids.append(Command.create({
|
||||
'name': _("Total Due from Parent - %s") % session.name,
|
||||
'account_id': agg_data['intercompany_account'].id,
|
||||
'credit': 0.0,
|
||||
'debit': agg_data['total_company_curr'],
|
||||
'currency_id': session.currency_id.id,
|
||||
'amount_currency': agg_data['total_amount'],
|
||||
}))
|
||||
|
||||
# --- BRANCH SIDE: Aggregated Clearing Move (Target: 2 items) ---
|
||||
move_vals = {
|
||||
'journal_id': clearing_journal.id,
|
||||
'date': session.stop_at or fields.Date.context_today(session),
|
||||
'ref': _("Inter-company clearing for %s") % session.name,
|
||||
'move_type': 'entry',
|
||||
'company_id': session.company_id.id,
|
||||
'line_ids': line_ids,
|
||||
}
|
||||
|
||||
try:
|
||||
clearing_move = self.env['account.move'].sudo().with_company(session.company_id).create(move_vals)
|
||||
clearing_move._post()
|
||||
|
||||
# 1. Reconcile aggregated lines with session move
|
||||
for key, agg_data in aggregated_data.items():
|
||||
receivable_account = agg_data['receivable_account']
|
||||
pm_list = agg_data['pms']
|
||||
|
||||
try:
|
||||
# Find the aggregated credit line
|
||||
clearing_credit_line = clearing_move.line_ids.filtered(
|
||||
lambda l: l.account_id == receivable_account and l.credit > 0
|
||||
)
|
||||
# Find all matching debit lines in the session move
|
||||
# Odoo 17 names these lines as "SessionName - PMName"
|
||||
pos_debit_lines = session.move_id.line_ids.filtered(
|
||||
lambda l: l.account_id == receivable_account and l.debit > 0 and
|
||||
any(l.name.endswith(" - %s" % pm.name) for pm in pm_list)
|
||||
)
|
||||
|
||||
if clearing_credit_line and pos_debit_lines:
|
||||
(clearing_credit_line + pos_debit_lines).reconcile()
|
||||
except Exception as re_e:
|
||||
_logger.warning(
|
||||
"Could not auto-reconcile aggregated clearing lines for session %s: %s",
|
||||
session.name, re_e
|
||||
)
|
||||
|
||||
# 2. Create parent mirror move
|
||||
self._create_aggregated_parent_mirror_move(session, pm_level_data, clearing_move)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
"Failed to create/post aggregated inter-company clearing move for session %s: %s",
|
||||
session.name, e
|
||||
)
|
||||
|
||||
def _get_related_account_moves(self):
|
||||
res = super()._get_related_account_moves()
|
||||
for session in self:
|
||||
clearing_moves = self.env['account.move'].sudo().search([
|
||||
('company_id', '=', session.company_id.id),
|
||||
('ref', '=', _("Inter-company clearing for %s") % session.name)
|
||||
])
|
||||
res |= clearing_moves
|
||||
return res
|
||||
|
||||
def _create_aggregated_parent_mirror_move(self, session, pm_level_data, branch_clearing_move):
|
||||
"""Create a single mirror journal entry in the parent company, with separate lines per PM.
|
||||
|
||||
For each PM:
|
||||
Debit: Outstanding receipt account of the parent bank journal
|
||||
Credit: Hubungan RK liability (229101) in the parent
|
||||
"""
|
||||
parent_groups = {}
|
||||
for data in pm_level_data:
|
||||
pm = data['pm']
|
||||
parent_company = pm.parent_company_id or session.company_id.parent_id
|
||||
if not parent_company:
|
||||
_logger.info("No parent company configured for PM %s, skipping parent mirror entry.", pm.name)
|
||||
continue
|
||||
|
||||
parent_bank_journal = pm.parent_bank_journal_id
|
||||
parent_rk_account = pm.parent_intercompany_account_id
|
||||
parent_clearing_journal = pm.parent_clearing_journal_id
|
||||
|
||||
if not parent_bank_journal or not parent_rk_account or not parent_clearing_journal:
|
||||
_logger.warning("Parent clearing not fully configured for PM %s. Skipping.", pm.name)
|
||||
continue
|
||||
|
||||
outstanding_receipt_account = None
|
||||
for pml in parent_bank_journal.inbound_payment_method_line_ids:
|
||||
if pml.payment_account_id:
|
||||
outstanding_receipt_account = pml.payment_account_id
|
||||
break
|
||||
|
||||
if not outstanding_receipt_account:
|
||||
_logger.warning(
|
||||
"No outstanding receipt account found on parent bank journal %s. Skipping PM %s.",
|
||||
parent_bank_journal.name, pm.name
|
||||
)
|
||||
continue
|
||||
|
||||
group_key = (parent_company.id, parent_clearing_journal.id)
|
||||
if group_key not in parent_groups:
|
||||
parent_groups[group_key] = {
|
||||
'parent_company': parent_company,
|
||||
'parent_clearing_journal': parent_clearing_journal,
|
||||
'lines_data': []
|
||||
}
|
||||
|
||||
parent_groups[group_key]['lines_data'].append({
|
||||
'pm': pm,
|
||||
'amount': data['amount'],
|
||||
'amount_company_curr': data['amount_company_curr'],
|
||||
'outstanding_receipt_account': outstanding_receipt_account,
|
||||
'parent_rk_account': parent_rk_account,
|
||||
})
|
||||
|
||||
entry_date = session.stop_at or fields.Date.context_today(session)
|
||||
|
||||
for group_key, group_data in parent_groups.items():
|
||||
parent_company = group_data['parent_company']
|
||||
parent_clearing_journal = group_data['parent_clearing_journal']
|
||||
|
||||
line_ids = []
|
||||
|
||||
for line_data in group_data['lines_data']:
|
||||
pm = line_data['pm']
|
||||
amount = line_data['amount']
|
||||
|
||||
parent_currency = parent_company.currency_id
|
||||
if session.currency_id != parent_currency:
|
||||
amount_parent_curr = session.currency_id._convert(
|
||||
amount, parent_currency, parent_company, entry_date
|
||||
)
|
||||
else:
|
||||
amount_parent_curr = amount
|
||||
|
||||
line_ids.append(Command.create({
|
||||
'name': _("POS Receipt: %s (%s)") % (pm.name, session.company_id.name),
|
||||
'account_id': line_data['outstanding_receipt_account'].id,
|
||||
'debit': amount_parent_curr,
|
||||
'credit': 0.0,
|
||||
'currency_id': session.currency_id.id,
|
||||
'amount_currency': amount,
|
||||
}))
|
||||
|
||||
line_ids.append(Command.create({
|
||||
'name': _("Due to Branch: %s (%s)") % (pm.name, session.company_id.name),
|
||||
'account_id': line_data['parent_rk_account'].id,
|
||||
'debit': 0.0,
|
||||
'credit': amount_parent_curr,
|
||||
'currency_id': session.currency_id.id,
|
||||
'amount_currency': -amount,
|
||||
}))
|
||||
|
||||
if not line_ids:
|
||||
continue
|
||||
|
||||
parent_move_vals = {
|
||||
'journal_id': parent_clearing_journal.id,
|
||||
'date': entry_date,
|
||||
'ref': _("POS Mirror: %s - %s") % (session.name, session.company_id.name),
|
||||
'move_type': 'entry',
|
||||
'company_id': parent_company.id,
|
||||
'line_ids': line_ids,
|
||||
}
|
||||
|
||||
try:
|
||||
parent_move = self.env['account.move'].sudo().with_company(parent_company).create(parent_move_vals)
|
||||
parent_move._post()
|
||||
_logger.info(
|
||||
"Created aggregated parent mirror entry %s in company %s for session %s (Lines: %s)",
|
||||
parent_move.name, parent_company.name, session.name, len(line_ids)
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
"Failed to create aggregated parent mirror entry for session %s in company %s: %s",
|
||||
session.name, parent_company.name, e
|
||||
)
|
||||
31
views/account_journal_views.xml
Normal file
31
views/account_journal_views.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_account_journal_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account.journal.form.inherit.shared.bank</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">
|
||||
<group>
|
||||
<field name="is_centralized"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('is_centralized', '=', False)]}">
|
||||
<group string="Parent Company Settings">
|
||||
<field name="parent_company_id"
|
||||
attrs="{'required': [('is_centralized', '=', True)]}"/>
|
||||
<field name="parent_journal_id"
|
||||
attrs="{'required': [('is_centralized', '=', True)]}"/>
|
||||
</group>
|
||||
<group string="Inter-Company (RK) Accounts">
|
||||
<field name="parent_intercompany_account_id"
|
||||
attrs="{'required': [('is_centralized', '=', True)]}"/>
|
||||
<field name="branch_intercompany_account_id"
|
||||
attrs="{'required': [('is_centralized', '=', True)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
31
views/pos_payment_method_views.xml
Normal file
31
views/pos_payment_method_views.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_pos_payment_method_form_inherit" model="ir.ui.view">
|
||||
<field name="name">pos.payment.method.form.inherit.shared.bank</field>
|
||||
<field name="model">pos.payment.method</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='Payment methods']" position="after">
|
||||
<group name="intercompany_clearing" string="Inter-Company Clearing">
|
||||
<group>
|
||||
<field name="intercompany_clearing_account_id"/>
|
||||
<field name="intercompany_clearing_journal_id"
|
||||
attrs="{'required': [('intercompany_clearing_account_id', '!=', False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="parent_mirror_entry" string="Parent Mirror Entry"
|
||||
attrs="{'invisible': [('intercompany_clearing_account_id', '=', False)]}">
|
||||
<group>
|
||||
<field name="parent_company_id"/>
|
||||
<field name="parent_bank_journal_id"
|
||||
attrs="{'required': [('parent_company_id', '!=', False)]}"/>
|
||||
<field name="parent_intercompany_account_id"
|
||||
attrs="{'required': [('parent_company_id', '!=', False)]}"/>
|
||||
<field name="parent_clearing_journal_id"
|
||||
attrs="{'required': [('parent_company_id', '!=', False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</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
|
||||
BIN
wizard/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
wizard/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
wizard/__pycache__/account_payment_register.cpython-312.pyc
Normal file
BIN
wizard/__pycache__/account_payment_register.cpython-312.pyc
Normal file
Binary file not shown.
122
wizard/account_payment_register.py
Normal file
122
wizard/account_payment_register.py
Normal file
@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountPaymentRegister(models.TransientModel):
|
||||
_inherit = 'account.payment.register'
|
||||
|
||||
def _create_payments(self):
|
||||
"""Intercept centralized payments to create inter-company clearing moves."""
|
||||
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.
|
||||
|
||||
Branch side: Creates a clearing move (Debit Payable, Credit RK)
|
||||
Parent side: Creates an account.payment (Debit RK, Credit Bank)
|
||||
"""
|
||||
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']
|
||||
|
||||
# Use _get_batches() - Odoo 17 method (not self.batches property)
|
||||
all_batches = self._get_batches()
|
||||
|
||||
for batch_result in all_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')))
|
||||
|
||||
# Currency logic: debit/credit in company currency, amount_currency in foreign currency.
|
||||
amount_company_curr = abs(sum(lines.mapped('amount_residual')))
|
||||
amount_currency = 0.0
|
||||
currency_id = False
|
||||
|
||||
if self.currency_id != branch_company.currency_id:
|
||||
amount_currency = sum(lines.mapped('amount_residual_currency'))
|
||||
currency_id = self.currency_id.id
|
||||
|
||||
# 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_company_curr if self.payment_type == 'outbound' else 0.0,
|
||||
'credit': amount_company_curr if self.payment_type == 'inbound' else 0.0,
|
||||
'currency_id': currency_id,
|
||||
'amount_currency': amount_currency if self.payment_type == 'outbound' else -amount_currency,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': _("Due to Parent (%s)") % parent_company.name,
|
||||
'partner_id': False,
|
||||
'account_id': journal.branch_intercompany_account_id.id,
|
||||
'debit': amount_company_curr if self.payment_type == 'inbound' else 0.0,
|
||||
'credit': amount_company_curr if self.payment_type == 'outbound' else 0.0,
|
||||
'currency_id': currency_id,
|
||||
'amount_currency': -amount_currency if self.payment_type == 'outbound' else amount_currency,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
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': abs(amount_currency) if currency_id else amount_company_curr,
|
||||
'currency_id': self.currency_id.id,
|
||||
'date': self.payment_date,
|
||||
'ref': _("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