feat: implement expense realization module with receipt tracking and automated journal entries
This commit is contained in:
parent
7d70bab223
commit
4128ac60e6
@ -6,8 +6,11 @@
|
|||||||
'author': 'Suherdy Yacob',
|
'author': 'Suherdy Yacob',
|
||||||
'depends': ['hr_expense', 'account'],
|
'depends': ['hr_expense', 'account'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/ir_sequence_data.xml',
|
||||||
'views/product_views.xml',
|
'views/product_views.xml',
|
||||||
'views/hr_expense_views.xml',
|
'views/hr_expense_views.xml',
|
||||||
|
'views/hr_expense_realization_views.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
12
data/ir_sequence_data.xml
Normal file
12
data/ir_sequence_data.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="seq_hr_expense_realization" model="ir.sequence">
|
||||||
|
<field name="name">Expense Realization</field>
|
||||||
|
<field name="code">hr.expense.realization</field>
|
||||||
|
<field name="prefix">RLZ/%(year)s/%(month)s/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@ -2,3 +2,4 @@ from . import product_template
|
|||||||
from . import hr_expense
|
from . import hr_expense
|
||||||
from . import hr_expense_sheet
|
from . import hr_expense_sheet
|
||||||
from . import account_move_line
|
from . import account_move_line
|
||||||
|
from . import hr_expense_realization
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo.tools import float_round
|
from odoo.tools import float_round
|
||||||
|
|
||||||
class HrExpense(models.Model):
|
class HrExpense(models.Model):
|
||||||
@ -37,6 +38,46 @@ class HrExpense(models.Model):
|
|||||||
store=True,
|
store=True,
|
||||||
help="True if receipt is not received and past due date."
|
help="True if receipt is not received and past due date."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
realization_ids = fields.One2many('hr.expense.realization', 'expense_id', string='Realizations')
|
||||||
|
realization_count = fields.Integer(string='Realization Count', compute='_compute_realization_count')
|
||||||
|
|
||||||
|
@api.depends('realization_ids')
|
||||||
|
def _compute_realization_count(self):
|
||||||
|
for expense in self:
|
||||||
|
expense.realization_count = len(expense.realization_ids)
|
||||||
|
|
||||||
|
def action_create_realization(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.payment_mode != 'company_account':
|
||||||
|
raise UserError(_("Realization is only for company-paid expenses."))
|
||||||
|
|
||||||
|
# Check if already has a realization
|
||||||
|
if self.realization_count > 0:
|
||||||
|
return self.action_view_realizations()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': _('Create Realization'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'hr.expense.realization',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'context': {
|
||||||
|
'default_expense_id': self.id,
|
||||||
|
'default_employee_id': self.employee_id.id,
|
||||||
|
},
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_view_realizations(self):
|
||||||
|
self.ensure_one()
|
||||||
|
action = self.env["ir.actions.actions"]._for_xml_id("hr_expense_account_split.action_hr_expense_realization")
|
||||||
|
if self.realization_count > 1:
|
||||||
|
action['domain'] = [('expense_id', '=', self.id)]
|
||||||
|
elif self.realization_count == 1:
|
||||||
|
res = self.env['hr.expense.realization'].search([('expense_id', '=', self.id)], limit=1)
|
||||||
|
action['views'] = [(self.env.ref('hr_expense_account_split.hr_expense_realization_view_form').id, 'form')]
|
||||||
|
action['res_id'] = res.id
|
||||||
|
return action
|
||||||
|
|
||||||
@api.depends('receipt_due_date', 'receipt_received')
|
@api.depends('receipt_due_date', 'receipt_received')
|
||||||
def _compute_receipt_overdue(self):
|
def _compute_receipt_overdue(self):
|
||||||
|
|||||||
134
models/hr_expense_realization.py
Normal file
134
models/hr_expense_realization.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
from odoo import api, fields, models, _, Command
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
class HrExpenseRealization(models.Model):
|
||||||
|
_name = 'hr.expense.realization'
|
||||||
|
_description = 'Expense Realization'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
name = fields.Char(string='Reference', required=True, copy=False, default=lambda self: _('New'))
|
||||||
|
expense_id = fields.Many2one(
|
||||||
|
'hr.expense',
|
||||||
|
string='Source Expense',
|
||||||
|
required=True,
|
||||||
|
domain="[('payment_mode', '=', 'company_account')]",
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
employee_id = fields.Many2one('hr.employee', string='Employee', related='expense_id.employee_id', store=True)
|
||||||
|
company_id = fields.Many2one('res.company', string='Company', related='expense_id.company_id', store=True)
|
||||||
|
currency_id = fields.Many2one('res.currency', string='Currency', related='expense_id.currency_id', store=True)
|
||||||
|
date = fields.Date(string='Date', default=fields.Date.context_today, required=True)
|
||||||
|
description = fields.Text(string='Description')
|
||||||
|
|
||||||
|
line_ids = fields.One2many('hr.expense.realization.line', 'realization_id', string='Receipt Lines')
|
||||||
|
total_amount = fields.Monetary(string='Total Amount', compute='_compute_total_amount', store=True, currency_field='currency_id')
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('posted', 'Posted')
|
||||||
|
], string='Status', default='draft', tracking=True)
|
||||||
|
|
||||||
|
default_counterpart_account_id = fields.Many2one('account.account', string='Default Counterpart Account', tracking=True, groups="account.group_account_invoice")
|
||||||
|
journal_id = fields.Many2one('account.journal', string='Journal', tracking=True, groups="account.group_account_invoice", default=lambda self: self.env['account.journal'].search([('name', '=', 'Realisasi')], limit=1))
|
||||||
|
move_id = fields.Many2one('account.move', string='Journal Entry', readonly=True, groups="account.group_account_invoice", help="Reference to the first journal entry created.")
|
||||||
|
|
||||||
|
@api.depends('line_ids.amount')
|
||||||
|
def _compute_total_amount(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_amount = sum(rec.line_ids.mapped('amount'))
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('name', _('New')) == _('New'):
|
||||||
|
vals['name'] = self.env['ir.sequence'].next_by_code('hr.expense.realization') or _('New')
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def action_confirm(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.line_ids:
|
||||||
|
raise UserError(_("Please add at least one receipt line."))
|
||||||
|
self.state = 'confirmed'
|
||||||
|
if self.expense_id:
|
||||||
|
self.expense_id.write({'receipt_received': True})
|
||||||
|
# Explicitly trigger recompute of the sheet status
|
||||||
|
if self.expense_id.sheet_id:
|
||||||
|
self.expense_id.sheet_id._compute_receipt_status()
|
||||||
|
|
||||||
|
def action_apply_default_account(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.default_counterpart_account_id:
|
||||||
|
raise UserError(_("Please set a Default Counterpart Account first."))
|
||||||
|
|
||||||
|
for line in self.line_ids:
|
||||||
|
if not line.counterpart_account_id:
|
||||||
|
line.counterpart_account_id = self.default_counterpart_account_id
|
||||||
|
|
||||||
|
def action_post(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'confirmed':
|
||||||
|
raise UserError(_("Only confirmed realizations can be posted."))
|
||||||
|
if not self.journal_id:
|
||||||
|
raise UserError(_("Please specify the Journal before posting."))
|
||||||
|
|
||||||
|
# Determine the Expense Account
|
||||||
|
product = self.expense_id.product_id.with_company(self.company_id)
|
||||||
|
expense_account = product.property_account_expense_company_id or product.property_account_expense_id
|
||||||
|
if not expense_account:
|
||||||
|
expense_account = self.env['ir.property']._get('property_account_expense_categ_id', 'product.category')
|
||||||
|
|
||||||
|
if not expense_account:
|
||||||
|
raise UserError(_("No expense account found for the product or its category."))
|
||||||
|
|
||||||
|
moves = self.env['account.move']
|
||||||
|
for line in self.line_ids:
|
||||||
|
if not line.counterpart_account_id:
|
||||||
|
raise UserError(_("Please specify a Counterpart Account for the receipt: %s") % line.description)
|
||||||
|
|
||||||
|
move_vals = {
|
||||||
|
'journal_id': self.journal_id.id,
|
||||||
|
'date': self.date,
|
||||||
|
'ref': f"Realization: {self.expense_id.name} - {line.description}",
|
||||||
|
'move_type': 'entry',
|
||||||
|
'line_ids': [
|
||||||
|
Command.create({
|
||||||
|
'name': f"Realization: {self.expense_id.name} ({line.description})",
|
||||||
|
'account_id': expense_account.id,
|
||||||
|
'debit': 0.0,
|
||||||
|
'credit': line.amount,
|
||||||
|
'partner_id': self.employee_id.sudo().work_contact_id.id,
|
||||||
|
'expense_id': self.expense_id.id,
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'name': f"Realization Counterpart: {line.description}",
|
||||||
|
'account_id': line.counterpart_account_id.id,
|
||||||
|
'debit': line.amount,
|
||||||
|
'credit': 0.0,
|
||||||
|
'partner_id': self.employee_id.sudo().work_contact_id.id,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
move = self.env['account.move'].create(move_vals)
|
||||||
|
move.action_post()
|
||||||
|
line.move_id = move.id
|
||||||
|
moves |= move
|
||||||
|
|
||||||
|
self.write({
|
||||||
|
'state': 'posted',
|
||||||
|
'move_id': moves[0].id if moves else False
|
||||||
|
})
|
||||||
|
|
||||||
|
class HrExpenseRealizationLine(models.Model):
|
||||||
|
_name = 'hr.expense.realization.line'
|
||||||
|
_description = 'Expense Realization Line'
|
||||||
|
|
||||||
|
realization_id = fields.Many2one('hr.expense.realization', string='Realization', ondelete='cascade', required=True)
|
||||||
|
currency_id = fields.Many2one('res.currency', related='realization_id.currency_id')
|
||||||
|
description = fields.Char(string='Description', required=True)
|
||||||
|
amount = fields.Monetary(string='Amount', required=True, currency_field='currency_id')
|
||||||
|
attachment_id = fields.Binary(string='Receipt Attachment')
|
||||||
|
attachment_name = fields.Char(string='Attachment Name')
|
||||||
|
counterpart_account_id = fields.Many2one('account.account', string='Counterpart Account', groups="account.group_account_invoice")
|
||||||
|
move_id = fields.Many2one('account.move', string='Journal Entry', readonly=True, groups="account.group_account_invoice")
|
||||||
@ -19,3 +19,20 @@ class HrExpenseSheet(models.Model):
|
|||||||
if not expense.receipt_due_date:
|
if not expense.receipt_due_date:
|
||||||
due_days = expense.product_id.receipt_due_days or 0
|
due_days = expense.product_id.receipt_due_days or 0
|
||||||
expense.receipt_due_date = today + timedelta(days=due_days)
|
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)
|
||||||
|
|
||||||
|
@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'
|
||||||
|
|||||||
4
security/ir.model.access.csv
Normal file
4
security/ir.model.access.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_hr_expense_realization_user,hr.expense.realization,model_hr_expense_realization,hr_expense.group_hr_expense_user,1,1,1,0
|
||||||
|
access_hr_expense_realization_manager,hr.expense.realization,model_hr_expense_realization,hr_expense.group_hr_expense_manager,1,1,1,1
|
||||||
|
access_hr_expense_realization_line_user,hr.expense.realization.line,model_hr_expense_realization_line,hr_expense.group_hr_expense_user,1,1,1,1
|
||||||
|
152
views/hr_expense_realization_views.xml
Normal file
152
views/hr_expense_realization_views.xml
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Realization Tree View -->
|
||||||
|
<record id="hr_expense_realization_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.realization.view.tree</field>
|
||||||
|
<field name="model">hr.expense.realization</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Expense Realization">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="expense_id"/>
|
||||||
|
<field name="total_amount" sum="Total Amount"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-warning="state == 'confirmed'" decoration-success="state == 'posted'"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Realization Form View -->
|
||||||
|
<record id="hr_expense_realization_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.realization.view.form</field>
|
||||||
|
<field name="model">hr.expense.realization</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Expense Realization">
|
||||||
|
<header>
|
||||||
|
<button name="action_confirm" string="Confirm" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_post" string="Post Journal" type="object" class="oe_highlight" groups="account.group_account_invoice" invisible="state != 'confirmed'"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,posted"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" readonly="1"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="expense_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="date" readonly="state == 'posted'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
|
<field name="total_amount"/>
|
||||||
|
<field name="move_id" invisible="not move_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Accounting Information" invisible="state == 'draft'" groups="account.group_account_invoice">
|
||||||
|
<group name="accounting_main">
|
||||||
|
<field name="journal_id" required="state == 'confirmed'" readonly="state == 'posted'"/>
|
||||||
|
<label for="default_counterpart_account_id" string="Counterpart Account"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="default_counterpart_account_id" placeholder="Select Default Account..." readonly="state == 'posted'"/>
|
||||||
|
<button name="action_apply_default_account"
|
||||||
|
type="object"
|
||||||
|
string="Set on Lines"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
invisible="state == 'posted' or not default_counterpart_account_id"/>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<group name="accounting_info">
|
||||||
|
<div class="alert alert-info" role="alert" invisible="state != 'confirmed'">
|
||||||
|
Use the "Set on Lines" button to quickly assign the counterpart account to all receipt lines.
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Receipts" name="receipt_lines">
|
||||||
|
<field name="line_ids" readonly="state == 'posted'">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="amount" sum="Total"/>
|
||||||
|
<field name="attachment_id" filename="attachment_name" widget="binary"/>
|
||||||
|
<field name="attachment_name" column_invisible="1"/>
|
||||||
|
<field name="counterpart_account_id" groups="account.group_account_invoice" required="parent.state == 'confirmed'"/>
|
||||||
|
<field name="move_id" groups="account.group_account_invoice" widget="many2one_clickable" readonly="1" invisible="not move_id"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Notes" name="notes">
|
||||||
|
<field name="description" placeholder="Add some notes..."/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids" widget="mail_followers"/>
|
||||||
|
<field name="activity_ids" widget="mail_activity"/>
|
||||||
|
<field name="message_ids" widget="mail_thread"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Realization Search View -->
|
||||||
|
<record id="hr_expense_realization_view_search" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.realization.view.search</field>
|
||||||
|
<field name="model">hr.expense.realization</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Realization">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="expense_id"/>
|
||||||
|
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
||||||
|
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
|
||||||
|
<filter string="Posted" name="posted" domain="[('state', '=', 'posted')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
|
||||||
|
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Date" name="group_date" context="{'group_by': 'date'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Realization Action -->
|
||||||
|
<record id="action_hr_expense_realization" model="ir.actions.act_window">
|
||||||
|
<field name="name">Realization Report</field>
|
||||||
|
<field name="res_model">hr.expense.realization</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="search_view_id" ref="hr_expense_realization_view_search"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new Realization Report
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Manage employee receipts and their final accounting reconciliation.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu Structure Adjustment -->
|
||||||
|
|
||||||
|
<!-- 1. Create a new parent menu "Expense Reports" -->
|
||||||
|
<menuitem id="menu_expense_reports_parent"
|
||||||
|
name="Expense Reports"
|
||||||
|
parent="hr_expense.menu_hr_expense_root"
|
||||||
|
sequence="2"/>
|
||||||
|
|
||||||
|
<!-- 2. Move the standard "Expense Reports" menu under it and rename it to "Expenses" -->
|
||||||
|
<menuitem id="hr_expense.menu_hr_expense_report"
|
||||||
|
name="Expenses"
|
||||||
|
parent="menu_expense_reports_parent"
|
||||||
|
sequence="1"/>
|
||||||
|
|
||||||
|
<!-- 3. Add the "Realization Report" submenu under it -->
|
||||||
|
<menuitem id="menu_hr_expense_realization"
|
||||||
|
name="Realization Report"
|
||||||
|
parent="menu_expense_reports_parent"
|
||||||
|
action="action_hr_expense_realization"
|
||||||
|
sequence="2"/>
|
||||||
|
</odoo>
|
||||||
@ -5,17 +5,65 @@
|
|||||||
<field name="name">hr.expense.view.form.receipt</field>
|
<field name="name">hr.expense.view.form.receipt</field>
|
||||||
<field name="model">hr.expense</field>
|
<field name="model">hr.expense</field>
|
||||||
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
|
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
|
||||||
|
<field name="priority">1000</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='account_id']" position="after">
|
<!-- Remove all standard Submit Buttons -->
|
||||||
<field name="receipt_due_date" widget="date" invisible="not receipt_due_date"/>
|
<xpath expr="(//header//button[@name='action_submit_expenses'])[1]" position="replace"/>
|
||||||
<field name="receipt_received" widget="boolean_toggle" invisible="not receipt_due_date"/>
|
<xpath expr="(//header//button[@name='action_submit_expenses'])[1]" position="replace"/>
|
||||||
<field name="receipt_overdue" invisible="1"/>
|
|
||||||
|
<!-- Remove all standard Attach Receipt widgets -->
|
||||||
|
<xpath expr="(//header//widget[@name='attach_document'])[1]" position="replace"/>
|
||||||
|
<xpath expr="(//header//widget[@name='attach_document'])[1]" position="replace"/>
|
||||||
|
|
||||||
|
<!-- Remove Split Expense to re-add with correct logic -->
|
||||||
|
<xpath expr="//header//button[@name='action_split_wizard']" position="replace"/>
|
||||||
|
|
||||||
|
<!-- Re-add ONLY one Create Report button as primary -->
|
||||||
|
<xpath expr="//header" position="inside">
|
||||||
|
<button name="action_submit_expenses"
|
||||||
|
string="Create Report"
|
||||||
|
type="object"
|
||||||
|
class="oe_highlight o_expense_submit"
|
||||||
|
invisible="sheet_id"
|
||||||
|
data-hotkey="v"/>
|
||||||
|
<!-- Re-add Attach Receipt but hide it on new records -->
|
||||||
|
<widget name="attach_document"
|
||||||
|
string="Attach Receipt"
|
||||||
|
action="attach_document"
|
||||||
|
highlight="nb_attachment < 1"
|
||||||
|
invisible="not id or sheet_id"/>
|
||||||
|
<!-- Re-add Split Expense with correct logic -->
|
||||||
|
<button name="action_split_wizard" string="Split Expense" type="object" invisible="not id or sheet_id or product_has_cost"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[hasclass('oe_title')]" position="before">
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_view_realizations"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-file-text-o"
|
||||||
|
invisible="realization_count == 0">
|
||||||
|
<field name="realization_count" widget="statinfo" string="Realization"/>
|
||||||
|
</button>
|
||||||
|
<!-- Button to create new realization if none exists -->
|
||||||
|
<button name="action_create_realization"
|
||||||
|
string="Realization"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-plus-square-o"
|
||||||
|
invisible="payment_mode != 'company_account' or state != 'done' or realization_count != 0"/>
|
||||||
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//sheet" position="before">
|
<xpath expr="//sheet" position="before">
|
||||||
<div class="alert alert-danger mb-2" role="alert" invisible="not receipt_overdue">
|
<div class="alert alert-danger mb-2" role="alert" invisible="not receipt_overdue">
|
||||||
This receipt is <strong>Overdue</strong>! Please submit the original receipt as soon as possible.
|
This receipt is <strong>Overdue</strong>! Please submit the original receipt as soon as possible.
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='account_id']" position="after">
|
||||||
|
<field name="realization_count" invisible="1"/>
|
||||||
|
<field name="receipt_due_date" widget="date" invisible="not receipt_due_date"/>
|
||||||
|
<field name="receipt_received" widget="boolean_toggle" invisible="not receipt_due_date"/>
|
||||||
|
<field name="receipt_overdue" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
@ -73,4 +121,72 @@
|
|||||||
parent="hr_expense.menu_hr_expense_reports"
|
parent="hr_expense.menu_hr_expense_reports"
|
||||||
action="action_hr_expense_overdue_receipts"
|
action="action_hr_expense_overdue_receipts"
|
||||||
sequence="20"/>
|
sequence="20"/>
|
||||||
|
<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">
|
||||||
|
<!-- Restrict Posting to Accountants -->
|
||||||
|
<xpath expr="//button[@name='action_sheet_move_create']" position="attributes">
|
||||||
|
<attribute name="groups">account.group_account_invoice</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//button[@name='action_register_payment']" position="attributes">
|
||||||
|
<attribute name="groups">account.group_account_invoice</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//field[@name='expense_line_ids']/tree/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', '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', 'done'] or realization_count == 0"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Inherit 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="before">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Inherit 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="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>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user