feat: add payment wizard to route company-paid expenses to Uang Muka account 118101

This commit is contained in:
Suherdy Yacob 2026-04-30 09:17:04 +07:00
parent 30b7c6a513
commit fce5f0e286
7 changed files with 145 additions and 1 deletions

View File

@ -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 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. - **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. - **Mandatory Receipts**: Prevents submission of employee-paid expenses without attachments.
- **Overdue Tracking**: Highlights missing realization receipts in red/alerts for company-paid expenses. - **Overdue Tracking**: Highlights missing realization receipts in red/alerts for company-paid expenses.

View File

@ -11,6 +11,7 @@
'views/product_views.xml', 'views/product_views.xml',
'views/hr_expense_views.xml', 'views/hr_expense_views.xml',
'views/hr_expense_realization_views.xml', 'views/hr_expense_realization_views.xml',
'views/hr_expense_payment_wizard_views.xml',
'views/hr_expense_kiosk_templates.xml', 'views/hr_expense_kiosk_templates.xml',
'views/res_config_settings_views.xml', 'views/res_config_settings_views.xml',
], ],

View File

@ -7,3 +7,4 @@ from . import hr_expense_realization
from . import account_payment from . import account_payment
from . import res_company from . import res_company
from . import res_config_settings from . import res_config_settings
from . import hr_expense_payment_wizard

View File

@ -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'}

View File

@ -264,3 +264,17 @@ class HrExpenseSheet(models.Model):
""" Public wrapper to allow triggering recompute from a button. """ """ Public wrapper to allow triggering recompute from a button. """
self._compute_state() self._compute_state()
self._compute_from_account_move_ids() 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,
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_expense_payment_wizard_form" model="ir.ui.view">
<field name="name">hr.expense.payment.wizard.form</field>
<field name="model">hr.expense.payment.wizard</field>
<field name="arch" type="xml">
<form string="Register Payment">
<group>
<group>
<field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier'}"/>
<field name="journal_id" options="{'no_create': True}"/>
<field name="payment_method_line_id" options="{'no_create': True, 'no_open': True}"/>
</group>
<group>
<field name="amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="payment_date"/>
<field name="company_id" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="expense_sheet_id" invisible="1"/>
</group>
</group>
<footer>
<button string="Create Payment" name="action_create_payment" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -143,6 +143,18 @@
<xpath expr="//header//button[@name='action_refuse_expense_sheets']" position="attributes"> <xpath expr="//header//button[@name='action_refuse_expense_sheets']" position="attributes">
<attribute name="invisible">state not in ['submit', 'approve', 'post', 'wait_post']</attribute> <attribute name="invisible">state not in ['submit', 'approve', 'post', 'wait_post']</attribute>
</xpath> </xpath>
<xpath expr="//header//button[@name='action_sheet_move_create']" position="attributes">
<attribute name="invisible">state != 'approve' or payment_mode == 'company_account'</attribute>
</xpath>
<xpath expr="//header//button[@name='action_sheet_move_create']" position="after">
<button name="action_open_payment_wizard"
string="Register Payment"
type="object"
data-hotkey="y"
class="oe_highlight o_expense_sheet_post"
invisible="state != 'approve' or payment_mode != 'company_account'"
groups="account.group_account_invoice"/>
</xpath>
<xpath expr="//field[@name='state']" position="attributes"> <xpath expr="//field[@name='state']" position="attributes">
<attribute name="statusbar_visible">draft,submit,approve,post,wait_post,done</attribute> <attribute name="statusbar_visible">draft,submit,approve,post,wait_post,done</attribute>