From fce5f0e2867262b2f8eccc94449584b6f84b3694 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 30 Apr 2026 09:17:04 +0700 Subject: [PATCH] feat: add payment wizard to route company-paid expenses to Uang Muka account 118101 --- README.md | 7 +- __manifest__.py | 1 + models/__init__.py | 1 + models/hr_expense_payment_wizard.py | 82 +++++++++++++++++++++++ models/hr_expense_sheet.py | 14 ++++ views/hr_expense_payment_wizard_views.xml | 29 ++++++++ views/hr_expense_views.xml | 12 ++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 models/hr_expense_payment_wizard.py create mode 100644 views/hr_expense_payment_wizard_views.xml diff --git a/README.md b/README.md index 7b39e4b..a6a6668 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,12 @@ This module enhances Odoo's standard Expense workflow by providing account-split - **Create Vendor Payment**: One-click button on the realization form to pay the employee the difference. - **Create Customer Payment**: One-click button on the realization form to record the employee returning the excess funds. -### 6. Validation & Security +### 6. Custom Payment Wizard +- **Company Advances**: Replaces the standard Odoo "Post Journal Entries" workflow for company-paid expenses with a custom "Register Payment" wizard. +- **Vendor & Date Selection**: Allows the finance team to specify the exact vendor and payment date during the advance payment. +- **Automated Uang Muka Routing**: The wizard automatically forces the payment debit to `118101 Uang Muka Operasional`, ensuring advances are correctly booked as prepaid expenses before receipts are realized. + +### 7. Validation & Security - **Mandatory Receipts**: Prevents submission of employee-paid expenses without attachments. - **Overdue Tracking**: Highlights missing realization receipts in red/alerts for company-paid expenses. diff --git a/__manifest__.py b/__manifest__.py index c5e25a1..9701e17 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -11,6 +11,7 @@ 'views/product_views.xml', 'views/hr_expense_views.xml', 'views/hr_expense_realization_views.xml', + 'views/hr_expense_payment_wizard_views.xml', 'views/hr_expense_kiosk_templates.xml', 'views/res_config_settings_views.xml', ], diff --git a/models/__init__.py b/models/__init__.py index 4b9255e..dca7fdb 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -7,3 +7,4 @@ from . import hr_expense_realization from . import account_payment from . import res_company from . import res_config_settings +from . import hr_expense_payment_wizard diff --git a/models/hr_expense_payment_wizard.py b/models/hr_expense_payment_wizard.py new file mode 100644 index 0000000..72dd7bb --- /dev/null +++ b/models/hr_expense_payment_wizard.py @@ -0,0 +1,82 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class HrExpensePaymentWizard(models.TransientModel): + _name = 'hr.expense.payment.wizard' + _description = 'Expense Payment Wizard' + + expense_sheet_id = fields.Many2one('hr.expense.sheet', required=True) + amount = fields.Monetary(string='Payment Amount', required=True, readonly=True) + currency_id = fields.Many2one('res.currency', related='expense_sheet_id.currency_id') + company_id = fields.Many2one('res.company', related='expense_sheet_id.company_id') + + partner_id = fields.Many2one( + 'res.partner', + string='Vendor', + required=True, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]" + ) + journal_id = fields.Many2one( + 'account.journal', + string='Payment Journal', + required=True, + domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', company_id)]" + ) + payment_method_line_id = fields.Many2one( + 'account.payment.method.line', + string='Payment Method', + required=True, + domain="[('journal_id', '=', journal_id), ('payment_type', '=', 'outbound')]" + ) + payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True) + + @api.onchange('journal_id') + def _onchange_journal_id(self): + if self.journal_id: + available_payment_methods = self.journal_id.outbound_payment_method_line_ids + if available_payment_methods: + self.payment_method_line_id = available_payment_methods[0].id + else: + self.payment_method_line_id = False + + def action_create_payment(self): + self.ensure_one() + + # Find 118101 account + uang_muka_account = self.env['account.account'].search([ + ('code', '=', '118101'), + ('company_id', '=', self.company_id.id) + ], limit=1) + + if not uang_muka_account: + raise UserError(_("Account 118101 Uang Muka Operasional not found for this company!")) + + payment_vals = { + 'date': self.payment_date, + 'amount': self.amount, + 'payment_type': 'outbound', + 'partner_type': 'supplier', + 'partner_id': self.partner_id.id, + 'journal_id': self.journal_id.id, + 'currency_id': self.currency_id.id, + 'payment_method_line_id': self.payment_method_line_id.id, + 'ref': self.expense_sheet_id.name, + 'destination_account_id': uang_muka_account.id, + } + + payment = self.env['account.payment'].create(payment_vals) + payment.action_post() + + # Link the payment's move to the expense sheet + payment.move_id.write({'expense_sheet_id': self.expense_sheet_id.id}) + + # Update sheet status to Paid + self.expense_sheet_id.write({ + 'state': 'done', + 'amount_residual': 0.0, + 'payment_state': 'paid' + }) + # Our custom logic will push it to 'wait_post' if realization is pending + self.expense_sheet_id.action_recompute_state() + + return {'type': 'ir.actions.act_window_close'} diff --git a/models/hr_expense_sheet.py b/models/hr_expense_sheet.py index 0d13bd6..eb97e75 100644 --- a/models/hr_expense_sheet.py +++ b/models/hr_expense_sheet.py @@ -264,3 +264,17 @@ class HrExpenseSheet(models.Model): """ Public wrapper to allow triggering recompute from a button. """ self._compute_state() self._compute_from_account_move_ids() + + def action_open_payment_wizard(self): + self.ensure_one() + return { + 'name': _('Register Payment'), + 'type': 'ir.actions.act_window', + 'res_model': 'hr.expense.payment.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_expense_sheet_id': self.id, + 'default_amount': self.total_amount, + } + } diff --git a/views/hr_expense_payment_wizard_views.xml b/views/hr_expense_payment_wizard_views.xml new file mode 100644 index 0000000..bef2c48 --- /dev/null +++ b/views/hr_expense_payment_wizard_views.xml @@ -0,0 +1,29 @@ + + + + hr.expense.payment.wizard.form + hr.expense.payment.wizard + +
+ + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/views/hr_expense_views.xml b/views/hr_expense_views.xml index cee25a8..17f3f18 100644 --- a/views/hr_expense_views.xml +++ b/views/hr_expense_views.xml @@ -143,6 +143,18 @@ state not in ['submit', 'approve', 'post', 'wait_post'] + + state != 'approve' or payment_mode == 'company_account' + + +