From 9c8a1dc96c5d5204d69f53ad1ba3b230085f9b01 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 8 May 2026 17:00:28 +0700 Subject: [PATCH] feat: add reset to draft functionality and automate receipt status synchronization for expense realizations --- models/hr_expense.py | 6 ++++++ models/hr_expense_realization.py | 26 +++++++++++++++++++++++++- models/hr_expense_sheet.py | 2 ++ views/hr_expense_realization_views.xml | 1 + 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/models/hr_expense.py b/models/hr_expense.py index d436529..e3122b7 100644 --- a/models/hr_expense.py +++ b/models/hr_expense.py @@ -111,6 +111,12 @@ class HrExpense(models.Model): else: expense.receipt_overdue = False + def _update_receipt_received_status(self): + for expense in self: + if expense.payment_mode == 'company_account': + has_valid_realization = any(r.state in ['confirmed', 'posted'] for r in expense.realization_ids) + expense.receipt_received = has_valid_realization + def _prepare_move_lines_vals(self): res = super()._prepare_move_lines_vals() if res.get('price_unit'): diff --git a/models/hr_expense_realization.py b/models/hr_expense_realization.py index 0db513f..6f92919 100644 --- a/models/hr_expense_realization.py +++ b/models/hr_expense_realization.py @@ -81,13 +81,37 @@ class HrExpenseRealization(models.Model): vals['name'] = self.env['ir.sequence'].next_by_code('hr.expense.realization') or _('New') return super().create(vals_list) + def unlink(self): + expenses = self.mapped('expense_id') + for rec in self: + if rec.state == 'posted': + raise UserError(_("You cannot delete a posted realization.")) + res = super().unlink() + if expenses: + expenses._update_receipt_received_status() + # Recompute sheet status for affected expenses + sheets = expenses.mapped('sheet_id') + if sheets: + sheets._compute_receipt_status() + return res + + def action_draft(self): + for rec in self: + if rec.state == 'posted': + raise UserError(_("You cannot reset a posted realization to draft. Please cancel the journal entry first.")) + rec.state = 'draft' + if rec.expense_id: + rec.expense_id._update_receipt_received_status() + if rec.expense_id.sheet_id: + rec.expense_id.sheet_id._compute_receipt_status() + 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}) + self.expense_id._update_receipt_received_status() # Explicitly trigger recompute of the sheet status if self.expense_id.sheet_id: self.expense_id.sheet_id._compute_receipt_status() diff --git a/models/hr_expense_sheet.py b/models/hr_expense_sheet.py index c469171..d93b1c5 100644 --- a/models/hr_expense_sheet.py +++ b/models/hr_expense_sheet.py @@ -258,6 +258,7 @@ class HrExpenseSheet(models.Model): # Reset draft/confirmed ones back to draft if resetting the sheet realizations.filtered(lambda r: r.state != 'posted').write({'state': 'draft'}) + sheet.expense_line_ids._update_receipt_received_status() return super().action_reset_expense_sheets() @@ -268,6 +269,7 @@ class HrExpenseSheet(models.Model): 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'}) + sheet.expense_line_ids._update_receipt_received_status() return super().action_refuse_expense_sheets() def action_recompute_state(self): diff --git a/views/hr_expense_realization_views.xml b/views/hr_expense_realization_views.xml index 8c74fc1..81b1408 100644 --- a/views/hr_expense_realization_views.xml +++ b/views/hr_expense_realization_views.xml @@ -24,6 +24,7 @@