diff --git a/models/pos_payment_method.py b/models/pos_payment_method.py index ef76c44..ac12b2f 100644 --- a/models/pos_payment_method.py +++ b/models/pos_payment_method.py @@ -2,9 +2,11 @@ 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', @@ -12,10 +14,41 @@ class PosPaymentMethod(models.Model): 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 index 8cd3d5b..4c5260c 100644 --- a/models/pos_session.py +++ b/models/pos_session.py @@ -16,34 +16,69 @@ class PosSession(models.Model): 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 - # Key: pos.payment.method, Value: float (amount) clearing_amounts = {} - # Find all payments for this session - orders = session.get_session_orders() - payments = orders.payment_ids + # Find all orders and payments for this session + orders = session.order_ids - for payment in payments: - 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 - + 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 + for pm, amount in clearing_amounts.items(): - if session.currency_id.compare_amounts(amount, 0) <= 0: + if session.currency_id.is_zero(amount): continue - # The outstanding account of the payment method is the Shared Bank Account - shared_bank_account = pm.outstanding_account_id or pm.journal_id.default_account_id - if not shared_bank_account: - _logger.warning("No outstanding account found on payment method %s, cannot create clearing entry.", pm.name) + # The account to CLEAR is the receivable account used in the main POS move (e.g., AR in Transit) + receivable_account = self._get_receivable_account(pm) + if not receivable_account: + _logger.warning("No receivable account found on payment method %s, skipping inter-company clearing.", pm.name) continue intercompany_account = pm.intercompany_clearing_account_id @@ -53,25 +88,26 @@ class PosSession(models.Model): 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, fields.Date.context_today(session) + amount, session.company_id.currency_id, session.company_id, session.stop_at or fields.Date.context_today(session) ) - # We need to CREDIT the shared bank account (to clear it in the Branch) - # and DEBIT the intercompany account - + # --- BRANCH SIDE: Clearing Move --- + # DEBIT: Hubungan RK (129101) - records that parent owes us + # CREDIT: AR in Transit - clears the receivable from the main POS move move_vals = { 'journal_id': clearing_journal.id, - 'date': fields.Date.context_today(session), + 'date': session.stop_at or fields.Date.context_today(session), 'ref': f"Inter-company clearing for {session.name} ({pm.name})", - 'pos_session_id': session.id, # Link back to session if pos_session_id field exists on move + 'move_type': 'entry', + 'company_id': session.company_id.id, 'line_ids': [ Command.create({ 'name': f"Clearing: {pm.name}", - 'account_id': shared_bank_account.id, + 'account_id': receivable_account.id, 'credit': amount_company_curr, 'debit': 0.0, 'currency_id': session.currency_id.id, - 'amount_currency': -amount if session.currency_id != session.company_id.currency_id else 0.0, + 'amount_currency': -amount, }), Command.create({ 'name': f"Due from Parent: {pm.name}", @@ -79,24 +115,124 @@ class PosSession(models.Model): 'credit': 0.0, 'debit': amount_company_curr, 'currency_id': session.currency_id.id, - 'amount_currency': amount if session.currency_id != session.company_id.currency_id else 0.0, + 'amount_currency': amount, }) ] } - # Check if 'pos_session_id' exists on account.move, if not remove it - if 'pos_session_id' not in self.env['account.move']._fields: - move_vals.pop('pos_session_id', None) - - clearing_move = self.env['account.move'].sudo().with_company(session.company_id).create(move_vals) - clearing_move._post() - - # Attempt to auto-reconcile the credit line with the debit line from the main POS move try: - clearing_credit_line = clearing_move.line_ids.filtered(lambda l: l.account_id == shared_bank_account and l.credit > 0) - pos_debit_lines = session.move_id.line_ids.filtered(lambda l: l.account_id == shared_bank_account and l.debit > 0) + clearing_move = self.env['account.move'].sudo().with_company(session.company_id).create(move_vals) + clearing_move._post() - if clearing_credit_line and pos_debit_lines: - (clearing_credit_line + pos_debit_lines).reconcile() + # Auto-reconcile with the main POS move lines (branch side) + try: + clearing_credit_line = clearing_move.line_ids.filtered(lambda l: l.account_id == receivable_account and l.credit > 0) + # Find the debit line in the session move that hits the same receivable account and matches the PM name + pos_debit_lines = session.move_id.line_ids.filtered( + lambda l: l.account_id == receivable_account and l.debit > 0 and l.name == f"{session.name} - {pm.name}" + ) + + 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 inter-company clearing lines for session %s: %s", session.name, re_e) + + # --- PARENT SIDE: Mirror Entry --- + self._create_parent_mirror_move(session, pm, amount, amount_company_curr, clearing_move) + except Exception as e: - _logger.warning("Could not auto-reconcile inter-company clearing lines for session %s: %s", session.name, e) + _logger.error("Failed to create/post inter-company clearing move for session %s, PM %s: %s", session.name, pm.name, e) + + def _create_parent_mirror_move(self, session, pm, amount, amount_company_curr, branch_clearing_move): + """Create the mirror journal entry in the parent company. + + Debit: Outstanding receipt account of the parent bank journal + (for bank statement reconciliation in the parent) + Credit: Hubungan RK liability (229101) in the parent + (records the parent's liability to the branch) + """ + # Determine parent company + 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) + return + + # Validate required parent configuration + 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. " + "Need: parent_bank_journal_id, parent_intercompany_account_id, parent_clearing_journal_id. " + "Skipping parent mirror entry.", + pm.name + ) + return + + # Get the outstanding receipt account from the parent bank journal + 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 parent mirror entry for PM %s.", + parent_bank_journal.name, pm.name + ) + return + + # Convert amount to parent company currency if needed + parent_currency = parent_company.currency_id + if session.currency_id != parent_currency: + amount_parent_curr = session.currency_id._convert( + amount, parent_currency, parent_company, + session.stop_at or fields.Date.context_today(session) + ) + else: + amount_parent_curr = amount + + entry_date = session.stop_at or fields.Date.context_today(session) + + parent_move_vals = { + 'journal_id': parent_clearing_journal.id, + 'date': entry_date, + 'ref': f"POS Mirror: {session.name} ({pm.name}) - {session.company_id.name}", + 'move_type': 'entry', + 'company_id': parent_company.id, + 'line_ids': [ + Command.create({ + 'name': f"POS Receipt: {pm.name} ({session.company_id.name})", + 'account_id': outstanding_receipt_account.id, + 'debit': amount_parent_curr, + 'credit': 0.0, + 'currency_id': session.currency_id.id, + 'amount_currency': amount, + }), + Command.create({ + 'name': f"Due to Branch: {pm.name} ({session.company_id.name})", + 'account_id': parent_rk_account.id, + 'debit': 0.0, + 'credit': amount_parent_curr, + 'currency_id': session.currency_id.id, + 'amount_currency': -amount, + }), + ] + } + + try: + parent_move = self.env['account.move'].sudo().with_company(parent_company).create(parent_move_vals) + parent_move._post() + _logger.info( + "Created parent mirror entry %s in company %s for session %s PM %s (amount: %s)", + parent_move.name, parent_company.name, session.name, pm.name, amount_parent_curr + ) + except Exception as e: + _logger.error( + "Failed to create parent mirror entry for session %s, PM %s in company %s: %s", + session.name, pm.name, parent_company.name, e + ) diff --git a/scripts/check_session.py b/scripts/check_session.py new file mode 100644 index 0000000..5da1ca3 --- /dev/null +++ b/scripts/check_session.py @@ -0,0 +1,26 @@ + +import sys + +# Get the last session +session = env['pos.session'].search([('state', '=', 'closed')], order='id desc', limit=1) +print(f"Session: {session.name} (ID {session.id})") + +# Check main POS move +print(f"Main POS Move: {session.move_id.name} (ID {session.move_id.id})") +for line in session.move_id.line_ids: + print(f" Line: {line.name} | Account: {line.account_id.code} | Debit: {line.debit} | Credit: {line.credit} | Reconciled: {line.reconciled}") + +# Check for Bank Journal moves (these should NOT exist for intercompany PMs) +# Bank journal moves usually have payment_method_id set on account.payment +payments = env['account.payment'].search([('pos_session_id', '=', session.id)]) +print(f"Payment moves found: {len(payments)}") +for p in payments: + print(f" Payment: {p.name} | Journal: {p.journal_id.name} | Amount: {p.amount}") + +# Check for Inter-company clearing moves +clearing_moves = env['account.move'].search([('ref', 'like', f"Inter-company clearing for {session.name}%")]) +print(f"Clearing moves found: {len(clearing_moves)}") +for move in clearing_moves: + print(f"Move: {move.name} | Ref: {move.ref} | State: {move.state}") + for line in move.line_ids: + print(f" Line: {line.name} | Account: {line.account_id.code} {line.account_id.name} | Debit: {line.debit} | Credit: {line.credit} | Reconciled: {line.reconciled}") diff --git a/scripts/debug_clearing.py b/scripts/debug_clearing.py new file mode 100644 index 0000000..599b7f5 --- /dev/null +++ b/scripts/debug_clearing.py @@ -0,0 +1,11 @@ + +# Fix: Change the Studio view's required attribute from True to conditional +view = env['ir.ui.view'].browse(2122) +print(f"Before: {view.arch_db}") + +new_arch = view.arch_db.replace('required="True"', 'required="state == "draft""') +view.write({'arch_db': new_arch}) + +print(f"\nAfter: {view.arch_db}") +env.cr.commit() +print("\nDone! x_studio_analytic_account is now only required for draft entries.") diff --git a/views/pos_payment_method_views.xml b/views/pos_payment_method_views.xml index 92cb06d..ec76ac4 100644 --- a/views/pos_payment_method_views.xml +++ b/views/pos_payment_method_views.xml @@ -6,12 +6,22 @@ - + + + + + + +