commit 7d70bab223fb70ebccdbd9b883936f1dab87b185 Author: Suherdy Yacob Date: Tue Mar 31 17:25:04 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dde51df --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.pyo +*~ +__pycache__ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..8aeef5d --- /dev/null +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..0482338 --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..43b8483 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_template +from . import hr_expense +from . import hr_expense_sheet +from . import account_move_line diff --git a/models/account_move_line.py b/models/account_move_line.py new file mode 100644 index 0000000..b182b94 --- /dev/null +++ b/models/account_move_line.py @@ -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) diff --git a/models/hr_expense.py b/models/hr_expense.py new file mode 100644 index 0000000..dd8a214 --- /dev/null +++ b/models/hr_expense.py @@ -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 diff --git a/models/hr_expense_sheet.py b/models/hr_expense_sheet.py new file mode 100644 index 0000000..d2df7eb --- /dev/null +++ b/models/hr_expense_sheet.py @@ -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) diff --git a/models/product_template.py b/models/product_template.py new file mode 100644 index 0000000..fd65105 --- /dev/null +++ b/models/product_template.py @@ -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." + ) diff --git a/views/hr_expense_views.xml b/views/hr_expense_views.xml new file mode 100644 index 0000000..66a3494 --- /dev/null +++ b/views/hr_expense_views.xml @@ -0,0 +1,76 @@ + + + + + hr.expense.view.form.receipt + hr.expense + + + + + + + + + + + + + + + + hr.expense.tree.receipt + hr.expense + + + + + + + + + + + + + hr.expense.search.receipt + hr.expense + + + + + + + + + + + + + + + + + + Overdue Receipts + hr.expense + list,pivot,form + [('receipt_overdue', '=', True), ('receipt_received', '=', False)] + {'search_default_group_receipt_due': 1} + +

+ No overdue receipts found! +

+ This report shows all expenses that have been paid but the original receipts haven't been submitted yet. +

+
+
+ + +
diff --git a/views/product_views.xml b/views/product_views.xml new file mode 100644 index 0000000..f9259fe --- /dev/null +++ b/views/product_views.xml @@ -0,0 +1,33 @@ + + + + product.product.expense.form.split + product.product + + + + Default Expense Account + Fallback account used if no employee or company specific account is set. + + + + + + + + + + + + product.template.expense.form.split + product.template + + + + + + + + + +