diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 656d4f1..d2077b4 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -19,6 +19,7 @@ "account_move_unique_reversal", "account_negative_debit_credit", "account_payment_cancel_group", + "account_search_date_range", "account_show_full_features", "bank_statement_import_csv", "canada_account_types", diff --git a/Dockerfile b/Dockerfile index 351f5b7..b824123 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ COPY account_move_unique_reversal /mnt/extra-addons/account_move_unique_reversal COPY account_negative_debit_credit /mnt/extra-addons/account_negative_debit_credit COPY account_payment_cancel_group /mnt/extra-addons/account_payment_cancel_group COPY account_show_full_features /mnt/extra-addons/account_show_full_features +COPY account_search_date_range /mnt/extra-addons/account_search_date_range COPY bank_statement_import_csv /mnt/extra-addons/bank_statement_import_csv COPY canada_account_types /mnt/extra-addons/canada_account_types COPY canada_mis_report /mnt/extra-addons/canada_mis_report diff --git a/account_search_date_range/README.rst b/account_search_date_range/README.rst new file mode 100644 index 0000000..958fe2f --- /dev/null +++ b/account_search_date_range/README.rst @@ -0,0 +1,56 @@ +============================= +Account Search Date Range Account +============================= + +This module extends the web_search_date_range module with date ranges related to accounting. + +It enables date filters that are bound to the fiscal year of the company. + +.. contents:: Table of Contents + +Configuration +------------- + +The module uses the Fiscal Year settings to determine the fiscal year and trimester ranges. + +.. image:: static/description/account_onboarding.png + +.. image:: static/description/account_onboarding_fiscal_period.png + +Usage +----- + +To add a filter to the search view of a model: + +* Go to: Settings / Technical / User Interface / Date Filters + +.. image:: static/description/date_filters.png + +* Refresh your page, then go to the list view of the model (i.e. Journal Items) + +.. image:: static/description/invoice_list.png + +Available Ranges +---------------- + +For now, the following filters are available. + +* Previous Trimester +* Current Trimester +* Next Trimester +* Previous Fiscal Year +* Current Fiscal Year +* Next Fiscal Year + +Multi-Company +------------- + +The module works with multi-company. + +If you have subsidiaries with different account closing dates, the same filters will work for all. +You will not need to recreate your dashboards for each subsidiary. + +Contributors +------------ + +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/account_search_date_range/__init__.py b/account_search_date_range/__init__.py new file mode 100644 index 0000000..ac4a686 --- /dev/null +++ b/account_search_date_range/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 - today 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/account_search_date_range/__manifest__.py b/account_search_date_range/__manifest__.py new file mode 100644 index 0000000..6244b06 --- /dev/null +++ b/account_search_date_range/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Account Date Range Account", + "version": "16.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "LGPL-3", + "category": "Project", + "summary": "Add accounting date range filters.", + "depends": ["web_search_date_range", "account"], + "data": ["data/search_date_range.xml"], + "installable": True, +} diff --git a/account_search_date_range/data/search_date_range.xml b/account_search_date_range/data/search_date_range.xml new file mode 100644 index 0000000..159ade9 --- /dev/null +++ b/account_search_date_range/data/search_date_range.xml @@ -0,0 +1,64 @@ + + + + + Previous Fiscal Trimester + [ + '&', + (field, '>=', (trimester_start - relativedelta(months=3)).strftime('%Y-%m-%d')), + (field, '<', trimester_start.strftime('%Y-%m-%d')), + ] + + + + + Current Fiscal Trimester + [ + '&', + (field, '>=', trimester_start.strftime('%Y-%m-%d')), + (field, '<', (trimester_start + relativedelta(months=3)).strftime('%Y-%m-%d')), + ] + + + + + Next Fiscal Trimester + [ + '&', + (field, '>=', (trimester_start + relativedelta(months=3)).strftime('%Y-%m-%d')), + (field, '<', (trimester_start + relativedelta(months=6)).strftime('%Y-%m-%d')), + ] + + + + + Previous Fiscal Year + [ + '&', + (field, '>=', (fiscal_year_start - relativedelta(years=1)).strftime('%Y-%m-%d')), + (field, '<', fiscal_year_start.strftime('%Y-%m-%d')), + ] + + + + + Current Fiscal Year + [ + '&', + (field, '>=', fiscal_year_start.strftime('%Y-%m-%d')), + (field, '<', (fiscal_year_start + relativedelta(years=1)).strftime('%Y-%m-%d')), + ] + + + + + Next Fiscal Year + [ + '&', + (field, '>=', (fiscal_year_start + relativedelta(years=1)).strftime('%Y-%m-%d')), + (field, '<', (fiscal_year_start + relativedelta(years=2)).strftime('%Y-%m-%d')), + ] + + + + diff --git a/account_search_date_range/i18n/fr.po b/account_search_date_range/i18n/fr.po new file mode 100644 index 0000000..d79ca17 --- /dev/null +++ b/account_search_date_range/i18n/fr.po @@ -0,0 +1,48 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_search_date_range +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-18 21:11+0000\n" +"PO-Revision-Date: 2018-10-18 17:11-0400\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 2.0.6\n" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_current_trimester +msgid "Current Fiscal Trimester" +msgstr "Trimestre fiscal courant" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_current_fiscal_year +msgid "Current Fiscal Year" +msgstr "Année fiscale courante" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_next_trimester +msgid "Next Fiscal Trimester" +msgstr "Trimestre fiscal suivant" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_next_fiscal_year +msgid "Next Fiscal Year" +msgstr "Année fiscale suivante" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_previous_trimester +msgid "Previous Fiscal Trimester" +msgstr "Trimestre fiscal précédent" + +#. module: account_search_date_range +#: model:search.date.range,label:account_search_date_range.range_previous_fiscal_year +msgid "Previous Fiscal Year" +msgstr "Année fiscale précédente" diff --git a/account_search_date_range/models/__init__.py b/account_search_date_range/models/__init__.py new file mode 100644 index 0000000..2ebf283 --- /dev/null +++ b/account_search_date_range/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import search_date_range diff --git a/account_search_date_range/models/search_date_range.py b/account_search_date_range/models/search_date_range.py new file mode 100644 index 0000000..9acc490 --- /dev/null +++ b/account_search_date_range/models/search_date_range.py @@ -0,0 +1,37 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models + + +class SearchDateRange(models.AbstractModel): + + _inherit = "search.date.range" + + @api.model + def _get_domain_context(self, field): + res = super()._get_domain_context(field) + fiscal_year_start = self._get_fiscal_year_first_date() + trimester_start = self._get_trimester_start(fiscal_year_start) + res.update( + fiscal_year_start=fiscal_year_start, + trimester_start=trimester_start, + ) + return res + + def _get_trimester_start(self, fiscal_year_start): + months_passed = relativedelta(datetime.now(), fiscal_year_start).months + return fiscal_year_start + relativedelta(months=3 * (months_passed // 3)) + + def _get_fiscal_year_first_date(self): + today = fields.Date.context_today(self) + last_month = int(self.env.user.company_id.fiscalyear_last_month) + return ( + today + + relativedelta( + years=-1 if last_month >= today.month else 0, month=last_month, day=31 + ) + + timedelta(1) + ) diff --git a/account_search_date_range/static/description/account_onboarding.png b/account_search_date_range/static/description/account_onboarding.png new file mode 100644 index 0000000..66913e5 Binary files /dev/null and b/account_search_date_range/static/description/account_onboarding.png differ diff --git a/account_search_date_range/static/description/account_onboarding_fiscal_period.png b/account_search_date_range/static/description/account_onboarding_fiscal_period.png new file mode 100644 index 0000000..01012c6 Binary files /dev/null and b/account_search_date_range/static/description/account_onboarding_fiscal_period.png differ diff --git a/account_search_date_range/static/description/date_filters.png b/account_search_date_range/static/description/date_filters.png new file mode 100644 index 0000000..0aa1d60 Binary files /dev/null and b/account_search_date_range/static/description/date_filters.png differ diff --git a/account_search_date_range/static/description/icon.png b/account_search_date_range/static/description/icon.png new file mode 100644 index 0000000..92a86b1 Binary files /dev/null and b/account_search_date_range/static/description/icon.png differ diff --git a/account_search_date_range/static/description/invoice_list.png b/account_search_date_range/static/description/invoice_list.png new file mode 100644 index 0000000..90b6e9c Binary files /dev/null and b/account_search_date_range/static/description/invoice_list.png differ diff --git a/account_search_date_range/tests/__init__.py b/account_search_date_range/tests/__init__.py new file mode 100644 index 0000000..38de02a --- /dev/null +++ b/account_search_date_range/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_date_ranges diff --git a/account_search_date_range/tests/test_date_ranges.py b/account_search_date_range/tests/test_date_ranges.py new file mode 100644 index 0000000..7f65580 --- /dev/null +++ b/account_search_date_range/tests/test_date_ranges.py @@ -0,0 +1,130 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from ddt import data, ddt, unpack +from freezegun import freeze_time +from odoo.tests import common + + +@ddt +class TestSearchDateRange(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.model = cls.env.ref("base.model_res_partner") + cls.field = cls.env.ref("base.field_res_partner__create_date") + cls.env.user.company_id.write( + {"fiscalyear_last_month": "3", "fiscalyear_last_day": "31"} + ) + + def _eval_filter_domain(self, range_ref): + date_range = self.env.ref(f"account_search_date_range.{range_ref}") + return date_range.generate_domain(self.field.name) + + @data( + ("2018-03-31", "2016-04-01", "2017-04-01"), + ("2018-04-01", "2017-04-01", "2018-04-01"), + ) + @unpack + def test_range_previous_fiscal_year(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_previous_fiscal_year") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) + + @data( + ("2018-03-31", "2017-04-01", "2018-04-01"), + ("2018-04-01", "2018-04-01", "2019-04-01"), + ) + @unpack + def test_range_current_fiscal_year(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_current_fiscal_year") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) + + @data( + ("2018-03-31", "2018-04-01", "2019-04-01"), + ("2018-04-01", "2019-04-01", "2020-04-01"), + ) + @unpack + def test_range_next_fiscal_year(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_next_fiscal_year") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) + + @data( + ("2018-03-31", "2017-10-01", "2018-01-01"), + ("2018-04-01", "2018-01-01", "2018-04-01"), + ) + @unpack + def test_range_previous_trimester(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_previous_trimester") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) + + @data( + ("2018-03-31", "2018-01-01", "2018-04-01"), + ("2018-04-01", "2018-04-01", "2018-07-01"), + ) + @unpack + def test_range_current_trimester(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_current_trimester") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) + + @data( + ("2018-03-31", "2018-04-01", "2018-07-01"), + ("2018-04-01", "2018-07-01", "2018-10-01"), + ) + @unpack + def test_range_next_trimester(self, today, date_from, date_to): + with freeze_time(today): + domain = self._eval_filter_domain("range_next_trimester") + + self.assertEqual( + domain, + [ + "&", + ("create_date", ">=", date_from), + ("create_date", "<", date_to), + ], + ) diff --git a/account_search_date_range/tests/test_fiscal_year.py b/account_search_date_range/tests/test_fiscal_year.py new file mode 100644 index 0000000..3a0edac --- /dev/null +++ b/account_search_date_range/tests/test_fiscal_year.py @@ -0,0 +1,69 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from ddt import data, ddt, unpack +from freezegun import freeze_time +from odoo import fields +from odoo.tests import common + + +@ddt +class TestSessionInfo(common.TransactionCase): + + @data( + ('2018-01-01', 12, 31, '2018-01-01'), + ('2018-05-15', 12, 31, '2018-01-01'), + ('2018-12-31', 12, 31, '2018-01-01'), + ('2018-02-28', 2, 29, '2017-03-01'), + ('2018-03-01', 2, 29, '2018-03-01'), + ('2018-12-31', 2, 29, '2018-03-01'), + ('2018-02-01', 11, 30, '2017-12-01'), + ('2018-03-01', 11, 30, '2017-12-01'), + ('2018-12-31', 11, 30, '2018-12-01'), + ) + @unpack + def test_fiscal_year_start(self, today, last_month, last_day, year_start): + today = fields.Date.to_date(today) + year_start = fields.Date.to_date(year_start) + + self.env.user.company_id.write( + { + 'fiscalyear_last_month': str(last_month), + 'fiscalyear_last_day': str(last_day), + } + ) + + with freeze_time(today): + context = self._get_domain_context() + assert context['fiscal_year_start'] == year_start + + @data( + ('2018-01-01', 12, 31, '2018-01-01'), + ('2018-05-15', 12, 31, '2018-04-01'), + ('2018-12-31', 12, 31, '2018-10-01'), + ('2018-02-01', 2, 29, '2017-12-01'), + ('2018-03-01', 2, 29, '2018-03-01'), + ('2018-12-31', 2, 29, '2018-12-01'), + ('2018-11-30', 11, 30, '2018-09-01'), + ('2018-03-01', 11, 30, '2018-03-01'), + ('2018-12-31', 11, 30, '2018-12-01'), + ('2018-07-15', 11, 30, '2018-06-01'), + ) + @unpack + def test_trimester_start(self, today, last_month, last_day, year_start): + today = fields.Date.to_date(today) + year_start = fields.Date.to_date(year_start) + + self.env.user.company_id.write( + { + 'fiscalyear_last_month': str(last_month), + 'fiscalyear_last_day': str(last_day), + } + ) + + with freeze_time(today): + context = self._get_domain_context() + assert context['trimester_start'] == year_start + + def _get_domain_context(self): + return self.env["search.date.range"]._get_domain_context("date") diff --git a/gitoo.yml b/gitoo.yml index 9f5f501..c5f0f5e 100644 --- a/gitoo.yml +++ b/gitoo.yml @@ -53,3 +53,4 @@ branch: "16.0" includes: - web_custom_label + - web_search_date_range