feat: add payment registration wizard for expense sheets and introduce wait_post state for company account reconciliations
This commit is contained in:
parent
58db2572a4
commit
f2a4c72101
@ -50,4 +50,4 @@ This module enhances Odoo's standard Expense workflow by providing account-split
|
||||
## 📋 Technical Notes
|
||||
- **Controller**: `/hr_expense/kiosk/<token>`
|
||||
- **Models**: `hr.expense`, `hr.expense.sheet`, `hr.expense.realization`, `account.payment`
|
||||
- **JS Framework**: Odoo 17 OWL
|
||||
- **JS Framework**: Odoo 19 OWL
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
'data/ir_sequence_data.xml',
|
||||
'views/product_views.xml',
|
||||
'views/hr_expense_views.xml',
|
||||
'views/hr_expense_payment_wizard_views.xml',
|
||||
'views/hr_expense_realization_views.xml',
|
||||
'views/hr_expense_kiosk_templates.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from . import product_template
|
||||
from . import hr_expense
|
||||
from . import hr_expense_sheet
|
||||
from . import hr_expense_payment_wizard
|
||||
from . import account_move_line
|
||||
from . import account_move
|
||||
from . import hr_expense_realization
|
||||
|
||||
@ -128,7 +128,7 @@ class AccountPayment(models.Model):
|
||||
Override _seek_for_lines to explicitly force the destination_account_id
|
||||
to be recognized as the counterpart_line.
|
||||
|
||||
Native Odoo sometimes misclassifies the advance/expense account (e.g. 118101)
|
||||
Native Odoo sometimes misclassifies the advance/expense account (e.g. 115101)
|
||||
as a write-off line if it's not strictly a receivable/payable account type.
|
||||
Simultaneously, if a tax deduction account (e.g. 217103 PPh 23) is a payable account type,
|
||||
Odoo erroneously classifies the deduction as the counterpart.
|
||||
|
||||
82
models/hr_expense_payment_wizard.py
Normal file
82
models/hr_expense_payment_wizard.py
Normal 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 115101 account (Uang Muka Operasional)
|
||||
uang_muka_account = self.env['account.account'].search([
|
||||
('code', '=', '115101'),
|
||||
('company_id', '=', self.company_id.id)
|
||||
], limit=1)
|
||||
|
||||
if not uang_muka_account:
|
||||
raise UserError(_("Account 115101 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,
|
||||
'expense_ids': [Command.set(self.expense_sheet_id.expense_line_ids.ids)],
|
||||
}
|
||||
|
||||
payment = self.env['account.payment'].create(payment_vals)
|
||||
payment.action_post()
|
||||
|
||||
# Link the payment's move to the expense sheet (if field exists, standard Odoo usually has it)
|
||||
if hasattr(payment.move_id, 'expense_sheet_id'):
|
||||
payment.move_id.write({'expense_sheet_id': self.expense_sheet_id.id})
|
||||
|
||||
# Update sheet status to Paid
|
||||
self.expense_sheet_id.write({
|
||||
'state': 'done',
|
||||
})
|
||||
# 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'}
|
||||
@ -108,7 +108,7 @@ class HrExpenseRealization(models.Model):
|
||||
raise UserError(_("Please specify the Journal before posting."))
|
||||
|
||||
# 1. Determine accounts
|
||||
# Advance Account (Product's expense account, e.g. 118101)
|
||||
# Advance Account (Product's expense account, e.g. 115101)
|
||||
product = self.expense_id.product_id.with_company(self.company_id)
|
||||
advance_account = product.property_account_expense_company_id or product.property_account_expense_id
|
||||
if not advance_account:
|
||||
|
||||
269
models/hr_expense_sheet.py
Normal file
269
models/hr_expense_sheet.py
Normal file
@ -0,0 +1,269 @@
|
||||
from odoo import api, fields, models, _, Command
|
||||
from datetime import timedelta
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
class HrExpenseSheet(models.Model):
|
||||
_inherit = 'hr.expense.sheet'
|
||||
|
||||
state = fields.Selection(selection_add=[
|
||||
('wait_post', 'Wait Post')
|
||||
], ondelete={'wait_post': 'set default'})
|
||||
|
||||
@api.depends('account_move_ids.payment_state', 'account_move_ids.amount_residual', 'account_move_ids.state', 'expense_line_ids.receipt_received', 'expense_line_ids.realization_ids.state')
|
||||
def _compute_state(self):
|
||||
# Store original states to detect transition to 'done'
|
||||
original_states = {sheet.id: sheet.state for sheet in self}
|
||||
|
||||
super()._compute_state()
|
||||
|
||||
for sheet in self:
|
||||
# FIX: If we have moves but they are ALL canceled/draft, Odoo super() incorrectly sets state='post' or 'done'.
|
||||
# We must force it back to approval_state (Approved) or draft.
|
||||
active_moves = sheet.account_move_ids.filtered(lambda m: m.state == 'posted')
|
||||
if not active_moves:
|
||||
if sheet.state in ('post', 'done', 'wait_post'):
|
||||
# Odoo 19 uses 'approve' instead of 'approval_state' field often, but let's be safe
|
||||
sheet.state = 'approve' if sheet.state != 'cancel' else 'cancel'
|
||||
|
||||
# Check for Company Account expenses
|
||||
company_paid = sheet.expense_line_ids.filtered(lambda e: e.payment_mode == 'company_account')
|
||||
|
||||
if company_paid:
|
||||
# If Odoo thought it was 'done' (fully or partially paid/in_payment),
|
||||
# we may need to hold it at 'wait_post' until realization is complete.
|
||||
if sheet.state == 'done':
|
||||
realizations = company_paid.mapped('realization_ids')
|
||||
has_posted_realization = realizations and all(r.state == 'posted' for r in realizations)
|
||||
|
||||
if sheet.payment_state in ('paid', 'in_payment'):
|
||||
if sheet.receipt_status != 'received' or not has_posted_realization:
|
||||
sheet.state = 'wait_post'
|
||||
else:
|
||||
# If not paid, it should drop back
|
||||
sheet.state = 'approve'
|
||||
|
||||
if original_states.get(sheet.id) != 'done' and sheet.state == 'done':
|
||||
# Transitioned to 'Paid/Done'
|
||||
today = fields.Date.today()
|
||||
for expense in sheet.expense_line_ids:
|
||||
if not expense.receipt_due_date:
|
||||
due_days = expense.product_id.receipt_due_days or 0
|
||||
expense.receipt_due_date = today + timedelta(days=due_days)
|
||||
|
||||
receipt_status = fields.Selection([
|
||||
('pending', 'Pending Receipts'),
|
||||
('received', 'Receipts Received'),
|
||||
('none', 'No Receipt Required')
|
||||
], string='Receipt Status', compute='_compute_receipt_status', store=True, tracking=True)
|
||||
|
||||
realization_total_amount = fields.Monetary(
|
||||
string='Realization Total',
|
||||
compute='_compute_realization_total_amount',
|
||||
currency_field='currency_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
expense_sequences = fields.Char(
|
||||
string='Expense Sequences',
|
||||
compute='_compute_expense_sequences',
|
||||
store=True,
|
||||
help="Concatenated sequences of all expenses in this report."
|
||||
)
|
||||
|
||||
amount_paid = fields.Monetary(
|
||||
string='Payment Total',
|
||||
compute='_compute_amount_paid',
|
||||
currency_field='currency_id',
|
||||
store=True,
|
||||
help="Total amount paid by the finance team (Total - Residual)."
|
||||
)
|
||||
|
||||
@api.depends('account_move_ids.line_ids.matched_debit_ids', 'account_move_ids.line_ids.matched_credit_ids', 'total_amount', 'amount_residual')
|
||||
def _compute_amount_paid(self):
|
||||
for sheet in self:
|
||||
total_paid = 0.0
|
||||
seen_statement_line_ids = set()
|
||||
seen_payment_ids = set()
|
||||
seen_move_line_ids = set()
|
||||
|
||||
# Find all relevant lines of the sheet's moves (Payable or Clearing accounts)
|
||||
reconcilable_lines = sheet.account_move_ids.line_ids.filtered(
|
||||
lambda l: l.account_id.account_type in ('asset_receivable', 'liability_payable') or
|
||||
l.account_id.reconcile
|
||||
)
|
||||
|
||||
for line in reconcilable_lines:
|
||||
# Get the counterpart lines from partial reconciliations
|
||||
partials = line.matched_debit_ids | line.matched_credit_ids
|
||||
for partial in partials:
|
||||
counterpart = partial.debit_move_id if partial.credit_move_id == line else partial.credit_move_id
|
||||
|
||||
# If the counterpart is from a Bank/Cash journal, it's a "Payment"
|
||||
if counterpart.journal_id.type in ('bank', 'cash'):
|
||||
st_line = counterpart.move_id.statement_line_id
|
||||
payment = counterpart.payment_id
|
||||
|
||||
if st_line:
|
||||
if st_line.id not in seen_statement_line_ids:
|
||||
total_paid += abs(st_line.amount)
|
||||
seen_statement_line_ids.add(st_line.id)
|
||||
elif payment:
|
||||
if payment.id not in seen_payment_ids:
|
||||
total_paid += payment.amount
|
||||
seen_payment_ids.add(payment.id)
|
||||
else:
|
||||
# Fallback to the specific move line's balance (absolute)
|
||||
if counterpart.id not in seen_move_line_ids:
|
||||
total_paid += abs(counterpart.balance)
|
||||
seen_move_line_ids.add(counterpart.id)
|
||||
|
||||
# If no bank transactions found but report is clearly paid/partially paid,
|
||||
# fall back to standard calculation for non-bank flows (e.g. manual journal reconciliation)
|
||||
if not total_paid and sheet.total_amount != sheet.amount_residual:
|
||||
total_paid = sheet.total_amount - sheet.amount_residual
|
||||
|
||||
sheet.amount_paid = total_paid
|
||||
|
||||
@api.depends('expense_line_ids.realization_total_amount')
|
||||
def _compute_realization_total_amount(self):
|
||||
for sheet in self:
|
||||
sheet.realization_total_amount = sum(sheet.expense_line_ids.mapped('realization_total_amount'))
|
||||
|
||||
@api.depends('expense_line_ids.sequence_name')
|
||||
def _compute_expense_sequences(self):
|
||||
for sheet in self:
|
||||
sequences = sheet.expense_line_ids.mapped('sequence_name')
|
||||
# Filter out false values and joins them
|
||||
sheet.expense_sequences = ", ".join(filter(None, sequences))
|
||||
|
||||
@api.depends('expense_line_ids.receipt_received', 'expense_line_ids.payment_mode')
|
||||
def _compute_receipt_status(self):
|
||||
for sheet in self:
|
||||
company_paid_expenses = sheet.expense_line_ids.filtered(lambda e: e.payment_mode == 'company_account')
|
||||
if not company_paid_expenses:
|
||||
sheet.receipt_status = 'none'
|
||||
elif all(e.receipt_received for e in company_paid_expenses):
|
||||
sheet.receipt_status = 'received'
|
||||
else:
|
||||
sheet.receipt_status = 'pending'
|
||||
|
||||
@api.depends('account_move_ids.payment_state', 'account_move_ids.amount_residual', 'account_move_ids.state', 'account_move_ids.payment_id.is_matched')
|
||||
def _compute_from_account_move_ids(self):
|
||||
"""
|
||||
Overriding to fix the 'IN PAYMENT' ribbon issue.
|
||||
Standard Odoo assumes 'paid' if any move exists for company_account.
|
||||
We check if the moves are actually in 'posted' state.
|
||||
"""
|
||||
for sheet in self:
|
||||
if sheet.payment_mode == 'company_account':
|
||||
if sheet.account_move_ids:
|
||||
# Filter for moves that are NOT canceled
|
||||
active_moves = sheet.account_move_ids.filtered(lambda m: m.state == 'posted')
|
||||
if active_moves:
|
||||
# If there are active moves that are not reversed
|
||||
moves = active_moves - active_moves.filtered('reversal_move_id')
|
||||
if moves:
|
||||
payments = moves.mapped('payment_id')
|
||||
unmatched_payments = payments.filtered(lambda p: not p.is_matched)
|
||||
|
||||
if unmatched_payments:
|
||||
sheet.payment_state = 'in_payment'
|
||||
else:
|
||||
sheet.payment_state = 'paid'
|
||||
sheet.amount_residual = 0.
|
||||
else:
|
||||
sheet.payment_state = 'reversed'
|
||||
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
|
||||
else:
|
||||
# Moves exist but none are 'posted' (e.g. they are all 'cancel' or 'draft')
|
||||
sheet.payment_state = 'not_paid'
|
||||
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
|
||||
else:
|
||||
sheet.payment_state = 'not_paid'
|
||||
sheet.amount_residual = 0.0
|
||||
else:
|
||||
# Standard Odoo logic for own_account
|
||||
if sheet.account_move_ids:
|
||||
sheet.amount_residual = sum(sheet.account_move_ids.mapped('amount_residual'))
|
||||
sheet.payment_state = sheet.account_move_ids[:1].payment_state
|
||||
else:
|
||||
sheet.amount_residual = 0.0
|
||||
sheet.payment_state = 'not_paid'
|
||||
|
||||
def _do_refuse(self, reason):
|
||||
"""
|
||||
Bypass the standard Odoo lock: 'You cannot cancel an expense sheet linked to a journal entry'.
|
||||
We allow it but we'll try to cancel the moves first.
|
||||
"""
|
||||
self._do_reverse_moves()
|
||||
return super()._do_refuse(reason)
|
||||
|
||||
def _do_reverse_moves(self):
|
||||
"""
|
||||
Overriding to handle account.payment explicitly.
|
||||
"""
|
||||
self = self.with_context(clean_context(self.env.context))
|
||||
if self.account_move_ids:
|
||||
for sheet in self:
|
||||
# Handle payments linked to this sheet
|
||||
payments = sheet.account_move_ids.mapped('payment_id')
|
||||
if payments:
|
||||
# Cancel the payments directly
|
||||
for payment in payments:
|
||||
if payment.state == 'posted':
|
||||
payment.action_cancel()
|
||||
elif payment.state == 'draft':
|
||||
payment.action_cancel()
|
||||
|
||||
# Standard reversal for non-payment moves (if any)
|
||||
non_payment_moves = sheet.account_move_ids.filtered(lambda m: not m.payment_id)
|
||||
if non_payment_moves:
|
||||
non_payment_moves._reverse_moves(
|
||||
default_values_list=[{'invoice_date': fields.Date.context_today(move), 'ref': False} for move in non_payment_moves],
|
||||
cancel=True
|
||||
)
|
||||
|
||||
# Unlink draft/canceled moves (including payment moves that are now draft/cancel)
|
||||
sheet.account_move_ids.filtered(lambda m: m.state in ('draft', 'cancel')).unlink()
|
||||
|
||||
def action_reset_expense_sheets(self):
|
||||
""" Overriding reset to handle realizations. """
|
||||
for sheet in self:
|
||||
realizations = sheet.expense_line_ids.mapped('realization_ids')
|
||||
posted_realizations = realizations.filtered(lambda r: r.state == 'posted')
|
||||
if posted_realizations:
|
||||
raise UserError(_("You cannot reset this report because it has one or more Posted Realizations (%s). Please reverse or cancel the realization journal entries first.") % ", ".join(posted_realizations.mapped('name')))
|
||||
|
||||
# Reset draft/confirmed ones back to draft if resetting the sheet
|
||||
realizations.filtered(lambda r: r.state != 'posted').write({'state': 'draft'})
|
||||
|
||||
return super().action_reset_expense_sheets()
|
||||
|
||||
def action_refuse_expense_sheets(self):
|
||||
""" Handle realizations on refusal as well. """
|
||||
for sheet in self:
|
||||
realizations = sheet.expense_line_ids.mapped('realization_ids')
|
||||
if realizations.filtered(lambda r: r.state == 'posted'):
|
||||
raise UserError(_("You cannot refuse this report because it has Posted Realizations. Revert them first."))
|
||||
realizations.write({'state': 'draft'})
|
||||
return super().action_refuse_expense_sheets()
|
||||
|
||||
def action_recompute_state(self):
|
||||
""" 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,
|
||||
}
|
||||
}
|
||||
38
views/hr_expense_payment_wizard_views.xml
Normal file
38
views/hr_expense_payment_wizard_views.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_expense_payment_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.expense.payment.wizard.view.form</field>
|
||||
<field name="model">hr.expense.payment.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Register Payment">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="expense_sheet_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'supplier'}"/>
|
||||
<field name="journal_id" widget="selection"/>
|
||||
<field name="payment_method_line_id" widget="selection"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="payment_date"/>
|
||||
<field name="amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_create_payment" string="Create Payment" type="object" class="oe_highlight" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="x"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_expense_payment_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Register Payment</field>
|
||||
<field name="res_model">hr.expense.payment.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@ -143,7 +143,7 @@
|
||||
action="action_hr_expense_overdue_receipts"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Update Actions to include Wait Post in SearchPanel -->
|
||||
<!-- Update My Reports Action to include Wait Post in SearchPanel if applicable -->
|
||||
<record id="hr_expense.hr_expense_actions_my_all" model="ir.actions.act_window">
|
||||
<field name="context">{ 'searchpanel_default_state': ["draft", "submitted", "approved", "posted", "wait_post", "paid"], 'search_default_my_open_expenses': 1 }</field>
|
||||
</record>
|
||||
@ -151,4 +151,116 @@
|
||||
<record id="hr_expense.hr_expense_actions_all" model="ir.actions.act_window">
|
||||
<field name="context">{ 'searchpanel_default_state': ["draft", "submitted", "approved", "posted", "wait_post", "paid"] }</field>
|
||||
</record>
|
||||
|
||||
<!-- Expense Report (Sheet) Form View -->
|
||||
<record id="view_hr_expense_sheet_form_inherit_realization" model="ir.ui.view">
|
||||
<field name="name">hr.expense.sheet.form.inherit.realization</field>
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Update standard Reset to Draft and Cancel buttons to include 'wait_post' -->
|
||||
<xpath expr="//header//button[@name='action_reset_expense_sheets']" position="attributes">
|
||||
<attribute name="invisible">state not in ['approve', 'post', 'done', 'cancel', 'wait_post']</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//header//button[@name='action_refuse_expense_sheets']" position="attributes">
|
||||
<attribute name="invisible">state not in ['submit', 'approve', 'post', 'wait_post']</attribute>
|
||||
</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">
|
||||
<attribute name="statusbar_visible">draft,submit,approve,post,wait_post,done</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_recompute_state"
|
||||
string="Recompute Status"
|
||||
type="object"
|
||||
groups="base.group_erp_manager"
|
||||
class="btn btn-secondary"
|
||||
help="Force refresh the report status based on current payments and receipts."/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='expense_line_ids']/list/field[@name='name']" position="before">
|
||||
<field name="sequence_name" column_invisible="not parent.id"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='expense_line_ids']/list/field[@name='total_amount']" position="after">
|
||||
<field name="realization_total_amount" optional="show" invisible="payment_mode != 'company_account'"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='expense_line_ids']/list/field[@name='name']" position="after">
|
||||
<field name="payment_mode" column_invisible="True"/>
|
||||
<field name="realization_count" column_invisible="True"/>
|
||||
<!-- Button to Add Receipt if none exists -->
|
||||
<button name="action_create_realization"
|
||||
string="Add Receipt"
|
||||
type="object"
|
||||
icon="fa-plus"
|
||||
class="text-primary"
|
||||
title="Add receipts for this expense"
|
||||
invisible="payment_mode != 'company_account' or parent.state not in ['post', 'wait_post', 'done'] or realization_count != 0"/>
|
||||
<!-- Button to View Receipts if already exist -->
|
||||
<button name="action_view_realizations"
|
||||
string="View Receipts"
|
||||
type="object"
|
||||
icon="fa-external-link"
|
||||
class="text-success"
|
||||
title="View linked receipts"
|
||||
invisible="payment_mode != 'company_account' or parent.state not in ['post', 'wait_post', 'done'] or realization_count == 0"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Expense Report Tree View -->
|
||||
<record id="view_hr_expense_sheet_tree_inherit_receipt" model="ir.ui.view">
|
||||
<field name="name">hr.expense.sheet.tree.receipt</field>
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='state']" position="attributes">
|
||||
<attribute name="decoration-warning">state == 'wait_post'</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='state']" position="before">
|
||||
<field name="expense_sequences" optional="show"/>
|
||||
<field name="amount_paid" optional="show" sum="Total Payment" string="Payment Total"/>
|
||||
<field name="realization_total_amount" optional="show" sum="Total Realization"/>
|
||||
<field name="receipt_status" widget="badge"
|
||||
decoration-info="receipt_status == 'pending'"
|
||||
decoration-success="receipt_status == 'received'"
|
||||
decoration-muted="receipt_status == 'none'"
|
||||
optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Expense Report Search View -->
|
||||
<record id="hr_expense_sheet_view_search_inherit_receipt" model="ir.ui.view">
|
||||
<field name="name">hr.expense.sheet.search.receipt</field>
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="inherit_id" ref="hr_expense.hr_expense_sheet_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='my_reports']" position="after">
|
||||
<separator/>
|
||||
<filter string="Wait Post" name="filter_wait_post" domain="[('state', '=', 'wait_post')]"/>
|
||||
<filter string="Pending Receipts" name="filter_receipt_pending" domain="[('receipt_status', '=', 'pending')]"/>
|
||||
<filter string="Receipts Received" name="filter_receipt_received" domain="[('receipt_status', '=', 'received')]"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='group_filters']" position="inside">
|
||||
<filter string="Receipt Status" name="group_receipt_status" context="{'group_by': 'receipt_status'}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Update All Reports Action to include Wait Post in SearchPanel -->
|
||||
<record id="hr_expense.action_hr_expense_sheet_all" model="ir.actions.act_window">
|
||||
<field name="context">{ 'searchpanel_default_state': ["draft", "submit", "approve", "post", "wait_post", "done"] }</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user