first commit
This commit is contained in:
commit
7d70bab223
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*~
|
||||||
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# HR Expense Account Split
|
||||||
|
|
||||||
|
This module allows configuring separate expense accounts for expenses based on their payment mode (Employee vs. Company).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Adds `Expense Account (Employee)` to Expense Categories.
|
||||||
|
- Adds `Expense Account (Company)` to Expense Categories.
|
||||||
|
- Automatically selects the correct account when creating an expense based on the chosen payment mode.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
1. Go to **Expenses > Configuration > Expense Categories**.
|
||||||
|
2. Open an expense category.
|
||||||
|
3. In the **Accounting** section, set the specific accounts for Employee and Company modes.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
15
__manifest__.py
Normal file
15
__manifest__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
'name': 'HR Expense: Split Account by Payment Mode',
|
||||||
|
'version': '17.0.1.0.0',
|
||||||
|
'summary': 'Set different expense accounts for Employee paid vs Company paid expenses on Expense Categories.',
|
||||||
|
'category': 'Human Resources/Expenses',
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'depends': ['hr_expense', 'account'],
|
||||||
|
'data': [
|
||||||
|
'views/product_views.xml',
|
||||||
|
'views/hr_expense_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import product_template
|
||||||
|
from . import hr_expense
|
||||||
|
from . import hr_expense_sheet
|
||||||
|
from . import account_move_line
|
||||||
22
models/account_move_line.py
Normal file
22
models/account_move_line.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from odoo import models, api
|
||||||
|
from odoo.tools import float_round
|
||||||
|
|
||||||
|
class AccountMoveLine(models.Model):
|
||||||
|
_inherit = 'account.move.line'
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('expense_id') and vals.get('price_unit'):
|
||||||
|
# Force rounding on expense-related lines to prevent noise from 10-decimal precision
|
||||||
|
currency = self.env['res.currency'].browse(vals.get('currency_id')) or self.env.company.currency_id
|
||||||
|
vals['price_unit'] = float_round(vals['price_unit'], precision_digits=currency.decimal_places or 2)
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'price_unit' in vals:
|
||||||
|
for line in self:
|
||||||
|
if line.expense_id:
|
||||||
|
currency = line.currency_id or self.env.company.currency_id
|
||||||
|
vals['price_unit'] = float_round(vals['price_unit'], precision_digits=currency.decimal_places or 2)
|
||||||
|
return super().write(vals)
|
||||||
56
models/hr_expense.py
Normal file
56
models/hr_expense.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.tools import float_round
|
||||||
|
|
||||||
|
class HrExpense(models.Model):
|
||||||
|
_inherit = 'hr.expense'
|
||||||
|
|
||||||
|
@api.depends('product_id', 'company_id', 'payment_mode')
|
||||||
|
def _compute_account_id(self):
|
||||||
|
super()._compute_account_id()
|
||||||
|
for expense in self:
|
||||||
|
if not expense.product_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Use specific accounts based on payment mode if configured
|
||||||
|
product = expense.product_id.with_company(expense.company_id)
|
||||||
|
if expense.payment_mode == 'own_account':
|
||||||
|
if product.property_account_expense_employee_id:
|
||||||
|
expense.account_id = product.property_account_expense_employee_id
|
||||||
|
elif expense.payment_mode == 'company_account':
|
||||||
|
if product.property_account_expense_company_id:
|
||||||
|
expense.account_id = product.property_account_expense_company_id
|
||||||
|
|
||||||
|
receipt_due_date = fields.Date(
|
||||||
|
string="Receipt Due Date",
|
||||||
|
readonly=True,
|
||||||
|
help="Date the employee must submit the receipt."
|
||||||
|
)
|
||||||
|
receipt_received = fields.Boolean(
|
||||||
|
string="Receipt Received",
|
||||||
|
default=False,
|
||||||
|
tracking=True,
|
||||||
|
help="Mark if original receipt has been received."
|
||||||
|
)
|
||||||
|
receipt_overdue = fields.Boolean(
|
||||||
|
string="Receipt Overdue",
|
||||||
|
compute='_compute_receipt_overdue',
|
||||||
|
store=True,
|
||||||
|
help="True if receipt is not received and past due date."
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('receipt_due_date', 'receipt_received')
|
||||||
|
def _compute_receipt_overdue(self):
|
||||||
|
today = fields.Date.today()
|
||||||
|
for expense in self:
|
||||||
|
if expense.receipt_due_date and not expense.receipt_received and expense.receipt_due_date < today:
|
||||||
|
expense.receipt_overdue = True
|
||||||
|
else:
|
||||||
|
expense.receipt_overdue = False
|
||||||
|
|
||||||
|
def _prepare_move_lines_vals(self):
|
||||||
|
res = super()._prepare_move_lines_vals()
|
||||||
|
if res.get('price_unit'):
|
||||||
|
# Round the price to the currency's decimal places to avoid floating point artifacts (e.g. ...0001)
|
||||||
|
# We use precision_digits=2 which is the standard for IDR/USD etc.
|
||||||
|
res['price_unit'] = float_round(res['price_unit'], precision_digits=self.currency_id.decimal_places or 2)
|
||||||
|
return res
|
||||||
21
models/hr_expense_sheet.py
Normal file
21
models/hr_expense_sheet.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
class HrExpenseSheet(models.Model):
|
||||||
|
_inherit = 'hr.expense.sheet'
|
||||||
|
|
||||||
|
@api.depends('account_move_ids.payment_state', 'account_move_ids.amount_residual')
|
||||||
|
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:
|
||||||
|
if original_states.get(sheet.id) != 'done' and sheet.state == 'done':
|
||||||
|
# Transitioned to 'Paid'
|
||||||
|
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)
|
||||||
24
models/product_template.py
Normal file
24
models/product_template.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
class ProductTemplate(models.Model):
|
||||||
|
_inherit = 'product.template'
|
||||||
|
|
||||||
|
property_account_expense_employee_id = fields.Many2one(
|
||||||
|
'account.account',
|
||||||
|
string="Expense Account (Employee)",
|
||||||
|
company_dependent=True,
|
||||||
|
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card')), ('deprecated', '=', False)]",
|
||||||
|
help="Account used for expenses paid by the employee (to be reimbursed)."
|
||||||
|
)
|
||||||
|
property_account_expense_company_id = fields.Many2one(
|
||||||
|
'account.account',
|
||||||
|
string="Expense Account (Company)",
|
||||||
|
company_dependent=True,
|
||||||
|
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card')), ('deprecated', '=', False)]",
|
||||||
|
help="Account used for expenses paid by the company."
|
||||||
|
)
|
||||||
|
receipt_due_days = fields.Integer(
|
||||||
|
string="Receipt Due Days",
|
||||||
|
default=0,
|
||||||
|
help="Number of days the employee has to submit the receipt after the expense is paid."
|
||||||
|
)
|
||||||
76
views/hr_expense_views.xml
Normal file
76
views/hr_expense_views.xml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Expense Line Form View -->
|
||||||
|
<record id="hr_expense_view_form_inherit_receipt" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.view.form.receipt</field>
|
||||||
|
<field name="model">hr.expense</field>
|
||||||
|
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='account_id']" position="after">
|
||||||
|
<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>
|
||||||
|
<xpath expr="//sheet" position="before">
|
||||||
|
<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.
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Expense Line Tree View -->
|
||||||
|
<record id="view_expenses_tree_inherit_receipt" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.tree.receipt</field>
|
||||||
|
<field name="model">hr.expense</field>
|
||||||
|
<field name="inherit_id" ref="hr_expense.view_expenses_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='state']" position="after">
|
||||||
|
<field name="receipt_due_date" optional="show" widget="remaining_days" decoration-danger="receipt_overdue"/>
|
||||||
|
<field name="receipt_received" optional="show" widget="boolean_toggle"/>
|
||||||
|
<field name="receipt_overdue" column_invisible="True"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Expense Search View -->
|
||||||
|
<record id="hr_expense_view_search_inherit_receipt" model="ir.ui.view">
|
||||||
|
<field name="name">hr.expense.search.receipt</field>
|
||||||
|
<field name="model">hr.expense</field>
|
||||||
|
<field name="inherit_id" ref="hr_expense.hr_expense_view_search"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[@name='no_report']" position="after">
|
||||||
|
<separator/>
|
||||||
|
<filter string="Overdue Receipts" name="filter_receipt_overdue" domain="[('receipt_overdue', '=', True), ('receipt_received', '=', False)]"/>
|
||||||
|
<filter string="Receipt Received" name="filter_receipt_received" domain="[('receipt_received', '=', True)]"/>
|
||||||
|
<filter string="Receipt Missing" name="filter_receipt_missing" domain="[('receipt_received', '=', False), ('receipt_due_date', '!=', False)]"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//group" position="inside">
|
||||||
|
<filter string="Receipt Status" name="group_receipt_status" context="{'group_by': 'receipt_received'}"/>
|
||||||
|
<filter string="Receipt Due Date" name="group_receipt_due" context="{'group_by': 'receipt_due_date'}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Report Action: Overdue Receipts -->
|
||||||
|
<record id="action_hr_expense_overdue_receipts" model="ir.actions.act_window">
|
||||||
|
<field name="name">Overdue Receipts</field>
|
||||||
|
<field name="res_model">hr.expense</field>
|
||||||
|
<field name="view_mode">list,pivot,form</field>
|
||||||
|
<field name="domain">[('receipt_overdue', '=', True), ('receipt_received', '=', False)]</field>
|
||||||
|
<field name="context">{'search_default_group_receipt_due': 1}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No overdue receipts found!
|
||||||
|
</p><p>
|
||||||
|
This report shows all expenses that have been paid but the original receipts haven't been submitted yet.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_hr_expense_overdue_receipts"
|
||||||
|
name="Overdue Receipts"
|
||||||
|
parent="hr_expense.menu_hr_expense_reports"
|
||||||
|
action="action_hr_expense_overdue_receipts"
|
||||||
|
sequence="20"/>
|
||||||
|
</odoo>
|
||||||
33
views/product_views.xml
Normal file
33
views/product_views.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="product_product_expense_form_view_inherit_split" model="ir.ui.view">
|
||||||
|
<field name="name">product.product.expense.form.split</field>
|
||||||
|
<field name="model">product.product</field>
|
||||||
|
<field name="inherit_id" ref="hr_expense.product_product_expense_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='property_account_expense_id']" position="attributes">
|
||||||
|
<attribute name="string">Default Expense Account</attribute>
|
||||||
|
<attribute name="help">Fallback account used if no employee or company specific account is set.</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='property_account_expense_id']" position="after">
|
||||||
|
<field name="property_account_expense_employee_id" class="w-50" groups="account.group_account_readonly"/>
|
||||||
|
<field name="property_account_expense_company_id" class="w-50" groups="account.group_account_readonly"/>
|
||||||
|
<field name="receipt_due_days" class="w-25"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Also inherit the template form for standard product views -->
|
||||||
|
<record id="product_template_expense_form_view_inherit_split" model="ir.ui.view">
|
||||||
|
<field name="name">product.template.expense.form.split</field>
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="inherit_id" ref="account.product_template_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//group[@name='payables']" position="inside">
|
||||||
|
<field name="property_account_expense_employee_id" invisible="not can_be_expensed"/>
|
||||||
|
<field name="property_account_expense_company_id" invisible="not can_be_expensed"/>
|
||||||
|
<field name="receipt_due_days" invisible="not can_be_expensed"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user