feat: add parent-side clearing configuration to POS payment methods and update session closing logic to support intercompany transfers
This commit is contained in:
parent
6a8fcab936
commit
63f551d551
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from odoo import models, fields
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
class PosPaymentMethod(models.Model):
|
class PosPaymentMethod(models.Model):
|
||||||
_inherit = 'pos.payment.method'
|
_inherit = 'pos.payment.method'
|
||||||
|
|
||||||
|
# --- Branch-side clearing configuration ---
|
||||||
intercompany_clearing_account_id = fields.Many2one(
|
intercompany_clearing_account_id = fields.Many2one(
|
||||||
'account.account',
|
'account.account',
|
||||||
string='Inter-Company Clearing Account',
|
string='Inter-Company Clearing Account',
|
||||||
@ -19,3 +21,34 @@ class PosPaymentMethod(models.Model):
|
|||||||
domain="[('type', '=', 'general')]",
|
domain="[('type', '=', 'general')]",
|
||||||
help="Journal to use for the automated inter-company clearing entries."
|
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."
|
||||||
|
)
|
||||||
|
|||||||
@ -16,34 +16,69 @@ class PosSession(models.Model):
|
|||||||
|
|
||||||
return res
|
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):
|
def _create_intercompany_clearing_moves(self):
|
||||||
for session in self:
|
for session in self:
|
||||||
if session.state != 'closed' or not session.move_id:
|
if session.state != 'closed' or not session.move_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Dictionary to accumulate amounts per payment method
|
# Dictionary to accumulate amounts per payment method
|
||||||
# Key: pos.payment.method, Value: float (amount)
|
|
||||||
clearing_amounts = {}
|
clearing_amounts = {}
|
||||||
|
|
||||||
# Find all payments for this session
|
# Find all orders and payments for this session
|
||||||
orders = session.get_session_orders()
|
orders = session.order_ids
|
||||||
payments = orders.payment_ids
|
|
||||||
|
|
||||||
for payment in payments:
|
for order in orders:
|
||||||
pm = payment.payment_method_id
|
for payment in order.payment_ids:
|
||||||
if pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id:
|
pm = payment.payment_method_id
|
||||||
if pm not in clearing_amounts:
|
if pm.intercompany_clearing_account_id and pm.intercompany_clearing_journal_id:
|
||||||
clearing_amounts[pm] = 0.0
|
if pm not in clearing_amounts:
|
||||||
clearing_amounts[pm] += payment.amount
|
clearing_amounts[pm] = 0.0
|
||||||
|
clearing_amounts[pm] += payment.amount
|
||||||
|
|
||||||
for pm, amount in clearing_amounts.items():
|
for pm, amount in clearing_amounts.items():
|
||||||
if session.currency_id.compare_amounts(amount, 0) <= 0:
|
if session.currency_id.is_zero(amount):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# The outstanding account of the payment method is the Shared Bank Account
|
# The account to CLEAR is the receivable account used in the main POS move (e.g., AR in Transit)
|
||||||
shared_bank_account = pm.outstanding_account_id or pm.journal_id.default_account_id
|
receivable_account = self._get_receivable_account(pm)
|
||||||
if not shared_bank_account:
|
if not receivable_account:
|
||||||
_logger.warning("No outstanding account found on payment method %s, cannot create clearing entry.", pm.name)
|
_logger.warning("No receivable account found on payment method %s, skipping inter-company clearing.", pm.name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
intercompany_account = pm.intercompany_clearing_account_id
|
intercompany_account = pm.intercompany_clearing_account_id
|
||||||
@ -53,25 +88,26 @@ class PosSession(models.Model):
|
|||||||
amount_company_curr = amount
|
amount_company_curr = amount
|
||||||
if session.currency_id != session.company_id.currency_id:
|
if session.currency_id != session.company_id.currency_id:
|
||||||
amount_company_curr = session.currency_id._convert(
|
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)
|
# --- BRANCH SIDE: Clearing Move ---
|
||||||
# and DEBIT the intercompany account
|
# DEBIT: Hubungan RK (129101) - records that parent owes us
|
||||||
|
# CREDIT: AR in Transit - clears the receivable from the main POS move
|
||||||
move_vals = {
|
move_vals = {
|
||||||
'journal_id': clearing_journal.id,
|
'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})",
|
'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': [
|
'line_ids': [
|
||||||
Command.create({
|
Command.create({
|
||||||
'name': f"Clearing: {pm.name}",
|
'name': f"Clearing: {pm.name}",
|
||||||
'account_id': shared_bank_account.id,
|
'account_id': receivable_account.id,
|
||||||
'credit': amount_company_curr,
|
'credit': amount_company_curr,
|
||||||
'debit': 0.0,
|
'debit': 0.0,
|
||||||
'currency_id': session.currency_id.id,
|
'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({
|
Command.create({
|
||||||
'name': f"Due from Parent: {pm.name}",
|
'name': f"Due from Parent: {pm.name}",
|
||||||
@ -79,24 +115,124 @@ class PosSession(models.Model):
|
|||||||
'credit': 0.0,
|
'credit': 0.0,
|
||||||
'debit': amount_company_curr,
|
'debit': amount_company_curr,
|
||||||
'currency_id': session.currency_id.id,
|
'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:
|
try:
|
||||||
clearing_credit_line = clearing_move.line_ids.filtered(lambda l: l.account_id == shared_bank_account and l.credit > 0)
|
clearing_move = self.env['account.move'].sudo().with_company(session.company_id).create(move_vals)
|
||||||
pos_debit_lines = session.move_id.line_ids.filtered(lambda l: l.account_id == shared_bank_account and l.debit > 0)
|
clearing_move._post()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
if clearing_credit_line and pos_debit_lines:
|
|
||||||
(clearing_credit_line + pos_debit_lines).reconcile()
|
|
||||||
except Exception as e:
|
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
|
||||||
|
)
|
||||||
|
|||||||
26
scripts/check_session.py
Normal file
26
scripts/check_session.py
Normal file
@ -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}")
|
||||||
11
scripts/debug_clearing.py
Normal file
11
scripts/debug_clearing.py
Normal file
@ -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.")
|
||||||
@ -6,12 +6,22 @@
|
|||||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//group[@name='Payment methods']" position="after">
|
<xpath expr="//group[@name='Payment methods']" position="after">
|
||||||
<group name="intercompany_clearing" string="Inter-Company Clearing">
|
<group name="intercompany_clearing" string="Inter-Company Clearing (Branch Side)">
|
||||||
<field name="intercompany_clearing_account_id"/>
|
<field name="intercompany_clearing_account_id"/>
|
||||||
<field name="intercompany_clearing_journal_id"
|
<field name="intercompany_clearing_journal_id"
|
||||||
invisible="not intercompany_clearing_account_id"
|
invisible="not intercompany_clearing_account_id"
|
||||||
required="intercompany_clearing_account_id"/>
|
required="intercompany_clearing_account_id"/>
|
||||||
</group>
|
</group>
|
||||||
|
<group name="parent_clearing" string="Inter-Company Clearing (Parent Side)"
|
||||||
|
invisible="not intercompany_clearing_account_id">
|
||||||
|
<field name="parent_company_id"/>
|
||||||
|
<field name="parent_bank_journal_id"
|
||||||
|
required="intercompany_clearing_account_id"/>
|
||||||
|
<field name="parent_intercompany_account_id"
|
||||||
|
required="intercompany_clearing_account_id"/>
|
||||||
|
<field name="parent_clearing_journal_id"
|
||||||
|
required="intercompany_clearing_account_id"/>
|
||||||
|
</group>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user