diff options
| author | Laurent Smet <las@openerp.com> | 2018-05-30 15:20:45 +0200 |
|---|---|---|
| committer | qdp-odoo <qdp@odoo.com> | 2018-05-30 18:28:13 +0200 |
| commit | dfde326e7532e8ddbe34a3ddf698f6a556e3cfa5 (patch) | |
| tree | 990ead942ad033e0d4502476bc364293def44d2a /addons | |
| parent | 075cafa828f2375846b3e2d03f0eed7c49a034f7 (diff) | |
[IMP] account: add account.fiscalyear model
In order to improve the fiscal year management, we allow the user to define custom fiscal years
using the account.fiscalyear model.
Was task: https://www.odoo.com/web#id=39878&view_type=form&model=project.task&menu_id=
Was PR #20936
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/account/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/account/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/account/models/account_fiscal_year.py | 56 | ||||
| -rw-r--r-- | addons/account/models/company.py | 75 | ||||
| -rw-r--r-- | addons/account/models/res_config_settings.py | 1 | ||||
| -rw-r--r-- | addons/account/security/account_security.xml | 5 | ||||
| -rw-r--r-- | addons/account/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | addons/account/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/account/tests/test_account_fiscal_year.py | 124 | ||||
| -rw-r--r-- | addons/account/views/account_fiscal_year_view.xml | 56 | ||||
| -rw-r--r-- | addons/account/views/res_config_settings_views.xml | 32 |
11 files changed, 318 insertions, 36 deletions
diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 47f91fd2cd4..9b98ed5ff11 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -67,6 +67,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'views/report_tax.xml', 'views/res_users_views.xml', 'data/service_cron_reverse.xml', + 'views/account_fiscal_year_view.xml', ], 'demo': [ 'demo/account_demo.xml', diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 4eb991c1183..95d9fcbf996 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -10,6 +10,7 @@ from . import chart_template from . import account_analytic_line from . import account_journal_dashboard from . import product +from . import account_fiscal_year from . import company from . import res_config_settings from . import account_cash_rounding diff --git a/addons/account/models/account_fiscal_year.py b/addons/account/models/account_fiscal_year.py new file mode 100644 index 00000000000..ce031fb3f59 --- /dev/null +++ b/addons/account/models/account_fiscal_year.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from odoo.exceptions import ValidationError +from odoo import api, fields, models, _ +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT + + +from datetime import datetime + + +class AccountFiscalYear(models.Model): + _name = 'account.fiscal.year' + _description = 'Fiscal Year' + + name = fields.Char(string='Name', required=True) + date_from = fields.Date(string='Start Date', required=True, + help='Start Date, included in the fiscal year.') + date_to = fields.Date(string='End Date', required=True, + help='Ending Date, included in the fiscal year.') + company_id = fields.Many2one('res.company', string='Company', required=True, + default=lambda self: self.env.user.company_id) + + @api.constrains('date_from', 'date_to', 'company_id') + def _check_dates(self): + ''' + Check interleaving between fiscal years. + There are 3 cases to consider: + + s1 s2 e1 e2 + ( [----)----] + + s2 s1 e2 e1 + [----(----] ) + + s1 s2 e2 e1 + ( [----] ) + ''' + for fy in self: + # Starting date must be prior to the ending date + date_from = datetime.strptime(fy.date_from, DEFAULT_SERVER_DATE_FORMAT) + date_to = datetime.strptime(fy.date_to, DEFAULT_SERVER_DATE_FORMAT) + if date_to < date_from: + raise ValidationError(_('The ending date must not be prior to the starting date.')) + + + domain = [ + ('id', '!=', fy.id), + ('company_id', '=', fy.company_id.id), + '|', '|', + '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_from), + '&', ('date_from', '<=', fy.date_to), ('date_to', '>=', fy.date_to), + '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_to), + ] + + if self.search_count(domain) > 0: + raise ValidationError(_('You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years.')) diff --git a/addons/account/models/company.py b/addons/account/models/company.py index aa1526d7ce8..638b0b4bcd7 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -7,6 +7,7 @@ from odoo import fields, models, api, _ from odoo.exceptions import ValidationError, UserError from odoo.exceptions import UserError from odoo.tools.float_utils import float_round, float_is_zero +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, date_utils class ResCompany(models.Model): @@ -74,27 +75,59 @@ Best Regards,''')) return last_day > last_day_of_month and last_day_of_month or last_day @api.multi - def compute_fiscalyear_dates(self, date): - """ Computes the start and end dates of the fiscalyear where the given 'date' belongs to - @param date: a datetime object - @returns: a dictionary with date_from and date_to - """ - self = self[0] - last_month = self.fiscalyear_last_month - last_day = self.fiscalyear_last_day - if (date.month < last_month or (date.month == last_month and date.day <= last_day)): - date = date.replace(month=last_month, day=last_day) - else: - if last_month == 2 and last_day == 29 and (date.year + 1) % 4 != 0: - date = date.replace(month=last_month, day=28, year=date.year + 1) - else: - date = date.replace(month=last_month, day=last_day, year=date.year + 1) - date_to = date - date_from = date + timedelta(days=1) - if date_from.month == 2 and date_from.day == 29: - date_from = date_from.replace(day=28, year=date_from.year - 1) - else: - date_from = date_from.replace(year=date_from.year - 1) + def compute_fiscalyear_dates(self, current_date): + '''Computes the start and end dates of the fiscal year where the given 'date' belongs to. + + :param current_date: A datetime.date/datetime.datetime object. + :return: A dictionary containing: + * date_from + * date_to + * [Optionally] record: The fiscal year record. + ''' + self.ensure_one() + date_str = current_date.strftime(DEFAULT_SERVER_DATE_FORMAT) + + # Search a fiscal year record containing the date. + # If a record is found, then no need further computation, we get the dates range directly. + fiscalyear = self.env['account.fiscal.year'].search([ + ('company_id', '=', self.id), + ('date_from', '<=', date_str), + ('date_to', '>=', date_str), + ], limit=1) + if fiscalyear: + return { + 'date_from': datetime.strptime(fiscalyear.date_from, DEFAULT_SERVER_DATE_FORMAT).date(), + 'date_to': datetime.strptime(fiscalyear.date_to, DEFAULT_SERVER_DATE_FORMAT).date(), + 'record': fiscalyear, + } + + date_from, date_to = date_utils.get_fiscal_year( + current_date, day=self.fiscalyear_last_day, month=self.fiscalyear_last_month) + + date_from_str = date_from.strftime(DEFAULT_SERVER_DATE_FORMAT) + date_to_str = date_to.strftime(DEFAULT_SERVER_DATE_FORMAT) + + # Search for fiscal year records reducing the delta between the date_from/date_to. + # This case could happen if there is a gap between two fiscal year records. + # E.g. two fiscal year records: 2017-01-01 -> 2017-02-01 and 2017-03-01 -> 2017-12-31. + # => The period 2017-02-02 - 2017-02-30 is not covered by a fiscal year record. + + fiscalyear_from = self.env['account.fiscal.year'].search([ + ('company_id', '=', self.id), + ('date_from', '<=', date_from_str), + ('date_to', '>=', date_from_str), + ], limit=1) + if fiscalyear_from: + date_from = datetime.strptime(fiscalyear_from.date_to, DEFAULT_SERVER_DATE_FORMAT).date() + timedelta(days=1) + + fiscalyear_to = self.env['account.fiscal.year'].search([ + ('company_id', '=', self.id), + ('date_from', '<=', date_to_str), + ('date_to', '>=', date_to_str), + ], limit=1) + if fiscalyear_to: + date_to = datetime.strptime(fiscalyear_to.date_from, DEFAULT_SERVER_DATE_FORMAT).date() - timedelta(days=1) + return {'date_from': date_from, 'date_to': date_to} def get_new_account_code(self, current_code, old_prefix, new_prefix): diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 4f3116abd55..601bda4541e 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -31,6 +31,7 @@ class ResConfigSettings(models.TransientModel): group_analytic_tags = fields.Boolean(string='Analytic Tags', implied_group='analytic.group_analytic_tags') group_warning_account = fields.Boolean(string="Warnings in Invoices", implied_group='account.group_warning_account') group_cash_rounding = fields.Boolean(string="Cash Rounding", implied_group='account.group_cash_rounding') + group_fiscal_year = fields.Boolean(string='Fiscal Years', implied_group='account.group_fiscal_year') module_account_asset = fields.Boolean(string='Assets Management') module_account_deferred_revenue = fields.Boolean(string="Revenue Recognition") module_account_budget = fields.Boolean(string='Budget Management') diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 6e6d9ce21ce..b73419fd7fb 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -34,6 +34,11 @@ <field name="name">Allow the cash rounding management</field> </record> + <record id="group_fiscal_year" model="res.groups"> + <field name="name">Allow to define fiscal years of more or less than a year</field> + <field name="category_id" ref="base.module_category_hidden"/> + </record> + </data> <data noupdate="1"> diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index 8171601d4d9..932c565be0f 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -84,3 +84,5 @@ access_account_cashbox_line,account.bank.statement.cashbox.line,model_account_ca access_account_tax_group_internal_user,account.tax.group internal user,model_account_tax_group,base.group_user,1,0,0,0 access_account_tax_group,account.tax.group,model_account_tax_group,account.group_account_invoice,1,0,0,0 access_account_tax_group_manager,account.tax.group,model_account_tax_group,account.group_account_manager,1,1,1,1 +access_account_fiscal_year_user,account.fiscal.year.user,model_account_fiscal_year,account.group_account_user,1,0,0,0 +access_account_fiscal_year_manager,account.fiscal.year.manager,model_account_fiscal_year,account.group_account_manager,1,1,1,1 diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index d50e277028c..750cddd82fa 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -16,3 +16,4 @@ from . import test_search from . import test_setup_bar from . import test_tax from . import test_templates_consistency +from . import test_account_fiscal_year diff --git a/addons/account/tests/test_account_fiscal_year.py b/addons/account/tests/test_account_fiscal_year.py new file mode 100644 index 00000000000..e1ce44c38fe --- /dev/null +++ b/addons/account/tests/test_account_fiscal_year.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from odoo.addons.account.tests.account_test_classes import AccountingTestCase +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT + +import odoo.tests + +from datetime import datetime + + +@odoo.tests.tagged('post_install', '-at_install') +class TestFiscalPosition(AccountingTestCase): + + def check_compute_fiscal_year(self, company, date, expected_date_from, expected_date_to): + '''Compute the fiscal year at a certain date for the company passed as parameter. + Then, check if the result matches the 'expected_date_from'/'expected_date_to' dates. + + :param company: The company. + :param date: The date belonging to the fiscal year. + :param expected_date_from: The expected date_from after computation. + :param expected_date_to: The expected date_to after computation. + ''' + current_date = datetime.strptime(date, DEFAULT_SERVER_DATE_FORMAT) + res = company.compute_fiscalyear_dates(current_date) + self.assertEqual(res['date_from'].strftime(DEFAULT_SERVER_DATE_FORMAT), expected_date_from) + self.assertEqual(res['date_to'].strftime(DEFAULT_SERVER_DATE_FORMAT), expected_date_to) + + def test_default_fiscal_year(self): + '''Basic case with a fiscal year xxxx-01-01 - xxxx-12-31.''' + company = self.env.ref('base.main_company') + company.fiscalyear_last_day = 31 + company.fiscalyear_last_month = 12 + + self.check_compute_fiscal_year( + company, + '2017-12-31', + '2017-01-01', + '2017-12-31', + ) + + self.check_compute_fiscal_year( + company, + '2017-01-01', + '2017-01-01', + '2017-12-31', + ) + + def test_leap_fiscal_year_1(self): + '''Case with a leap year ending the 29 February.''' + company = self.env.ref('base.main_company') + company.fiscalyear_last_day = 29 + company.fiscalyear_last_month = 2 + + self.check_compute_fiscal_year( + company, + '2016-02-29', + '2015-03-01', + '2016-02-29', + ) + + self.check_compute_fiscal_year( + company, + '2015-03-01', + '2015-03-01', + '2016-02-29', + ) + + def test_leap_fiscal_year_2(self): + '''Case with a leap year ending the 28 February.''' + company = self.env.ref('base.main_company') + company.fiscalyear_last_day = 28 + company.fiscalyear_last_month = 2 + + self.check_compute_fiscal_year( + company, + '2016-03-01', + '2016-02-29', + '2017-02-28', + ) + + def test_custom_fiscal_year(self): + '''Case with custom fiscal years.''' + company = self.env.ref('base.main_company') + company.fiscalyear_last_day = 31 + company.fiscalyear_last_month = 12 + + # Create custom fiscal year covering the 6 first months of 2017. + self.env['account.fiscal.year'].create({ + 'name': '6 month 2017', + 'date_from': '2017-01-01', + 'date_to': '2017-05-31', + 'company_id': company.id, + }) + + # Check before the custom fiscal year). + self.check_compute_fiscal_year( + company, + '2017-02-01', + '2017-01-01', + '2017-05-31', + ) + + # Check after the custom fiscal year. + self.check_compute_fiscal_year( + company, + '2017-11-01', + '2017-06-01', + '2017-12-31', + ) + + # Create custom fiscal year covering the 3 last months of 2017. + self.env['account.fiscal.year'].create({ + 'name': 'last 3 month 2017', + 'date_from': '2017-10-01', + 'date_to': '2017-12-31', + 'company_id': company.id, + }) + + # Check inside the custom fiscal years. + self.check_compute_fiscal_year( + company, + '2017-07-01', + '2017-06-01', + '2017-09-30', + ) diff --git a/addons/account/views/account_fiscal_year_view.xml b/addons/account/views/account_fiscal_year_view.xml new file mode 100644 index 00000000000..4714e5a0f8b --- /dev/null +++ b/addons/account/views/account_fiscal_year_view.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <data> + <record id="actions_account_fiscal_year" model="ir.actions.act_window"> + <field name="name">Fiscal Years</field> + <field name="res_model">account.fiscal.year</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="help" type="html"> + <p class="oe_view_nocontent_create"> + Click here to create a new fiscal year. + </p> + </field> + </record> + + <record id="action_account_fiscal_year_form" model="ir.ui.view"> + <field name="name">account.fiscal.year.form</field> + <field name="model">account.fiscal.year</field> + <field name="arch" type="xml"> + <form> + <sheet> + <group> + <field name="name" placeholder="Fiscal Year 2018"/> + <field name="date_from"/> + <field name="date_to"/> + <field name="company_id" groups="base.group_multi_company"/> + </group> + </sheet> + </form> + </field> + </record> + + <record id="action_account_fiscal_year_search" model="ir.ui.view"> + <field name="name">account.fiscal.year.search</field> + <field name="model">account.fiscal.year</field> + <field name="arch" type="xml"> + <search> + <field name="name"/> + </search> + </field> + </record> + + <record id="action_account_fiscal_year_tree" model="ir.ui.view"> + <field name="name">account.fiscal.year.tree</field> + <field name="model">account.fiscal.year</field> + <field name="arch" type="xml"> + <tree> + <field name="name"/> + <field name="date_from"/> + <field name="date_to"/> + <field name="company_id" groups="base.group_multi_company"/> + </tree> + </field> + </record> + </data> +</odoo> diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index c92d484504e..0776e2de628 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -183,6 +183,23 @@ </div> </div> </div> + <t groups="account.group_account_user"> + <h2>Fiscal Periods</h2> + <div class="row mt16 o_settings_container" id="accounting_reports"> + <div class="col-xs-12 col-md-6 o_setting_box" id="fiscalyear" invisible="1" groups="account.group_account_user"/> + <div class="col-xs-12 col-md-6 o_setting_box" id="dynamic_report" invisible="1" groups="account.group_account_user"> + <div class="o_setting_left_pane"> + <field name="module_account_reports" widget="upgrade_boolean"/> + </div> + <div class="o_setting_right_pane"> + <label for="module_account_reports"/> + <div class="text-muted" id="account_reports"> + Navigate easily through reports and see what is behind the numbers + </div> + </div> + </div> + </div> + </t> <h2>Customer Payments</h2> <div class="row mt16 o_settings_container"> <div class="col-xs-12 col-md-6 o_setting_box" id="account_followup" title="This feature is useful if you issue a high amounts of invoices."> @@ -266,21 +283,6 @@ </div> <t groups="account.group_account_user"> - <h2>Fiscal Periods</h2> - <div class="row mt16 o_settings_container" id="accounting_reports"> - <div class="col-xs-12 col-md-6 o_setting_box" id="fiscalyear" invisible="1"/> - <div class="col-xs-12 col-md-6 o_setting_box" id="dynamic_report" invisible="1" groups="account.group_account_user"> - <div class="o_setting_left_pane"> - <field name="module_account_reports" widget="upgrade_boolean"/> - </div> - <div class="o_setting_right_pane"> - <label for="module_account_reports"/> - <div class="text-muted" id="account_reports"> - Navigate easily through reports and see what is behind the numbers - </div> - </div> - </div> - </div> <h2>Bank & Cash</h2> <div class="row mt16 o_settings_container" id="bank_cash"> <div class="col-xs-12 col-md-6 o_setting_box" id="account_yodlee" title="Get your bank statements automatically imported every 4 hours, or in one-click, using Yodlee and Plaid services. Once installed, set “Bank Feeds” to “Bank Synchronization” in bank account settings. Then, click “Configure” on the online account to enter your bank credentials."> |
