diff --git a/__manifest__.py b/__manifest__.py index 0482338..04a1ea8 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -6,8 +6,11 @@ 'author': 'Suherdy Yacob', 'depends': ['hr_expense', 'account'], 'data': [ + 'security/ir.model.access.csv', + 'data/ir_sequence_data.xml', 'views/product_views.xml', 'views/hr_expense_views.xml', + 'views/hr_expense_realization_views.xml', ], 'installable': True, 'application': False, diff --git a/data/ir_sequence_data.xml b/data/ir_sequence_data.xml new file mode 100644 index 0000000..8e61be2 --- /dev/null +++ b/data/ir_sequence_data.xml @@ -0,0 +1,12 @@ + + + + + Expense Realization + hr.expense.realization + RLZ/%(year)s/%(month)s/ + 5 + + + + diff --git a/models/__init__.py b/models/__init__.py index 43b8483..540ca14 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -2,3 +2,4 @@ from . import product_template from . import hr_expense from . import hr_expense_sheet from . import account_move_line +from . import hr_expense_realization diff --git a/models/hr_expense.py b/models/hr_expense.py index dd8a214..83896e3 100644 --- a/models/hr_expense.py +++ b/models/hr_expense.py @@ -1,4 +1,5 @@ from odoo import api, fields, models, _ +from odoo.exceptions import UserError from odoo.tools import float_round class HrExpense(models.Model): @@ -37,6 +38,46 @@ class HrExpense(models.Model): store=True, 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') def _compute_receipt_overdue(self): diff --git a/models/hr_expense_realization.py b/models/hr_expense_realization.py new file mode 100644 index 0000000..e77dd46 --- /dev/null +++ b/models/hr_expense_realization.py @@ -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") diff --git a/models/hr_expense_sheet.py b/models/hr_expense_sheet.py index d2df7eb..856ec2f 100644 --- a/models/hr_expense_sheet.py +++ b/models/hr_expense_sheet.py @@ -19,3 +19,20 @@ class HrExpenseSheet(models.Model): 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) + + @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' diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..2952b4d --- /dev/null +++ b/security/ir.model.access.csv @@ -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 diff --git a/views/hr_expense_realization_views.xml b/views/hr_expense_realization_views.xml new file mode 100644 index 0000000..d39d25d --- /dev/null +++ b/views/hr_expense_realization_views.xml @@ -0,0 +1,152 @@ + + + + + hr.expense.realization.view.tree + hr.expense.realization + + + + + + + + + + + + + + + hr.expense.realization.view.form + hr.expense.realization + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use the "Set on Lines" button to quickly assign the counterpart account to all receipt lines. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hr.expense.realization.view.search + hr.expense.realization + + + + + + + + + + + + + + + + + + + + Realization Report + hr.expense.realization + tree,form + + + + Create a new Realization Report + + + Manage employee receipts and their final accounting reconciliation. + + + + + + + + + + + + + + + diff --git a/views/hr_expense_views.xml b/views/hr_expense_views.xml index 66a3494..135c811 100644 --- a/views/hr_expense_views.xml +++ b/views/hr_expense_views.xml @@ -5,17 +5,65 @@ hr.expense.view.form.receipt hr.expense + 1000 - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + This receipt is Overdue! Please submit the original receipt as soon as possible. + + + + + + @@ -73,4 +121,72 @@ parent="hr_expense.menu_hr_expense_reports" action="action_hr_expense_overdue_receipts" sequence="20"/> + + hr.expense.sheet.form.inherit.realization + hr.expense.sheet + + + + + account.group_account_invoice + + + account.group_account_invoice + + + + + + + + + + + + + + + + hr.expense.sheet.tree.receipt + hr.expense.sheet + + + + + + + + + + + hr.expense.sheet.search.receipt + hr.expense.sheet + + + + + + + + + + + +
+ Create a new Realization Report +
+ Manage employee receipts and their final accounting reconciliation. +