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 @@
-
+
+
+
+
+
+
+