diff --git a/hr_expense_advance_clearing/README.rst b/hr_expense_advance_clearing/README.rst index ded65754a..a672cdaf9 100644 --- a/hr_expense_advance_clearing/README.rst +++ b/hr_expense_advance_clearing/README.rst @@ -7,7 +7,7 @@ Employee Advance and Clearing !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3f86d32ea88a35bf634ff890883afa7eed74a9d596f50690ca541b95f506662f + !! source digest: sha256:0b18f15b48427652d4b03f061e7f129f606678149ad968880eb441c870633931 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -17,13 +17,13 @@ Employee Advance and Clearing :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--expense-lightgray.png?logo=github - :target: https://github.com/OCA/hr-expense/tree/15.0/hr_expense_advance_clearing + :target: https://github.com/OCA/hr-expense/tree/16.0/hr_expense_advance_clearing :alt: OCA/hr-expense .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/hr-expense-15-0/hr-expense-15-0-hr_expense_advance_clearing + :target: https://translation.odoo-community.org/projects/hr-expense-16-0/hr-expense-16-0-hr_expense_advance_clearing :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=15.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-expense&target_branch=16.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -112,7 +112,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -152,6 +152,6 @@ Current `maintainer `__: |maintainer-kittiu| -This module is part of the `OCA/hr-expense `_ project on GitHub. +This module is part of the `OCA/hr-expense `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_expense_advance_clearing/__manifest__.py b/hr_expense_advance_clearing/__manifest__.py index 8c3ba00c4..90084eea9 100644 --- a/hr_expense_advance_clearing/__manifest__.py +++ b/hr_expense_advance_clearing/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Employee Advance and Clearing", - "version": "15.0.1.2.0", + "version": "16.0.1.0.0", "category": "Human Resources", "author": "Ecosoft, Odoo Community Association (OCA)", "license": "AGPL-3", diff --git a/hr_expense_advance_clearing/models/hr_expense.py b/hr_expense_advance_clearing/models/hr_expense.py index 1e0e3476c..e1014be59 100644 --- a/hr_expense_advance_clearing/models/hr_expense.py +++ b/hr_expense_advance_clearing/models/hr_expense.py @@ -1,7 +1,7 @@ # Copyright 2019 Kitti Upariphutthiphong # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models +from odoo import Command, _, api, fields, models from odoo.exceptions import ValidationError @@ -26,12 +26,13 @@ class HrExpense(models.Model): help="Expense created from this advance expense line", ) + def _get_product_advance(self): + return self.env.ref("hr_expense_advance_clearing.product_emp_advance", False) + @api.constrains("advance") def _check_advance(self): for expense in self.filtered("advance"): - emp_advance = self.env.ref( - "hr_expense_advance_clearing.product_emp_advance" - ) + emp_advance = expense._get_product_advance() if not emp_advance.property_account_expense_id: raise ValidationError( _("Employee advance product has no payable account") @@ -54,42 +55,57 @@ def _check_advance(self): def onchange_advance(self): self.tax_ids = False if self.advance: - self.product_id = self.env.ref( - "hr_expense_advance_clearing.product_emp_advance" - ) + self.product_id = self._get_product_advance() + + def _get_move_line_src(self, move_line_name, partner_id): + self.ensure_one() + unit_amount = self.unit_amount or self.total_amount + quantity = self.quantity if self.unit_amount else 1 + taxes = self.tax_ids.with_context(round=True).compute_all( + unit_amount, self.currency_id, quantity, self.product_id + ) + amount_currency = self.total_amount - self.amount_tax + balance = self.total_amount_company - self.amount_tax_company + ml_src_dict = { + "name": move_line_name, + "quantity": quantity, + "debit": balance if balance > 0 else 0, + "credit": -balance if balance < 0 else 0, + "amount_currency": amount_currency, + "account_id": self.account_id.id, + "product_id": self.product_id.id, + "product_uom_id": self.product_uom_id.id, + "analytic_distribution": self.analytic_distribution, + "expense_id": self.id, + "partner_id": partner_id, + "tax_ids": [Command.set(self.tax_ids.ids)], + "tax_tag_ids": [Command.set(taxes["base_tags"])], + "currency_id": self.currency_id.id, + } + return ml_src_dict - def _get_account_move_line_values(self): - move_line_values_by_expense = super()._get_account_move_line_values() - # Only when do the clearing, change cr payable to cr advance - emp_advance = self.env.ref("hr_expense_advance_clearing.product_emp_advance") - sheets = self.mapped("sheet_id").filtered("advance_sheet_id") - sheets_x = sheets.filtered(lambda x: x.advance_sheet_residual <= 0.0) - if sheets_x: # Advance Sheets with no residual left - raise ValidationError( - _("Advance: %s has no amount to clear") - % ", ".join(sheets_x.mapped("name")) - ) - for sheet in sheets: - advance_to_clear = sheet.advance_sheet_residual - for move_lines in move_line_values_by_expense.values(): - payable_move_line = False - for move_line in move_lines: - credit = move_line["credit"] - if not credit: - continue - # cr payable -> cr advance - remain_payable = 0.0 - if credit > advance_to_clear: - remain_payable = credit - advance_to_clear - move_line["credit"] = advance_to_clear - advance_to_clear = 0.0 - # extra payable line - payable_move_line = move_line.copy() - payable_move_line["credit"] = remain_payable - else: - advance_to_clear -= credit - # advance line - move_line["account_id"] = emp_advance.property_account_expense_id.id - if payable_move_line: - move_lines.append(payable_move_line) - return move_line_values_by_expense + def _get_move_line_dst( + self, + move_line_name, + partner_id, + total_amount, + total_amount_currency, + account_advance, + ): + account_date = ( + self.date + or self.sheet_id.accounting_date + or fields.Date.context_today(self) + ) + ml_dst_dict = { + "name": move_line_name, + "debit": total_amount > 0 and total_amount, + "credit": total_amount < 0 and -total_amount, + "account_id": account_advance.id, + "date_maturity": account_date, + "amount_currency": total_amount_currency, + "currency_id": self.currency_id.id, + "expense_id": self.id, + "partner_id": partner_id, + } + return ml_dst_dict diff --git a/hr_expense_advance_clearing/models/hr_expense_sheet.py b/hr_expense_advance_clearing/models/hr_expense_sheet.py index b018bfc5b..18ebcbb60 100644 --- a/hr_expense_advance_clearing/models/hr_expense_sheet.py +++ b/hr_expense_advance_clearing/models/hr_expense_sheet.py @@ -1,9 +1,9 @@ # Copyright 2019 Kitti Upariphutthiphong # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models +from odoo import Command, _, api, fields, models from odoo.exceptions import ValidationError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_is_zero from odoo.tools.safe_eval import safe_eval @@ -64,12 +64,22 @@ def _check_advance_expense(self): if advance_lines and len(advance_lines) != len(self.expense_line_ids): raise ValidationError(_("Advance must contain only advance expense line")) + @api.depends("account_move_id.payment_state") + def _compute_payment_state(self): + """After clear advance. payment state will change to 'paid'""" + res = super()._compute_payment_state() + for sheet in self: + if sheet.advance_sheet_id and sheet.account_move_id.state == "posted": + sheet.payment_state = "paid" + return res + + def _get_product_advance(self): + return self.env.ref("hr_expense_advance_clearing.product_emp_advance", False) + @api.depends("account_move_id.line_ids.amount_residual") def _compute_clearing_residual(self): - emp_advance = self.env.ref( - "hr_expense_advance_clearing.product_emp_advance", False - ) for sheet in self: + emp_advance = sheet._get_product_advance() residual_company = 0.0 if emp_advance: for line in sheet.sudo().account_move_id.line_ids: @@ -90,13 +100,12 @@ def _compute_clearing_count(self): def action_sheet_move_create(self): res = super().action_sheet_move_create() - # Reconcile advance of this sheet with the advance_sheet - emp_advance = self.env.ref("hr_expense_advance_clearing.product_emp_advance") - ctx = self._context.copy() - ctx.update({"skip_account_move_synchronization": True}) for sheet in self: + if not sheet.advance_sheet_id: + continue + amount_residual_bf_reconcile = sheet.advance_sheet_residual advance_residual = float_compare( - sheet.advance_sheet_residual, + amount_residual_bf_reconcile, sheet.total_amount, precision_rounding=sheet.currency_id.rounding, ) @@ -104,6 +113,7 @@ def action_sheet_move_create(self): sheet.account_move_id.line_ids | sheet.advance_sheet_id.account_move_id.line_ids ) + emp_advance = sheet._get_product_advance() account_id = emp_advance.property_account_expense_id.id adv_move_lines = ( self.env["account.move.line"] @@ -116,10 +126,104 @@ def action_sheet_move_create(self): ] ) ) - adv_move_lines.with_context(**ctx).reconcile() + adv_move_lines.reconcile() # Update state on clearing advance when advance residual > total amount - if sheet.advance_sheet_id and advance_residual != -1: - sheet.write({"state": "done"}) + if advance_residual != -1: + sheet.write( + { + "state": "done", + } + ) + # Update amount residual and state when advance residual < total amount + else: + sheet.write( + { + "state": "post", + "payment_state": "not_paid", + "amount_residual": sheet.total_amount + - amount_residual_bf_reconcile, + } + ) + return res + + def _get_move_line_vals(self): + self.ensure_one() + move_line_vals = [] + advance_to_clear = self.advance_sheet_residual + emp_advance = self._get_product_advance() + account_advance = emp_advance.property_account_expense_id + for expense in self.expense_line_ids: + move_line_name = ( + expense.employee_id.name + ": " + expense.name.split("\n")[0][:64] + ) + total_amount = 0.0 + total_amount_currency = 0.0 + partner_id = ( + expense.employee_id.sudo().address_home_id.commercial_partner_id.id + ) + # source move line + move_line_src = expense._get_move_line_src(move_line_name, partner_id) + move_line_values = [move_line_src] + total_amount -= expense.total_amount_company + total_amount_currency -= expense.total_amount + + # destination move line + move_line_dst = expense._get_move_line_dst( + move_line_name, + partner_id, + total_amount, + total_amount_currency, + account_advance, + ) + # Check clearing > advance, it will split line + credit = move_line_dst["credit"] + # cr payable -> cr advance + remain_payable = 0.0 + payable_move_line = [] + rounding = expense.currency_id.rounding + if ( + float_compare( + credit, + advance_to_clear, + precision_rounding=rounding, + ) + == 1 + ): + remain_payable = credit - advance_to_clear + move_line_dst["credit"] = advance_to_clear + move_line_dst["amount_currency"] = -advance_to_clear + advance_to_clear = 0.0 + # extra payable line + payable_move_line = move_line_dst.copy() + payable_move_line["credit"] = remain_payable + payable_move_line["amount_currency"] = -remain_payable + payable_move_line[ + "account_id" + ] = expense._get_expense_account_destination() + else: + advance_to_clear -= credit + # Add destination first (if credit is not zero) + if not float_is_zero(move_line_dst["credit"], precision_rounding=rounding): + move_line_values.append(move_line_dst) + if payable_move_line: + move_line_values.append(payable_move_line) + move_line_vals.extend(move_line_values) + return move_line_vals + + def _prepare_bill_vals(self): + """create journal entry instead of bills when clearing document""" + self.ensure_one() + res = super()._prepare_bill_vals() + if self.advance_sheet_id and self.payment_mode == "own_account": + if ( + self.advance_sheet_residual <= 0.0 + ): # Advance Sheets with no residual left + raise ValidationError( + _("Advance: %s has no amount to clear") % (self.name) + ) + res["move_type"] = "entry" + move_line_vals = self._get_move_line_vals() + res["line_ids"] = [Command.create(x) for x in move_line_vals] return res def open_clear_advance(self): @@ -156,7 +260,7 @@ def _prepare_clear_advance(self, line): # Prepare the clearing expense clear_line_dict = { "advance": False, - "name": False, + "name": line.clearing_product_id.display_name, "product_id": line.clearing_product_id.id, "clearing_product_id": False, "date": fields.Date.context_today(self), @@ -166,7 +270,7 @@ def _prepare_clear_advance(self, line): "av_line_id": line.id, } clear_line = self.env["hr.expense"].new(clear_line_dict) - clear_line._compute_from_product_id_company_id() # Set some vals + clear_line._compute_account_id() # Set some vals # Prepare the original advance line adv_dict = line._convert_to_write(line._cache) # remove no update columns diff --git a/hr_expense_advance_clearing/static/description/index.html b/hr_expense_advance_clearing/static/description/index.html index b96f8b379..312370926 100644 --- a/hr_expense_advance_clearing/static/description/index.html +++ b/hr_expense_advance_clearing/static/description/index.html @@ -367,9 +367,9 @@

Employee Advance and Clearing

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:3f86d32ea88a35bf634ff890883afa7eed74a9d596f50690ca541b95f506662f +!! source digest: sha256:0b18f15b48427652d4b03f061e7f129f606678149ad968880eb441c870633931 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/hr-expense Translate me on Weblate Try me on Runboat

Standard Expenses module allow employee to do the expense reimbursement only after the expense has been made. In other world, employee will need to pay first and reimburse later.

This module, allow company to advance an amount to the employee. @@ -470,7 +470,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -498,7 +498,7 @@

Maintainers

promote its widespread use.

Current maintainer:

kittiu

-

This module is part of the OCA/hr-expense project on GitHub.

+

This module is part of the OCA/hr-expense project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py b/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py index 1f211fa3d..f45f4e3a5 100644 --- a/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py +++ b/hr_expense_advance_clearing/tests/test_hr_expense_advance_clearing.py @@ -41,9 +41,7 @@ def setUpClass(cls): { "code": "154000", "name": "Employee Advance", - "user_type_id": cls.env.ref( - "account.data_account_type_current_assets" - ).id, + "account_type": "asset_current", "reconcile": True, } ) @@ -51,7 +49,7 @@ def setUpClass(cls): { "code": "X1020", "name": "Product Sales - (test)", - "user_type_id": cls.env.ref("account.data_account_type_revenue").id, + "account_type": "asset_current", } ) cls.emp_advance = cls.env.ref("hr_expense_advance_clearing.product_emp_advance") @@ -81,7 +79,6 @@ def _create_expense( amount, advance=False, payment_mode="own_account", - account=False, ): with Form( self.env["hr.expense"].with_context(default_advance=advance) @@ -90,10 +87,8 @@ def _create_expense( expense.employee_id = employee if not advance: expense.product_id = product - expense.unit_amount = amount + expense.total_amount = amount expense.payment_mode = payment_mode - if account: - expense.account_id = account expense = expense.save() expense.tax_ids = False # Test no vat return expense @@ -145,8 +140,9 @@ def test_0_test_constraints(self): self.emp_advance, 1.0, advance=True, - account=self.account_sales, ) + expense.account_id = self.account_sales.id + expense._check_advance() # Advance Sheet should not have > 1 expense lines with self.assertRaises(ValidationError): expense = self._create_expense( @@ -333,5 +329,10 @@ def test_4_clearing_product_advance(self): self.assertEqual(len(ex_sheet.expense_line_ids), 0) ex_sheet._onchange_advance_sheet_id() self.assertEqual(len(ex_sheet.expense_line_ids), 1) - reverse_move = self.advance.account_move_id._reverse_moves(cancel=True) + reverse_move = self.advance.account_move_id._reverse_moves( + default_values_list=[ + {"invoice_date": self.advance.account_move_id.date, "ref": False} + ], + cancel=True, + ) self.assertNotEqual(reverse_move, self.advance.account_move_id) diff --git a/hr_expense_advance_clearing/views/hr_expense_views.xml b/hr_expense_advance_clearing/views/hr_expense_views.xml index 53f69c8c9..907db8a18 100644 --- a/hr_expense_advance_clearing/views/hr_expense_views.xml +++ b/hr_expense_advance_clearing/views/hr_expense_views.xml @@ -141,12 +141,7 @@ placeholder="Optional clearing product is used during clear advance" /> - - - {'invisible': [('advance', '!=', False)]} - - - + {'invisible': [('advance', '!=', False)]} diff --git a/hr_expense_advance_clearing/wizard/account_payment_register.py b/hr_expense_advance_clearing/wizard/account_payment_register.py index c0a46a782..b1606ad25 100644 --- a/hr_expense_advance_clearing/wizard/account_payment_register.py +++ b/hr_expense_advance_clearing/wizard/account_payment_register.py @@ -65,6 +65,9 @@ def default_get(self, fields_list): return self._default_return_advance(fields_list) return super().default_get(fields_list) + def _get_product_advance(self): + return self.env.ref("hr_expense_advance_clearing.product_emp_advance", False) + def _validate_over_return(self): """Actual remaining = amount to clear - clear pending and it is not legit to return more than remaining""" @@ -78,15 +81,21 @@ def _validate_over_return(self): more_info = "" symbol = self.source_currency_id.symbol if amount_not_clear: - more_info = _("\nNote: pending amount clearing is {}{}").format( - symbol, - "{:,.2f}".format(amount_not_clear), - ) + more_info = _("\nNote: pending amount clearing is %(symbol)s%(amount)s") % { + "symbol": symbol, + "amount": "{:,.2f}".format(amount_not_clear), + } if float_compare(self.amount, actual_remaining, 2) == 1: raise UserError( _( - "You cannot return advance more than actual remaining ({}{}){}" - ).format(symbol, "{:,.2f}".format(actual_remaining), more_info) + "You cannot return advance more than actual remaining " + "(%(symbol)s%(amount)s)%(more_info)s" + ) + % { + "symbol": symbol, + "amount": "{:,.2f}".format(actual_remaining), + "more_info": more_info, + } ) def action_create_payments(self): @@ -107,40 +116,36 @@ def expense_post_return_advance(self): ctx = self._context.copy() ctx.update({"skip_account_move_synchronization": True}) expense_sheet = move_ids.line_ids.expense_id.sheet_id - emp_advance = self.env.ref("hr_expense_advance_clearing.product_emp_advance") + emp_advance = self._get_product_advance() advance_account = emp_advance.property_account_expense_id # Create return advance and post it - payment_vals = self._create_payment_vals_from_wizard() + batches = self._get_batches() + first_batch_result = batches[0] + payment_vals = self._create_payment_vals_from_wizard(first_batch_result) + # Set new payment_type and payment entry to be Dr Bank, Cr Advance payment_vals["advance_id"] = expense_sheet.id + payment_vals["partner_type"] = "customer" + payment_vals["destination_account_id"] = advance_account.id payment_vals_list = [payment_vals] payment = ( self.env["account.payment"].with_context(**ctx).create(payment_vals_list) ) - # Set new payment_type and payment entry to be Dr Bank, Cr Advance - payment.write( - { - "payment_type": "inbound", - "partner_type": "customer", - "destination_account_id": advance_account, - } - ) payment.action_post() redirect_link = ( - "{}".format( - payment.id, payment.name - ) + f"" + f"{payment.name}" ) # Account Payment link # Log the return advance in the chatter body = _( - "A remaining advance return of {} {} with the reference " - "{} related to your expense {} has been made." - ).format( - payment.amount, - payment.currency_id.symbol, - redirect_link, - expense_sheet.name, - ) + "A remaining advance return of %(amount)s %(symbol)s with the reference " + "%(ref)s related to your expense %(name)s has been made." + ) % { + "amount": payment.amount, + "symbol": payment.currency_id.symbol, + "ref": redirect_link, + "name": expense_sheet.name, + } expense_sheet.message_post(body=body) # Reconcile the return advance and the advance,