From a913766534865e0255309af5ab7f18ebddba82a6 Mon Sep 17 00:00:00 2001 From: Saran440 Date: Tue, 23 Jun 2026 20:07:38 +0700 Subject: [PATCH] [IMP] hr_expense_petty_cash: fix journal items on petty cash with included tax and analytic --- hr_expense_petty_cash/models/account_move.py | 6 ++ hr_expense_petty_cash/models/hr_expense.py | 54 ++++++----- .../models/hr_expense_sheet.py | 24 +---- .../tests/test_hr_expense_petty_cash.py | 93 ++++++++++++++++++- 4 files changed, 132 insertions(+), 45 deletions(-) diff --git a/hr_expense_petty_cash/models/account_move.py b/hr_expense_petty_cash/models/account_move.py index 2bff78494..321ba8650 100644 --- a/hr_expense_petty_cash/models/account_move.py +++ b/hr_expense_petty_cash/models/account_move.py @@ -15,6 +15,12 @@ def action_post(self): self._check_petty_cash_amount() return super().action_post() + def _prepare_product_base_line_for_taxes_computation(self, product_line): + results = super()._prepare_product_base_line_for_taxes_computation(product_line) + if product_line.expense_id.payment_mode == "petty_cash": + results["special_mode"] = "total_included" + return results + @api.constrains("invoice_line_ids", "line_ids") def _check_petty_cash_amount(self): petty_cash_env = self.env["petty.cash"].sudo() diff --git a/hr_expense_petty_cash/models/hr_expense.py b/hr_expense_petty_cash/models/hr_expense.py index 9346b6d9c..360d4c5cf 100644 --- a/hr_expense_petty_cash/models/hr_expense.py +++ b/hr_expense_petty_cash/models/hr_expense.py @@ -97,30 +97,38 @@ def _get_default_expense_sheet_values(self): return [values] return super(HrExpense, todo)._get_default_expense_sheet_values() - def _get_petty_cash_move_line( - self, - move_line_name, - partner_id, - total_amount, - total_amount_currency, - tax_ids, - account=False, - ): - account_date = ( - self.date - or self.sheet_id.accounting_date - or fields.Date.context_today(self) - ) - ml_dict = { + def _get_petty_cash_move_line_source_vals(self, move_line_name, partner): + self.ensure_one() + return { "name": move_line_name, - "debit": total_amount if total_amount > 0.0 else 0.0, - "credit": -total_amount if total_amount < 0.0 else 0.0, - "account_id": account and account.id or self.account_id.id, - "date_maturity": account_date, - "amount_currency": total_amount_currency, "currency_id": self.currency_id.id, "expense_id": self.id, - "partner_id": partner_id, - "tax_ids": [Command.set(tax_ids.ids)], + "partner_id": partner.id, + "account_id": self._get_base_account().id, + "analytic_distribution": self.analytic_distribution, + "tax_ids": [Command.set(self.tax_ids.ids)], + "amount_currency": self.total_amount_currency, + "debit": self.total_amount if self.total_amount > 0.0 else 0.0, + "credit": 0.0, } - return ml_dict + + def _get_petty_cash_move_line_dest_vals(self, move_line_name, partner): + self.ensure_one() + return { + "name": move_line_name, + "currency_id": self.currency_id.id, + "expense_id": self.id, + "partner_id": self.petty_cash_id.partner_id.id, + "account_id": self.petty_cash_id.account_id.id, + "amount_currency": -self.total_amount_currency, + "debit": 0.0, + "credit": self.total_amount if self.total_amount > 0.0 else 0.0, + } + + def _get_petty_cash_move_line_vals(self, move_line_name, partner): + self.ensure_one() + move_line_vals = [ + self._get_petty_cash_move_line_source_vals(move_line_name, partner), + self._get_petty_cash_move_line_dest_vals(move_line_name, partner), + ] + return move_line_vals diff --git a/hr_expense_petty_cash/models/hr_expense_sheet.py b/hr_expense_petty_cash/models/hr_expense_sheet.py index 76bc0c69e..799af255b 100644 --- a/hr_expense_petty_cash/models/hr_expense_sheet.py +++ b/hr_expense_petty_cash/models/hr_expense_sheet.py @@ -118,28 +118,10 @@ def _get_petty_cash_move_line_vals(self): move_line_name = ( expense.employee_id.name + ": " + expense.name.split("\n")[0][:64] ) - partner_id = expense.employee_id.sudo().work_contact_id.id - # source move line - move_line_src = expense._get_petty_cash_move_line( - move_line_name, - partner_id, - expense.total_amount, - expense.total_amount_currency, - expense.tax_ids, + partner = expense.employee_id.sudo().work_contact_id + move_line_vals.extend( + expense._get_petty_cash_move_line_vals(move_line_name, partner) ) - move_line_values = [move_line_src] - - # destination move line - move_line_dst = expense._get_petty_cash_move_line( - move_line_name, - expense.petty_cash_id.partner_id.id, - -expense.total_amount, - -expense.total_amount_currency, - expense.tax_ids, - expense.petty_cash_id.account_id, - ) - move_line_values.append(move_line_dst) - move_line_vals.extend(move_line_values) return move_line_vals def _prepare_bills_vals(self): diff --git a/hr_expense_petty_cash/tests/test_hr_expense_petty_cash.py b/hr_expense_petty_cash/tests/test_hr_expense_petty_cash.py index 4a42153b1..895e8f84c 100644 --- a/hr_expense_petty_cash/tests/test_hr_expense_petty_cash.py +++ b/hr_expense_petty_cash/tests/test_hr_expense_petty_cash.py @@ -59,6 +59,28 @@ def setUpClass(cls): cls.petty_cash_holder = cls._create_petty_cash_holder(cls, cls.partner_1) cls.petty_cash_holder_2 = cls._create_petty_cash_holder(cls, cls.partner_3) + # Analytic setup, for testing analytic distribution on petty cash + cls.analytic_plan = cls.env["account.analytic.plan"].create( + {"name": "Petty Cash Plan - Test"} + ) + cls.analytic_account = cls.env["account.analytic.account"].create( + { + "name": "Petty Cash Analytic - Test", + "plan_id": cls.analytic_plan.id, + } + ) + # Tax setup (price-included) to test petty cash tax split. + cls.tax_included = cls.env["account.tax"].create( + { + "name": "Petty Cash VAT 7% (included) - Test", + "amount_type": "percent", + "amount": 7.0, + "price_include": True, + "type_tax_use": "purchase", + "company_id": cls.env.company.id, + } + ) + def _create_petty_cash_holder(self, partner): petty_cash_holder = self.petty_cash_obj.create( { @@ -85,6 +107,8 @@ def _create_expense( employee, payment_mode="own_account", petty_cash_holder=False, + analytic_distribution=False, + tax_ids=False, ): with Form(self.exp_obj) as expense: expense.name = "Expense - Test" @@ -94,7 +118,12 @@ def _create_expense( expense = expense.save() expense.total_amount = amount - expense.tax_ids = False # no VAT + if tax_ids: + expense.tax_ids = tax_ids + else: + expense.tax_ids = False # no VAT + if analytic_distribution: + expense.analytic_distribution = analytic_distribution if payment_mode == "petty_cash": expense.payment_mode = "petty_cash" @@ -314,3 +343,65 @@ def test_04_change_partner_bill_not_petty_cash(self): set(invoice.invoice_line_ids.mapped("name")), {"Test line 1", "Test line 2"}, ) + + def test_05_petty_cash_with_analytic_and_tax(self): + """Petty cash entry must carry analytic on the expense side and split + the included tax into proper base + tax lines while the clearing side + keeps the total amount paid.""" + # Fund the petty cash holder first. + self.petty_cash_holder.petty_cash_limit = 2000.0 + invoice = self._create_invoice(self.partner_1.id) + invoice.is_petty_cash = True + invoice._onchange_is_petty_cash() + invoice.invoice_line_ids.price_unit = 2000.0 + invoice.action_post() + self.petty_cash_holder._compute_petty_cash_balance() + self.assertEqual(self.petty_cash_holder.petty_cash_balance, 2000.0) + + # Expense 1070 (tax-included) -> base 1000 + 7% VAT 70. + expense = self._create_expense( + 1070.0, + self.employee_1, + "petty_cash", + self.petty_cash_holder, + analytic_distribution={str(self.analytic_account.id): 100}, + tax_ids=self.tax_included, + ) + expense.action_submit_expenses() + sheet = self._create_expense_sheet(expense) + sheet.action_submit_sheet() + sheet.action_approve_expense_sheets() + if sheet.state == "approve": + sheet.action_sheet_move_post() + self.assertEqual(sheet.state, "post") + + move = sheet.account_move_ids + self.assertEqual(move.move_type, "entry") + + # Expense base line carries the analytic distribution. + base_line = move.line_ids.filtered( + lambda line: not line.tax_line_id + and line.account_id != self.petty_cash_account_id + ) + self.assertEqual(len(base_line), 1) + self.assertEqual( + base_line.analytic_distribution, + {str(self.analytic_account.id): 100}, + ) + # Clearing side carries no analytic distribution. + petty_cash_line = move.line_ids.filtered( + lambda line: line.account_id == self.petty_cash_account_id + ) + self.assertEqual(len(petty_cash_line), 1) + self.assertFalse(petty_cash_line.analytic_distribution) + # Clearing side reflects the full tax-included amount paid. + self.assertEqual(sum(petty_cash_line.mapped("credit")), 1070.0) + # The move must be balanced. + self.assertEqual(sum(move.line_ids.mapped("debit")), 1070.0) + self.assertEqual(sum(move.line_ids.mapped("credit")), 1070.0) + # A tax line must have been generated. + tax_lines = move.line_ids.filtered(lambda line: line.tax_line_id) + self.assertTrue( + tax_lines, + "Petty cash entry should generate a tax line.", + )