[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
This commit is contained in:
Laurent Smet
2018-05-30 18:28:13 +02:00
committed by qdp-odoo
parent 075cafa828
commit dfde326e75
13 changed files with 383 additions and 36 deletions
+1
View File
@@ -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',
+1
View File
@@ -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
@@ -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.'))
+54 -21
View File
@@ -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):
@@ -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')
@@ -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">
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
84 access_account_tax_group_internal_user account.tax.group internal user model_account_tax_group base.group_user 1 0 0 0
85 access_account_tax_group account.tax.group model_account_tax_group account.group_account_invoice 1 0 0 0
86 access_account_tax_group_manager account.tax.group model_account_tax_group account.group_account_manager 1 1 1 1
87 access_account_fiscal_year_user account.fiscal.year.user model_account_fiscal_year account.group_account_user 1 0 0 0
88 access_account_fiscal_year_manager account.fiscal.year.manager model_account_fiscal_year account.group_account_manager 1 1 1 1
+1
View File
@@ -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
@@ -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',
)
@@ -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>
@@ -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 &amp; 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.">
+1
View File
@@ -17,3 +17,4 @@ from .mail import *
from .func import *
from .debugger import *
from .xml_utils import *
from .date_utils import *
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
import math
import calendar
from dateutil.relativedelta import relativedelta
def get_month(date):
''' Compute the month dates range on which the 'date' parameter belongs to.
:param date: A datetime.datetime or datetime.date object.
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
'''
date_from = type(date)(date.year, date.month, 1)
date_to = type(date)(date.year, date.month, calendar.monthrange(date.year, date.month)[1])
return date_from, date_to
def get_quarter_number(date):
''' Get the number of the quarter on which the 'date' parameter belongs to.
:param date: A datetime.datetime or datetime.date object.
:return: A [1-4] integer.
'''
return math.ceil(date.month / 3)
def get_quarter(date):
''' Compute the quarter dates range on which the 'date' parameter belongs to.
:param date: A datetime.datetime or datetime.date object.
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
'''
quarter_number = get_quarter_number(date)
month_from = ((quarter_number - 1) * 3) + 1
date_from = type(date)(date.year, month_from, 1)
date_to = (date_from + relativedelta(months=2))
date_to = date_to.replace(day=calendar.monthrange(date_to.year, date_to.month)[1])
return date_from, date_to
def get_fiscal_year(date, day=31, month=12):
''' Compute the fiscal year dates range on which the 'date' parameter belongs to.
A fiscal year is the period used by governments for accounting purposes and vary between countries.
By default, calling this method with only one parameter gives the calendar year because the ending date of the
fiscal year is set to the YYYY-12-31.
:param date: A datetime.datetime or datetime.date object.
:param day: The day of month the fiscal year ends.
:param month: The month of year the fiscal year ends.
:return: A tuple (date_from, date_to) having the same object type as the 'date' parameter.
'''
max_day = calendar.monthrange(date.year, month)[1]
date_to = type(date)(date.year, month, min(day, max_day))
if date <= date_to:
date_from = date_to - relativedelta(years=1)
date_from += relativedelta(days=1)
else:
date_from = date_to + relativedelta(days=1)
max_day = calendar.monthrange(date_to.year + 1, date_to.month)[1]
date_to = type(date)(date.year + 1, month, min(day, max_day))
return date_from, date_to