diff --git a/__init__.py b/__init__.py index f5ba686..d1c9c61 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,4 @@ -# -*- coding: utf-8 -*- -from . import models \ No newline at end of file +from . import models +from . import wizard +from . import data +from . import doc diff --git a/__manifest__.py b/__manifest__.py index b0fec18..be56a4b 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -17,9 +17,11 @@ account payable. 'data': [ 'security/ir.model.access.csv', 'data/account_payment_method_data.xml', + 'views/res_company_views.xml', 'views/account_batch_payment_views.xml', 'views/account_payment_views.xml', 'views/account_payment_register_views.xml', + 'wizard/intercompany_settlement_wizard_views.xml', ], 'installable': True, 'application': False, diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b3c161b..0000000 Binary files a/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/models/__init__.py b/models/__init__.py index 7ae7e6d..48ae44d 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- from . import account_batch_payment from . import account_payment from . import account_payment_register from . import account_payment_debug +from . import res_company diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index c2b51c6..0000000 Binary files a/models/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/models/__pycache__/account_batch_payment.cpython-312.pyc b/models/__pycache__/account_batch_payment.cpython-312.pyc deleted file mode 100644 index 4963f81..0000000 Binary files a/models/__pycache__/account_batch_payment.cpython-312.pyc and /dev/null differ diff --git a/models/account_batch_payment.py b/models/account_batch_payment.py index 9266563..0cb127b 100644 --- a/models/account_batch_payment.py +++ b/models/account_batch_payment.py @@ -91,35 +91,124 @@ class AccountBatchPayment(models.Model): payment_type = self.batch_type partner_type = 'customer' if self.batch_type == 'inbound' else 'supplier' - # Create the payment - payment_vals = { - 'payment_type': payment_type, - 'partner_type': partner_type, - 'partner_id': line.partner_id.id, - 'amount': line.amount, - 'currency_id': line.currency_id.id, - 'date': line.date, - 'journal_id': self.journal_id.id, - 'company_id': line.company_id.id, - 'payment_method_line_id': payment_method_line.id, - 'ref': line.memo, - 'expense_account_id': line.expense_account_id.id, - } + # Intercompany Logic + ho_company = self.company_id + branch_company = line.company_id - # Create the payment with the branch's company context, bypassing security limits momentarily - payment = self.env['account.payment'].sudo().with_context( - allowed_company_ids=[line.company_id.id, self.company_id.id, self.env.company.id], - default_company_id=line.company_id.id, - ).create(payment_vals) + is_intercompany = branch_company and branch_company != ho_company - # Since some compute methods might try to overwrite company_id based on journal, force it again - payment.write({'company_id': line.company_id.id}) - - payment.action_post() - - # Link the payment to the line - line.payment_id = payment.id - payment_ids.append(payment.id) + if is_intercompany: + if not ho_company.intercompany_receivable_account_id: + raise ValidationError(_("Please set an Intercompany Receivable Account on company %s (HO).", ho_company.name)) + if not ho_company.intercompany_payable_account_id: + raise ValidationError(_("Please set an Intercompany Payable Account on company %s (HO).", ho_company.name)) + if not branch_company.intercompany_receivable_account_id: + raise ValidationError(_("Please set an Intercompany Receivable Account on company %s (Branch).", branch_company.name)) + if not branch_company.intercompany_payable_account_id: + raise ValidationError(_("Please set an Intercompany Payable Account on company %s (Branch).", branch_company.name)) + + # Payment in Head Office + # HO pays for branch. Thus HO is owed money by the branch -> Debit Intercompany Receivable. + expense_account = ho_company.intercompany_receivable_account_id if payment_type == 'outbound' else ho_company.intercompany_payable_account_id + + # Create the HO Payment + payment_vals = { + 'payment_type': payment_type, + 'partner_type': partner_type, + 'partner_id': line.partner_id.id, + 'amount': line.amount, + 'currency_id': line.currency_id.id, + 'date': line.date, + 'journal_id': self.journal_id.id, + 'company_id': ho_company.id, + 'payment_method_line_id': payment_method_line.id, + 'ref': f"{line.memo or ''} (For {branch_company.name})", + 'expense_account_id': expense_account.id, + } + + payment = self.env['account.payment'].create(payment_vals) + payment.action_post() + + # Branch Move + # Branch records the actual expense, and owes money to HO -> Credit Intercompany Payable. + branch_journal = self.env['account.journal'].sudo().search([ + ('type', '=', 'general'), + ('company_id', '=', branch_company.id) + ], limit=1) + + if not branch_journal: + raise ValidationError(_("Please create a Miscellaneous journal in company %s.", branch_company.name)) + + line_amount = line.amount if payment_type == 'outbound' else -line.amount + + intercompany_account = branch_company.intercompany_payable_account_id if payment_type == 'outbound' else branch_company.intercompany_receivable_account_id + + move_lines = [ + (0, 0, { + 'name': line.memo or 'Intercompany Payment', + 'account_id': line.expense_account_id.id, + 'debit': line_amount if line_amount > 0 else 0.0, + 'credit': -line_amount if line_amount < 0 else 0.0, + 'partner_id': line.partner_id.id, + 'currency_id': line.currency_id.id, + }), + (0, 0, { + 'name': f"Intercompany Pay/Rec - {ho_company.name}", + 'account_id': intercompany_account.id, + 'debit': -line_amount if line_amount < 0 else 0.0, + 'credit': line_amount if line_amount > 0 else 0.0, + 'partner_id': ho_company.partner_id.id, # Owe HO + 'currency_id': line.currency_id.id, + }) + ] + + move_vals = { + 'journal_id': branch_journal.id, + 'date': line.date, + 'ref': f"Intercompany {ho_company.name} - {self.name}", + 'company_id': branch_company.id, + 'line_ids': move_lines, + } + + branch_move = self.env['account.move'].sudo().with_company(branch_company).create(move_vals) + branch_move.action_post() + + # Link both + line.payment_id = payment.id + line.branch_move_id = branch_move.id + payment_ids.append(payment.id) + + else: + # Standard Logic / Non-Intercompany + payment_vals = { + 'payment_type': payment_type, + 'partner_type': partner_type, + 'partner_id': line.partner_id.id, + 'amount': line.amount, + 'currency_id': line.currency_id.id, + 'date': line.date, + 'journal_id': self.journal_id.id, + 'company_id': line.company_id.id, + 'payment_method_line_id': payment_method_line.id, + 'ref': line.memo, + 'expense_account_id': line.expense_account_id.id, + } + + # Create the payment with the branch's company context, bypassing security limits momentarily + payment = self.env['account.payment'].sudo().with_context( + allowed_company_ids=[line.company_id.id, self.company_id.id, self.env.company.id], + default_company_id=line.company_id.id, + ).create(payment_vals) + + # Since some compute methods might try to overwrite company_id based on journal, force it again + payment.write({'company_id': line.company_id.id}) + + payment.action_post() + + # Link the payment to the line + line.payment_id = payment.id + payment_ids.append(payment.id) + # Add the generated payments to the batch if payment_ids: @@ -202,6 +291,11 @@ class AccountBatchPaymentLine(models.Model): string='Generated Payment', readonly=True ) + branch_move_id = fields.Many2one( + 'account.move', + string='Branch Intercompany Entry', + readonly=True + ) @api.onchange('partner_id') def _onchange_partner_id(self): diff --git a/models/res_company.py b/models/res_company.py new file mode 100644 index 0000000..6a676b1 --- /dev/null +++ b/models/res_company.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + intercompany_receivable_account_id = fields.Many2one( + 'account.account', + string="Intercompany Receivable Account", + check_company=True, + domain="[('company_id', '=', id)]", + help="Account used for recording intercompany receivables from branches." + ) + intercompany_payable_account_id = fields.Many2one( + 'account.account', + string="Intercompany Payable Account", + check_company=True, + domain="[('company_id', '=', id)]", + help="Account used for recording intercompany payables to the head office." + ) diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index 583848e..536465b 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_account_batch_payment_line,account.batch.payment.line,model_account_batch_payment_line,,1,1,1,1 \ No newline at end of file +access_account_batch_payment_line,account.batch.payment.line,model_account_batch_payment_line,,1,1,1,1 +access_intercompany_settlement_wizard,intercompany.settlement.wizard,model_intercompany_settlement_wizard,account.group_account_user,1,1,1,1 \ No newline at end of file diff --git a/views/account_batch_payment_views.xml b/views/account_batch_payment_views.xml index 6d7ee3b..c347696 100644 --- a/views/account_batch_payment_views.xml +++ b/views/account_batch_payment_views.xml @@ -9,13 +9,18 @@ +<<<<<<< HEAD +======= + +>>>>>>> 2b945da (feat: Implement intercompany settlement logic for batch payments, adding dedicated company accounts and a new settlement wizard.) + diff --git a/views/res_company_views.xml b/views/res_company_views.xml new file mode 100644 index 0000000..1ee93e4 --- /dev/null +++ b/views/res_company_views.xml @@ -0,0 +1,20 @@ + + + + res.company.form.inherit.vendor.batch.payment.merge + res.company + + + + + + + + + + + + + + + diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..b4bf599 --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import intercompany_settlement_wizard diff --git a/wizard/intercompany_settlement_wizard.py b/wizard/intercompany_settlement_wizard.py new file mode 100644 index 0000000..efc00ec --- /dev/null +++ b/wizard/intercompany_settlement_wizard.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class IntercompanySettlementWizard(models.TransientModel): + _name = 'intercompany.settlement.wizard' + _description = 'Intercompany Settlement Wizard' + + ho_company_id = fields.Many2one( + 'res.company', + string='Head Office', + required=True, + default=lambda self: self.env.company + ) + branch_company_id = fields.Many2one( + 'res.company', + string='Branch Company', + required=True, + domain="[('id', '!=', ho_company_id)]" + ) + amount = fields.Monetary( + string='Settlement Amount', + required=True + ) + currency_id = fields.Many2one( + 'res.currency', + string='Currency', + related='ho_company_id.currency_id' + ) + date = fields.Date( + string='Settlement Date', + required=True, + default=fields.Date.context_today + ) + ho_journal_id = fields.Many2one( + 'account.journal', + string='HO Bank/Cash Journal', + required=True, + domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', ho_company_id)]" + ) + branch_journal_id = fields.Many2one( + 'account.journal', + string='Branch Bank/Cash Journal', + required=True, + domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', branch_company_id)]" + ) + memo = fields.Char(string='Memo', default='Intercompany Settlement') + + def action_confirm_settlement(self): + self.ensure_one() + + if self.amount <= 0: + raise ValidationError(_("Settlement amount must be greater than zero.")) + + if not self.ho_company_id.intercompany_receivable_account_id: + raise ValidationError(_("Please set an Intercompany Receivable Account on company %s.", self.ho_company_id.name)) + + if not self.branch_company_id.intercompany_payable_account_id: + raise ValidationError(_("Please set an Intercompany Payable Account on company %s.", self.branch_company_id.name)) + + # 1. Branch Pays HO (Outbound Payment in Branch) + # Dr Intercompany Payable, Cr Bank + branch_payment_method = self.branch_journal_id._get_available_payment_method_lines('outbound').filtered(lambda x: x.code == 'manual')[:1] + + if not branch_payment_method: + branch_payment_method = self.branch_journal_id._get_available_payment_method_lines('outbound')[:1] + + if not branch_payment_method: + raise ValidationError(_("No payment method found for the branch journal.")) + + branch_payment_vals = { + 'payment_type': 'outbound', + 'partner_type': 'supplier', + 'partner_id': self.ho_company_id.partner_id.id, + 'amount': self.amount, + 'currency_id': self.currency_id.id, + 'date': self.date, + 'journal_id': self.branch_journal_id.id, + 'company_id': self.branch_company_id.id, + 'payment_method_line_id': branch_payment_method.id, + 'ref': self.memo, + 'expense_account_id': self.branch_company_id.intercompany_payable_account_id.id, + } + + branch_payment = self.env['account.payment'].sudo().with_company(self.branch_company_id).create(branch_payment_vals) + branch_payment.action_post() + + # 2. HO Receives Payment (Inbound Payment in HO) + # Dr Bank, Cr Intercompany Receivable + ho_payment_method = self.ho_journal_id._get_available_payment_method_lines('inbound').filtered(lambda x: x.code == 'manual')[:1] + + if not ho_payment_method: + ho_payment_method = self.ho_journal_id._get_available_payment_method_lines('inbound')[:1] + + if not ho_payment_method: + raise ValidationError(_("No payment method found for the HO journal.")) + + ho_payment_vals = { + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'partner_id': self.branch_company_id.partner_id.id, + 'amount': self.amount, + 'currency_id': self.currency_id.id, + 'date': self.date, + 'journal_id': self.ho_journal_id.id, + 'company_id': self.ho_company_id.id, + 'payment_method_line_id': ho_payment_method.id, + 'ref': self.memo, + 'expense_account_id': self.ho_company_id.intercompany_receivable_account_id.id, + } + + ho_payment = self.env['account.payment'].create(ho_payment_vals) + ho_payment.action_post() + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Settlement Successful'), + 'message': _('Successfully settled %s from %s to %s.', + self.amount, self.branch_company_id.name, self.ho_company_id.name), + 'sticky': False, + } + } diff --git a/wizard/intercompany_settlement_wizard_views.xml b/wizard/intercompany_settlement_wizard_views.xml new file mode 100644 index 0000000..d812dae --- /dev/null +++ b/wizard/intercompany_settlement_wizard_views.xml @@ -0,0 +1,54 @@ + + + + intercompany.settlement.wizard.form + intercompany.settlement.wizard + + + + + + + + + + + + + + + + + + + + + + + + + Intercompany Settlement + intercompany.settlement.wizard + form + new + + + Record an intercompany settlement payment + + + This will automatically generate both the inbound payment in the Head Office + and the outbound payment in the Branch company. + + + + + +
+ Record an intercompany settlement payment +
+ This will automatically generate both the inbound payment in the Head Office + and the outbound payment in the Branch company. +