Files
tryton/modules/account/period.py
T

409 lines
16 KiB
Python
Raw Normal View History

2014-12-02 12:54:29 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2018-12-29 14:20:28 +01:00
from trytond.cache import Cache
2021-12-11 17:58:41 +01:00
from trytond.const import OPERATORS
2018-12-29 14:20:28 +01:00
from trytond.i18n import gettext
from trytond.model import Index, ModelSQL, ModelView, Workflow, fields
2018-12-29 14:20:28 +01:00
from trytond.model.exceptions import AccessError
2021-12-11 17:58:41 +01:00
from trytond.pool import Pool
2021-03-11 23:00:09 +01:00
from trytond.pyson import Eval, Id
2010-08-19 22:02:00 +02:00
from trytond.transaction import Transaction
2012-09-24 22:17:29 +02:00
2021-12-11 17:58:41 +01:00
from .exceptions import (
ClosePeriodError, PeriodDatesError, PeriodNotFoundError,
PeriodSequenceError)
2018-12-29 14:20:28 +01:00
2008-03-17 18:32:06 +01:00
_STATES = {
'readonly': Eval('state') != 'open',
2008-03-13 23:22:59 +01:00
}
class Period(Workflow, ModelSQL, ModelView):
2008-03-13 23:22:59 +01:00
'Period'
2012-09-24 22:17:29 +02:00
__name__ = 'account.period'
2008-09-16 19:43:07 +02:00
name = fields.Char('Name', required=True)
start_date = fields.Date('Starting Date', required=True, states=_STATES,
domain=[('start_date', '<=', Eval('end_date', None))])
2008-09-16 19:43:07 +02:00
end_date = fields.Date('Ending Date', required=True, states=_STATES,
domain=[('end_date', '>=', Eval('start_date', None))])
fiscalyear = fields.Many2One(
'account.fiscalyear', "Fiscal Year", required=True, states=_STATES)
2008-03-13 23:22:59 +01:00
state = fields.Selection([
('open', 'Open'),
2023-06-20 10:13:56 +02:00
('closed', 'Closed'),
('locked', 'Locked'),
2022-02-15 00:08:46 +01:00
], 'State', readonly=True, required=True, sort=False)
2012-04-03 20:06:15 +02:00
post_move_sequence = fields.Many2One('ir.sequence', 'Post Move Sequence',
2014-12-03 16:13:08 +01:00
domain=[
2021-03-11 23:00:09 +01:00
('sequence_type', '=',
Id('account', 'sequence_type_account_move')),
2014-12-03 16:13:08 +01:00
['OR',
('company', '=', None),
('company', '=', Eval('company', -1)),
],
])
2008-09-16 19:43:07 +02:00
type = fields.Selection([
2011-08-04 16:57:14 +02:00
('standard', 'Standard'),
('adjustment', 'Adjustment'),
], 'Type', required=True,
states=_STATES)
company = fields.Function(fields.Many2One('company.company', 'Company',),
2014-12-03 16:13:08 +01:00
'on_change_with_company', searcher='search_company')
icon = fields.Function(fields.Char("Icon"), 'get_icon')
_find_cache = Cache(__name__ + '.find', context=False)
2008-03-13 23:22:59 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def __setup__(cls):
super(Period, cls).__setup__()
t = cls.__table__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('fiscalyear')
cls._sql_indexes.add(
Index(
t,
(t.start_date, Index.Range()),
(t.end_date, Index.Range()),
order='DESC'))
cls._order.insert(0, ('start_date', 'DESC'))
cls._transitions |= set((
2023-06-20 10:13:56 +02:00
('open', 'closed'),
('closed', 'locked'),
('closed', 'open'),
))
cls._buttons.update({
'close': {
'invisible': Eval('state') != 'open',
2018-02-19 11:36:13 +01:00
'depends': ['state'],
},
'reopen': {
2023-06-20 10:13:56 +02:00
'invisible': Eval('state') != 'closed',
2018-02-19 11:36:13 +01:00
'depends': ['state'],
},
2021-10-03 00:35:25 +02:00
'lock_': {
2023-06-20 10:13:56 +02:00
'invisible': Eval('state') != 'closed',
2018-02-19 11:36:13 +01:00
'depends': ['state'],
},
})
2023-06-20 10:13:56 +02:00
@classmethod
def __register__(cls, module):
cursor = Transaction().connection.cursor()
t = cls.__table__()
super().__register__(module)
# Migration from 6.8: rename state close to closed
cursor.execute(
*t.update([t.state], ['closed'], where=t.state == 'close'))
2012-09-24 22:17:29 +02:00
@staticmethod
def default_state():
2008-03-13 23:22:59 +01:00
return 'open'
2012-09-24 22:17:29 +02:00
@staticmethod
def default_type():
2008-09-16 19:43:07 +02:00
return 'standard'
2019-10-16 21:17:39 +02:00
@fields.depends('fiscalyear', '_parent_fiscalyear.company')
2014-12-03 16:13:08 +01:00
def on_change_with_company(self, name=None):
2023-03-26 09:49:00 +02:00
return self.fiscalyear.company if self.fiscalyear else None
2012-09-24 22:17:29 +02:00
@classmethod
def search_company(cls, name, clause):
2017-09-27 18:37:24 +02:00
return [('fiscalyear.' + clause[0],) + tuple(clause[1:])]
def get_icon(self, name):
return {
'open': 'tryton-account-open',
2023-06-20 10:13:56 +02:00
'closed': 'tryton-account-close',
'locked': 'tryton-account-block',
}.get(self.state)
@classmethod
2022-04-11 23:24:20 +02:00
def validate_fields(cls, periods, field_names):
super().validate_fields(periods, field_names)
cls.check_dates(periods, field_names)
cls.check_fiscalyear_dates(periods, field_names)
cls.check_post_move_sequence(periods, field_names)
2022-04-11 23:24:20 +02:00
@classmethod
def check_dates(cls, periods, field_names=None):
if field_names and not (
field_names & {
'start_date', 'end_date', 'fiscalyear', 'type'}):
return
2016-07-07 10:06:50 +02:00
transaction = Transaction()
connection = transaction.connection
2022-04-11 23:24:20 +02:00
cls.lock()
table = cls.__table__()
2016-07-07 10:06:50 +02:00
cursor = connection.cursor()
2022-04-11 23:24:20 +02:00
for period in periods:
if period.type != 'standard':
continue
cursor.execute(*table.select(table.id,
where=(((table.start_date <= period.start_date)
& (table.end_date >= period.start_date))
| ((table.start_date <= period.end_date)
& (table.end_date >= period.end_date))
| ((table.start_date >= period.start_date)
& (table.end_date <= period.end_date)))
& (table.fiscalyear == period.fiscalyear.id)
& (table.type == 'standard')
& (table.id != period.id)))
period_id = cursor.fetchone()
if period_id:
overlapping_period = cls(period_id[0])
raise PeriodDatesError(
gettext('account.msg_period_overlap',
first=period.rec_name,
second=overlapping_period.rec_name))
2012-09-24 22:17:29 +02:00
2022-04-11 23:24:20 +02:00
@classmethod
def check_fiscalyear_dates(cls, periods, field_names=None):
if field_names and not (
field_names & {
'start_date', 'end_date', 'fiscalyear'}):
return
2022-04-11 23:24:20 +02:00
for period in periods:
fiscalyear = period.fiscalyear
if (period.start_date < fiscalyear.start_date
or period.end_date > fiscalyear.end_date):
raise PeriodDatesError(
gettext('account.msg_period_fiscalyear_dates',
period=period.rec_name,
fiscalyear=fiscalyear.rec_name))
@classmethod
def check_post_move_sequence(cls, periods, field_names=None):
if field_names and not (
field_names & {'post_move_sequence', 'fiscalyear'}):
return
for period in periods:
if not period.post_move_sequence:
continue
periods = cls.search([
('post_move_sequence', '=', period.post_move_sequence.id),
('fiscalyear', '!=', period.fiscalyear.id),
])
if periods:
raise PeriodSequenceError(
gettext('account.msg_period_same_sequence',
first=period.rec_name,
second=periods[0].rec_name))
2012-09-24 22:17:29 +02:00
@classmethod
def find(cls, company, date=None, test_state=True):
2008-03-13 23:22:59 +01:00
'''
Return the period for the company at the date or the current date
or raise PeriodNotFoundError.
If test_state is true, it searches on non-closed periods
2008-03-13 23:22:59 +01:00
'''
pool = Pool()
Date = pool.get('ir.date')
Lang = pool.get('ir.lang')
Company = pool.get('company.company')
2008-10-08 12:17:47 +02:00
company_id = int(company) if company is not None else None
2008-03-13 23:22:59 +01:00
if not date:
with Transaction().set_context(company=company_id):
date = Date.today()
key = (company_id, date, test_state)
period = cls._find_cache.get(key, -1)
if period is not None and period < 0:
clause = [
('start_date', '<=', date),
('end_date', '>=', date),
('fiscalyear.company', '=', company_id),
('type', '=', 'standard'),
]
periods = cls.search(
clause, order=[('start_date', 'DESC')], limit=1)
if periods:
period, = periods
2008-03-13 23:22:59 +01:00
else:
period = None
cls._find_cache.set(key, int(period) if period else None)
elif period is not None:
period = cls(period)
found = period and (not test_state or period.state == 'open')
if not found:
lang = Lang.get()
if company is not None and not isinstance(company, Company):
company = Company(company)
if not period:
raise PeriodNotFoundError(
gettext('account.msg_no_period_date',
date=lang.strftime(date),
company=company.rec_name if company else ''))
else:
raise PeriodNotFoundError(
gettext('account.msg_no_open_period_date',
date=lang.strftime(date),
period=period.rec_name,
company=company.rec_name if company else ''))
else:
return period
2008-03-13 23:22:59 +01:00
2012-09-24 22:17:29 +02:00
@classmethod
def _check(cls, periods):
Move = Pool().get('account.move')
moves = Move.search([
('period', 'in', [p.id for p in periods]),
], limit=1)
if moves:
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_modify_delete_period_moves',
period=moves[0].period.rec_name))
2012-09-24 22:17:29 +02:00
@classmethod
def search(cls, args, offset=0, limit=None, order=None, count=False,
2013-09-06 22:33:47 +02:00
query=False):
args = args[:]
2012-04-03 20:06:15 +02:00
2008-09-04 18:24:33 +02:00
def process_args(args):
i = 0
while i < len(args):
# add test for xmlrpc and pyson that doesn't handle tuple
2020-03-01 12:33:50 +01:00
if ((
isinstance(args[i], tuple)
2013-02-16 18:54:35 +01:00
or (isinstance(args[i], list) and len(args[i]) > 2
and args[i][1] in OPERATORS))
and args[i][0] in ('start_date', 'end_date')
and isinstance(args[i][2], (list, tuple))):
if not args[i][2][0]:
args[i] = ('id', '!=', '0')
else:
2012-11-07 10:59:00 +01:00
period = cls(args[i][2][0])
2012-04-03 20:06:15 +02:00
args[i] = (args[i][0], args[i][1],
2012-11-07 10:59:00 +01:00
getattr(period, args[i][2][1]))
elif isinstance(args[i], list):
process_args(args[i])
2008-09-04 18:24:33 +02:00
i += 1
process_args(args)
2012-09-24 22:17:29 +02:00
return super(Period, cls).search(args, offset=offset, limit=limit,
2013-09-06 22:33:47 +02:00
order=order, count=count, query=query)
2012-09-24 22:17:29 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
2012-09-24 22:17:29 +02:00
FiscalYear = Pool().get('account.fiscalyear')
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
for vals in vlist:
if vals.get('fiscalyear'):
fiscalyear = FiscalYear(vals['fiscalyear'])
if fiscalyear.state != 'open':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account.msg_create_period_closed_fiscalyear',
fiscalyear=fiscalyear.rec_name))
2013-01-06 22:18:13 +01:00
if not vals.get('post_move_sequence'):
vals['post_move_sequence'] = (
fiscalyear.post_move_sequence.id)
periods = super(Period, cls).create(vlist)
cls._find_cache.clear()
return periods
2012-09-24 22:17:29 +02:00
@classmethod
2013-12-19 14:44:33 +01:00
def write(cls, *args):
2012-09-24 22:17:29 +02:00
Move = Pool().get('account.move')
2013-12-19 14:44:33 +01:00
actions = iter(args)
args = []
for periods, values in zip(actions, actions):
2018-06-26 19:11:40 +02:00
for key, value in values.items():
2013-12-19 14:44:33 +01:00
if key in ('start_date', 'end_date', 'fiscalyear'):
2015-03-23 21:19:09 +01:00
def modified(period):
if key in ['start_date', 'end_date']:
return getattr(period, key) != value
else:
return period.fiscalyear .id != value
2018-06-26 19:11:40 +02:00
cls._check(list(filter(modified, periods)))
2013-12-19 14:44:33 +01:00
break
if values.get('state') == 'open':
for period in periods:
if period.fiscalyear.state != 'open':
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext(
'account.msg_open_period_closed_fiscalyear',
period=period.rec_name,
fiscalyear=period.fiscalyear.rec_name))
2013-12-19 14:44:33 +01:00
if values.get('post_move_sequence'):
for period in periods:
if (period.post_move_sequence
2020-03-01 12:33:50 +01:00
and period.post_move_sequence.id
!= values['post_move_sequence']):
2013-12-19 14:44:33 +01:00
if Move.search([
('period', '=', period.id),
('state', '=', 'posted'),
]):
2018-12-29 14:20:28 +01:00
raise AccessError(
gettext('account'
'.msg_change_period_post_move_sequence',
period=period.rec_name))
2013-12-19 14:44:33 +01:00
args.extend((periods, values))
super(Period, cls).write(*args)
cls._find_cache.clear()
2012-09-24 22:17:29 +02:00
@classmethod
def delete(cls, periods):
cls._check(periods)
super(Period, cls).delete(periods)
cls._find_cache.clear()
2012-09-24 22:17:29 +02:00
@classmethod
@ModelView.button
2023-06-20 10:13:56 +02:00
@Workflow.transition('closed')
2012-09-24 22:17:29 +02:00
def close(cls, periods):
pool = Pool()
JournalPeriod = pool.get('account.journal.period')
Move = pool.get('account.move')
Account = pool.get('account.account')
transaction = Transaction()
2022-01-04 22:17:13 +01:00
# Lock period and move to be sure no new record will be created
JournalPeriod.lock()
2022-01-04 22:17:13 +01:00
Move.lock()
2008-03-31 14:41:54 +02:00
for period in periods:
with transaction.set_context(
fiscalyear=period.fiscalyear.id, date=period.end_date,
cumulate=True, journal=None):
for account in Account.search([
('company', '=', period.company.id),
('end_date', '>=', period.start_date),
('end_date', '<=', period.end_date),
]):
if account.balance:
raise ClosePeriodError(
gettext('account.'
'msg_close_period_inactive_accounts',
account=account.rec_name,
period=period.rec_name))
unposted_moves = Move.search([
2013-03-25 18:35:52 +00:00
('period', 'in', [p.id for p in periods]),
('state', '!=', 'posted'),
], limit=1)
if unposted_moves:
unposted_move, = unposted_moves
2018-12-29 14:20:28 +01:00
raise ClosePeriodError(
gettext('account.msg_close_period_non_posted_moves',
period=unposted_move.period.rec_name,
2020-10-28 23:44:17 +01:00
moves=unposted_move.rec_name))
2012-09-24 22:17:29 +02:00
journal_periods = JournalPeriod.search([
('period', 'in', [p.id for p in periods]),
2010-08-19 22:02:00 +02:00
])
2012-09-24 22:17:29 +02:00
JournalPeriod.close(journal_periods)
2012-09-24 22:17:29 +02:00
@classmethod
@ModelView.button
@Workflow.transition('open')
def reopen(cls, periods):
"Re-open period"
pass
@classmethod
@ModelView.button
@Workflow.transition('locked')
2021-10-03 00:35:25 +02:00
def lock_(cls, periods):
pass
2012-01-02 17:54:33 +01:00
2013-01-22 16:14:24 +01:00
@property
def post_move_sequence_used(self):
return self.post_move_sequence or self.fiscalyear.post_move_sequence