commit aaade5784b8fda49b9eaa43ff8f7afb63342c752 Author: Suherdy Yacob Date: Sat Jun 20 12:55:40 2026 +0700 refactored to 17.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..76fbb55 --- /dev/null +++ b/README.md @@ -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` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..425a749 --- /dev/null +++ b/__init__.py @@ -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 + ) diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..2d5b5d6 --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..6ae7cad Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/__manifest__.cpython-312.pyc b/__pycache__/__manifest__.cpython-312.pyc new file mode 100644 index 0000000..ff031e4 Binary files /dev/null and b/__pycache__/__manifest__.cpython-312.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..2964ed2 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import account_journal +from . import account_payment +from . import pos_payment_method +from . import pos_session diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0f2956d Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/account_journal.cpython-312.pyc b/models/__pycache__/account_journal.cpython-312.pyc new file mode 100644 index 0000000..373640c Binary files /dev/null and b/models/__pycache__/account_journal.cpython-312.pyc differ diff --git a/models/__pycache__/account_payment.cpython-312.pyc b/models/__pycache__/account_payment.cpython-312.pyc new file mode 100644 index 0000000..2eadbf9 Binary files /dev/null and b/models/__pycache__/account_payment.cpython-312.pyc differ diff --git a/models/__pycache__/pos_payment_method.cpython-312.pyc b/models/__pycache__/pos_payment_method.cpython-312.pyc new file mode 100644 index 0000000..0c5dee8 Binary files /dev/null and b/models/__pycache__/pos_payment_method.cpython-312.pyc differ diff --git a/models/__pycache__/pos_session.cpython-312.pyc b/models/__pycache__/pos_session.cpython-312.pyc new file mode 100644 index 0000000..78881a4 Binary files /dev/null and b/models/__pycache__/pos_session.cpython-312.pyc differ diff --git a/models/account_journal.py b/models/account_journal.py new file mode 100644 index 0000000..2941df9 --- /dev/null +++ b/models/account_journal.py @@ -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)." + ) diff --git a/models/account_payment.py b/models/account_payment.py new file mode 100644 index 0000000..8472a97 --- /dev/null +++ b/models/account_payment.py @@ -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 diff --git a/models/pos_payment_method.py b/models/pos_payment_method.py new file mode 100644 index 0000000..ac12b2f --- /dev/null +++ b/models/pos_payment_method.py @@ -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." + ) diff --git a/models/pos_session.py b/models/pos_session.py new file mode 100644 index 0000000..efb9105 --- /dev/null +++ b/models/pos_session.py @@ -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 + ) diff --git a/views/account_journal_views.xml b/views/account_journal_views.xml new file mode 100644 index 0000000..feb4959 --- /dev/null +++ b/views/account_journal_views.xml @@ -0,0 +1,31 @@ + + + + account.journal.form.inherit.shared.bank + account.journal + + + + + + + + + + + + + + + + + + + + + + diff --git a/views/pos_payment_method_views.xml b/views/pos_payment_method_views.xml new file mode 100644 index 0000000..816ef2f --- /dev/null +++ b/views/pos_payment_method_views.xml @@ -0,0 +1,31 @@ + + + + pos.payment.method.form.inherit.shared.bank + pos.payment.method + + + + + + + + + + + + + + + + + + + + + diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..353adbd --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import account_payment_register diff --git a/wizard/__pycache__/__init__.cpython-312.pyc b/wizard/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ffe982a Binary files /dev/null and b/wizard/__pycache__/__init__.cpython-312.pyc differ diff --git a/wizard/__pycache__/account_payment_register.cpython-312.pyc b/wizard/__pycache__/account_payment_register.cpython-312.pyc new file mode 100644 index 0000000..4ed0400 Binary files /dev/null and b/wizard/__pycache__/account_payment_register.cpython-312.pyc differ diff --git a/wizard/account_payment_register.py b/wizard/account_payment_register.py new file mode 100644 index 0000000..00a1217 --- /dev/null +++ b/wizard/account_payment_register.py @@ -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