From 6b0bdde4843d37f19359f867dd7e43eca87736e6 Mon Sep 17 00:00:00 2001 From: Lanto Razafindrabe Date: Wed, 13 Mar 2024 16:27:38 +0300 Subject: [PATCH] TA#61910 [ADD][MIG] project_wip_timesheet : migration to 14.0 --- project_wip_timesheet/__init__.py | 2 +- project_wip_timesheet/__manifest__.py | 34 ++++----- project_wip_timesheet/i18n/fr.po | 12 ++- project_wip_timesheet/models/__init__.py | 2 +- .../models/account_analytic_line.py | 76 ++++++++++++++++--- project_wip_timesheet/models/project_type.py | 3 +- project_wip_timesheet/tests/__init__.py | 2 +- .../tests/test_wip_journal_entries.py | 50 ++++++------ 8 files changed, 125 insertions(+), 56 deletions(-) diff --git a/project_wip_timesheet/__init__.py b/project_wip_timesheet/__init__.py index 6da4fa4e..bd036d3c 100644 --- a/project_wip_timesheet/__init__.py +++ b/project_wip_timesheet/__init__.py @@ -1,4 +1,4 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from . import models diff --git a/project_wip_timesheet/__manifest__.py b/project_wip_timesheet/__manifest__.py index 96b16368..a1d7291a 100644 --- a/project_wip_timesheet/__manifest__.py +++ b/project_wip_timesheet/__manifest__.py @@ -1,23 +1,23 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { - 'name': 'Project WIP Timesheet', - 'version': '1.0.0', - 'author': 'Numigi', - 'maintainer': 'Numigi', - 'website': 'https://bit.ly/numigi-com', - 'license': 'LGPL-3', - 'category': 'Project', - 'summary': 'Generate WIP journal entries from timesheets', - 'depends': [ - 'project_wip', - 'project_task_analytic_lines', - 'hr_timesheet', - 'sale_timesheet', + "name": "Project WIP Timesheet", + "version": "14.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "LGPL-3", + "category": "Project", + "summary": "Generate WIP journal entries from timesheets", + "depends": [ + "project_wip", + "project_task_analytic_lines", + "hr_timesheet", + "sale_timesheet", ], - 'data': [ - 'views/project_type.xml', + "data": [ + "views/project_type.xml", ], - 'installable': True, + "installable": True, } diff --git a/project_wip_timesheet/i18n/fr.po b/project_wip_timesheet/i18n/fr.po index 7c2acc61..2262cac2 100644 --- a/project_wip_timesheet/i18n/fr.po +++ b/project_wip_timesheet/i18n/fr.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 11.0+e\n" +"Project-Id-Version: Odoo Server 14.0+e\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-12-09 15:31+0000\n" "PO-Revision-Date: 2020-12-09 10:33-0500\n" @@ -121,5 +121,15 @@ msgstr "" "comptable de travaux en cours ({move_name}) serait renversée. L'écriture " "comptable est déjà transférée vers le coût des marchandises vendues." +#. module: project_wip_timesheet +#: code:addons/project_wip_timesheet/models/account_analytic_line.py:0 +#, python-format +msgid "" +"The entry {move_line} ({amount}) could not be reconciled.You should verify " +"if the Salary entry is partially reconciled." +msgstr "" +"L'écriture de {move_line} ({amount}) ne peut pas être lettrée. Vous devriez vérifier que " +"l'écriture de l'entrée de salaire n'est pas partiellement lettrée." + #~ msgid "(task: {})" #~ msgstr "(tâche: {})" diff --git a/project_wip_timesheet/models/__init__.py b/project_wip_timesheet/models/__init__.py index b38a5350..f4352fce 100644 --- a/project_wip_timesheet/models/__init__.py +++ b/project_wip_timesheet/models/__init__.py @@ -1,4 +1,4 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from . import account_analytic_line, project_type diff --git a/project_wip_timesheet/models/account_analytic_line.py b/project_wip_timesheet/models/account_analytic_line.py index 8c2d895f..1dc4935b 100644 --- a/project_wip_timesheet/models/account_analytic_line.py +++ b/project_wip_timesheet/models/account_analytic_line.py @@ -1,4 +1,4 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from odoo import api, fields, models, _ @@ -25,7 +25,6 @@ def create(self, vals): line.sudo()._create_update_or_reverse_salary_account_move() return line - @api.multi def write(self, vals): """When updating an analytic line, create / update / delete the wip entry. @@ -41,10 +40,9 @@ def write(self, vals): return True - @api.multi def unlink(self): """Reverse the salary account move entry when a timesheet line is deleted.""" - lines_with_moves = self.filtered(lambda l: l.salary_account_move_id) + lines_with_moves = self.filtered(lambda line: line.salary_account_move_id) for line in lines_with_moves: line.sudo()._reverse_salary_account_move_for_deleted_timesheet() return super().unlink() @@ -94,7 +92,8 @@ def _update_salary_account_move(self): ) ) - self.salary_account_move_id.state = "draft" + self.salary_account_move_id.button_draft() + self.salary_account_move_id.name = "" vals = self._get_salary_account_move_vals() self.salary_account_move_id.write(vals) self.salary_account_move_id.post() @@ -113,7 +112,17 @@ def _reverse_salary_account_move_for_updated_timesheet(self): move_name=self.salary_account_move_id.name, ) ) - self.salary_account_move_id.reverse_moves() + reversed_move = self.salary_account_move_id._reverse_moves() + reversed_move.action_post() + + # get the reversed move line and reconcile it with the salary move line + reversed_move_line, move_line = self._get_line_reconciliation_data( + self.salary_account_move_id, reversed_move + ) + + # reconcile the move lines + self._reconcile_move_lines(move_line, reversed_move_line) + self.salary_account_move_id = False def _reverse_salary_account_move_for_deleted_timesheet(self): @@ -129,10 +138,56 @@ def _reverse_salary_account_move_for_deleted_timesheet(self): move_name=self.salary_account_move_id.name, ) ) - self.salary_account_move_id.reverse_moves() + # reverse the move and post it to allow the reconciliation of the move lines + reversed_move = self.salary_account_move_id._reverse_moves() + reversed_move.action_post() + + # get the reversed move line and reconcile it with the salary move line + reversed_move_line, move_line = self._get_line_reconciliation_data( + self.salary_account_move_id, reversed_move + ) + + # reconcile the move lines + self._reconcile_move_lines(move_line, reversed_move_line) + + def _get_line_reconciliation_data(self, move, reversed_move): + """Get the reconciled move line and the original move line. + In case, we changed project in timesheet, and project has no type to select wip + account, we identify the account move line by task_id linked, that only on wip + account. + + :rtype: tuple + """ + move_line = move.line_ids.filtered( + lambda line: (line.account_id == self._get_wip_account()) or line.task_id + ) + reversed_move_line = reversed_move.line_ids.filtered( + lambda line: (line.account_id == self._get_wip_account()) or line.task_id + ) + return reversed_move_line, move_line + + def _reconcile_move_lines(self, move_line, reversal_line): + """Reconcile the move lines.""" + data = [ + { + "id": None, + "mv_line_ids": [move_line.id, reversal_line.id], + "new_mv_line_dicts": [], + "type": None, + } + ] + self.env["account.reconciliation.widget"].process_move_lines(data) + + if move_line.matching_number == "P": + raise ValidationError( + _( + "The entry {move_line} ({amount}) could not be reconciled." + "You should verify if the Salary entry is partially reconciled." + ).format(wip_line=move_line.display_name, amount=move_line.balance) + ) def _is_salary_account_move_reconciled(self): - return any(l.reconciled for l in self.salary_account_move_id.line_ids) + return any(line.reconciled for line in self.salary_account_move_id.line_ids) def _get_salary_account_move_vals(self): """Get the values for the wip account move. @@ -209,12 +264,11 @@ def _get_salary_journal(self): :rtype: account.journal """ self = self.with_context(force_company=self.company_id.id) - return self.project_id.project_type_id.salary_journal_id + return self.project_id.type_id.salary_journal_id def _get_salary_account(self): """Get the account to use for the salary move line. :rtype: account.account """ - self = self.with_context(force_company=self.company_id.id) - return self.project_id.project_type_id.salary_account_id + return self.project_id.type_id.salary_account_id diff --git a/project_wip_timesheet/models/project_type.py b/project_wip_timesheet/models/project_type.py index 123f85bd..a71cbb7d 100644 --- a/project_wip_timesheet/models/project_type.py +++ b/project_wip_timesheet/models/project_type.py @@ -1,4 +1,4 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). from odoo import api, fields, models, _ @@ -28,7 +28,6 @@ class ProjectType(models.Model): @api.constrains("salary_account_id", "salary_journal_id", "wip_account_id") def _check_required_fields_for_salary_entries(self): - self = self.with_context(force_company=self.env.user.company_id.id) project_types_with_salary_account = self.filtered(lambda t: t.salary_account_id) for project_type in project_types_with_salary_account: if not project_type.wip_account_id: diff --git a/project_wip_timesheet/tests/__init__.py b/project_wip_timesheet/tests/__init__.py index 6ef2df91..b5040c89 100644 --- a/project_wip_timesheet/tests/__init__.py +++ b/project_wip_timesheet/tests/__init__.py @@ -1,2 +1,2 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). diff --git a/project_wip_timesheet/tests/test_wip_journal_entries.py b/project_wip_timesheet/tests/test_wip_journal_entries.py index e5a77a5e..df34239f 100644 --- a/project_wip_timesheet/tests/test_wip_journal_entries.py +++ b/project_wip_timesheet/tests/test_wip_journal_entries.py @@ -1,4 +1,4 @@ -# © 2019 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# © 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import pytest @@ -18,7 +18,10 @@ def setUpClass(cls): "name": "Manager", "login": "manager", "email": "manager@test.com", - "groups_id": [(4, cls.env.ref("project.group_project_manager").id)], + "groups_id": [ + (4, cls.env.ref("project.group_project_manager").id), + (4, cls.env.ref("project_wip.group_wip_to_cgs").id), + ], "company_id": cls.company.id, "company_ids": [(4, cls.company.id)], } @@ -58,7 +61,7 @@ def setUpClass(cls): { "name": "Work in Progress", "code": "WIP", - "update_posted": True, + # "update_posted": True, "type": "general", "company_id": cls.company.id, } @@ -114,7 +117,7 @@ def setUpClass(cls): cls.project = cls.env["project.project"].create( { "name": "Job 123", - "project_type_id": cls.project_type.id, + "type_id": cls.project_type.id, "company_id": cls.company.id, } ) @@ -132,7 +135,7 @@ def _create_timesheet(cls, description="/", quantity=1, amount=50, date_=None): cls.employee.timesheet_cost = amount line = ( cls.env["account.analytic.line"] - .sudo(cls.timesheet_user) + .with_user(cls.timesheet_user) .create( { "company_id": cls.company.id, @@ -223,25 +226,25 @@ def test_salary_move_line_has_no_task(self): def test_on_change_timesheet_amount__debit_amount_updated(self): timesheet_line = self._create_timesheet() expected_amount = 25 - timesheet_line.sudo(self.timesheet_user).amount = -expected_amount + timesheet_line.with_user(self.timesheet_user).amount = -expected_amount wip_line = self._get_wip_move_line(timesheet_line) assert wip_line.debit == expected_amount def test_on_change_timesheet_quantity__move_quantity_updated(self): timesheet_line = self._create_timesheet() expected_quantity = 5 - timesheet_line.sudo(self.timesheet_user).unit_amount = expected_quantity + timesheet_line.with_user(self.timesheet_user).unit_amount = expected_quantity wip_line = self._get_wip_move_line(timesheet_line) assert wip_line.quantity == expected_quantity def test_on_change_timesheet_a_date__account_move_date_updated(self): timesheet_line = self._create_timesheet() new_date = datetime.now().date() + timedelta(30) - timesheet_line.sudo(self.timesheet_user).date = new_date + timesheet_line.with_user(self.timesheet_user).date = new_date assert timesheet_line.salary_account_move_id.date == new_date def test_if_project_has_no_type__no_account_move_created(self): - self.project.project_type_id = False + self.project.type_id = False timesheet_line = self._create_timesheet() assert not timesheet_line.salary_account_move_id @@ -264,11 +267,11 @@ def test_reversal_move_wip_line_has_task(self): def test_if_new_project_requires_no_timesheet__account_move_reversed(self): timesheet_line = self._create_timesheet() - new_project = self.project.copy({"project_type_id": False}) + new_project = self.project.copy({"type_id": False}) new_task = self.task.copy({"project_id": new_project.id}) wip_line = self._get_wip_move_line(timesheet_line) - timesheet_line.sudo(self.timesheet_user).write( + timesheet_line.with_user(self.timesheet_user).write( {"project_id": new_project.id, "task_id": new_task.id} ) assert wip_line.reconciled @@ -276,8 +279,8 @@ def test_if_new_project_requires_no_timesheet__account_move_reversed(self): def test_timesheet_amount_can_be_changed_twice(self): timesheet_line = self._create_timesheet() expected_amount = 25 - timesheet_line.sudo(self.timesheet_user).amount = -20 - timesheet_line.sudo(self.timesheet_user).amount = -expected_amount + timesheet_line.with_user(self.timesheet_user).amount = -20 + timesheet_line.with_user(self.timesheet_user).amount = -expected_amount wip_line = self._get_wip_move_line(timesheet_line) assert wip_line.debit == expected_amount @@ -288,7 +291,7 @@ def test_move_ref_contains_task_id(self): def test_after_change_task_on_timesheet__move_ref_contains_task_id(self): timesheet_line = self._create_timesheet() new_task = self.task.copy() - timesheet_line.sudo(self.timesheet_user).task_id = new_task + timesheet_line.with_user(self.timesheet_user).task_id = new_task assert str(new_task.id) in timesheet_line.salary_account_move_id.ref def test_move_ref_contains_project_name(self): @@ -297,11 +300,13 @@ def test_move_ref_contains_project_name(self): def test_after_change_project_on_timesheet__move_ref_contains_project_name(self): timesheet_line = self._create_timesheet() - new_project = self.project.copy() + self.assertIsNotNone(timesheet_line.salary_account_move_id.ref, msg=None) + new_project = self.project.copy({}) new_task = self.task.copy({"project_id": new_project.id}) - timesheet_line.sudo(self.timesheet_user).write( + timesheet_line.with_user(self.timesheet_user).write( {"project_id": new_project.id, "task_id": new_task.id} ) + assert new_project.name in timesheet_line.salary_account_move_id.ref def test_if_zero_hour__no_entry_created(self): @@ -316,24 +321,25 @@ class TestTimesheetEntryTransferedToWip(WIPJournalEntriesCase): def setUpClass(cls): super().setUpClass() cls.timesheet_line = cls._create_timesheet() - cls.project.sudo().action_wip_to_cgs() + cls.project.with_user(cls.manager).action_wip_to_cgs() def test_timesheet_amount_can_not_be_changed(self): with pytest.raises(ValidationError): - self.timesheet_line.sudo(self.timesheet_user).amount = -100 + self.timesheet_line.with_user(self.timesheet_user).amount = -100 def test_timesheet_quantity_can_not_be_changed(self): with pytest.raises(ValidationError): - self.timesheet_line.sudo(self.timesheet_user).unit_amount = 10 + self.timesheet_line.with_user(self.timesheet_user).unit_amount = 10 def test_project_with_no_type_can_not_be_set(self): - new_project = self.project.copy({"project_type_id": False}) + new_project = self.project.with_user(self.manager).copy() + new_project.type_id = False new_task = self.task.copy({"project_id": new_project.id}) with pytest.raises(ValidationError): - self.timesheet_line.sudo(self.timesheet_user).write( + self.timesheet_line.with_user(self.timesheet_user).write( {"project_id": new_project.id, "task_id": new_task.id} ) def test_timesheet_can_not_be_deleted(self): with pytest.raises(ValidationError): - self.timesheet_line.sudo(self.timesheet_user).unlink() + self.timesheet_line.with_user(self.timesheet_user).unlink()