[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:
@@ -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',
|
||||
|
||||
@@ -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.'))
|
||||
@@ -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
|
||||
|
||||
|
@@ -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 & 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.">
|
||||
|
||||
@@ -17,3 +17,4 @@ from .mail import *
|
||||
from .func import *
|
||||
from .debugger import *
|
||||
from .xml_utils import *
|
||||
from .date_utils import *
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user