first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,716 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from dateutil.relativedelta import relativedelta
from sql import Null
from sql.conditionals import Case
from sql.operators import Equal, NotEqual
from trytond.cache import Cache
from trytond.i18n import gettext
from trytond.model import (
Exclude, ModelSQL, ModelView, Unique, Workflow, fields)
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
from trytond.pyson import Eval, Id
from trytond.rpc import RPC
from trytond.sql.functions import DateRange
from trytond.sql.operators import RangeOverlap
from trytond.transaction import Transaction
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
from .exceptions import (
FiscalYearCloseError, FiscalYearNotFoundError, FiscalYearReOpenError)
STATES = {
'readonly': Eval('state') != 'open',
}
class FiscalYear(Workflow, ModelSQL, ModelView):
__name__ = 'account.fiscalyear'
name = fields.Char('Name', size=None, required=True)
start_date = fields.Date(
"Start Date", required=True, states=STATES,
domain=[('start_date', '<=', Eval('end_date', None))])
end_date = fields.Date(
"End Date", required=True, states=STATES,
domain=[('end_date', '>=', Eval('start_date', None))])
periods = fields.One2Many('account.period', 'fiscalyear', 'Periods',
states=STATES,
domain=[
('company', '=', Eval('company', -1)),
],
order=[('start_date', 'ASC'), ('id', 'ASC')])
state = fields.Selection([
('open', 'Open'),
('closed', 'Closed'),
('locked', 'Locked'),
], 'State', readonly=True, required=True, sort=False)
move_sequence = fields.Many2One(
'ir.sequence.strict', "Move Sequence", required=True,
domain=[
('sequence_type', '=',
Id('account', 'sequence_type_account_move')),
('company', '=', Eval('company', -1)),
],
help="Used to generate the move number when posting "
"if the period has no sequence.")
company = fields.Many2One(
'company.company', "Company", required=True)
icon = fields.Function(fields.Char("Icon"), 'get_icon')
_find_cache = Cache(__name__ + '.find', context=False)
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints += [
('dates_overlap',
Exclude(t,
(t.company, Equal),
(DateRange(t.start_date, t.end_date, '[]'), RangeOverlap),
),
'account.msg_fiscalyear_overlap'),
('open_earlier',
Exclude(t,
(t.company, Equal),
(DateRange(
Case(
(t.state == 'open', t.start_date), else_=Null),
t.end_date), RangeOverlap),
(Case((t.state == 'open', t.id), else_=-1), NotEqual)),
'account.msg_open_fiscalyear_earlier'),
('move_sequence_unique', Unique(t, t.move_sequence),
'account.msg_fiscalyear_move_sequence_unique'),
]
cls._order.insert(0, ('start_date', 'DESC'))
cls._transitions |= set((
('open', 'closed'),
('closed', 'locked'),
('closed', 'open'),
))
cls._buttons.update({
'create_periods': {
'invisible': ((Eval('state') != 'open')
| Eval('periods', [0])),
'depends': ['state'],
},
'close': {
'invisible': Eval('state') != 'open',
'depends': ['state'],
},
'reopen': {
'invisible': Eval('state') != 'closed',
'depends': ['state'],
},
'lock_': {
'invisible': Eval('state') != 'closed',
'depends': ['state'],
},
})
cls.__rpc__.update({
'create_period': RPC(readonly=False, instantiate=0),
})
@classmethod
def __register__(cls, module):
pool = Pool()
Period = pool.get('account.period')
Sequence = pool.get('ir.sequence')
SequenceStrict = pool.get('ir.sequence.strict')
table_h = cls.__table_handler__(module)
cursor = Transaction().connection.cursor()
t = cls.__table__()
migrate_move_sequence = not table_h.column_exist('move_sequence')
super().__register__(module)
# Migration from 6.8: rename state close to closed
cursor.execute(
*t.update([t.state], ['closed'], where=t.state == 'close'))
# Migrationn from 7.4: use strict sequence
if (table_h.column_exist('post_move_sequence')
and migrate_move_sequence):
table_h.not_null_action('post_move_sequence', 'remove')
period_h = Period.__table_handler__(module)
period = Period.__table__()
old2new = {}
period_migrated = (
period_h.column_exist('post_move_sequence')
and period_h.column_exist('move_sequence'))
if period_migrated:
cursor.execute(*period.select(
period.post_move_sequence, period.move_sequence))
old2new.update(cursor)
cursor.execute(*t.select(t.post_move_sequence, distinct=True))
for sequence_id, in cursor:
if sequence_id not in old2new:
sequence = Sequence(sequence_id)
new_sequence = SequenceStrict(
name=sequence.name,
sequence_type=sequence.sequence_type,
prefix=sequence.prefix,
suffix=sequence.suffix,
type=sequence.type,
number_next=sequence.number_next,
number_increment=sequence.number_increment,
padding=sequence.padding,
timestamp_rounding=sequence.timestamp_rounding,
timestamp_offset=sequence.timestamp_offset,
last_timestamp=sequence.last_timestamp,
company=sequence.company)
new_sequence.save()
old2new[sequence_id] = new_sequence.id
for old_id, new_id in old2new.items():
cursor.execute(*t.update(
[t.move_sequence], [new_id],
where=t.post_move_sequence == old_id))
if period_migrated:
table_h.drop_column('post_move_sequence')
period_h.drop_column('post_move_sequence')
@staticmethod
def default_state():
return 'open'
@staticmethod
def default_company():
return Transaction().context.get('company')
def get_icon(self, name):
return {
'open': 'tryton-account-open',
'closed': 'tryton-account-close',
'locked': 'tryton-account-block',
}.get(self.state)
@classmethod
def validate_fields(cls, fiscalyears, field_names):
super().validate_fields(fiscalyears, field_names)
cls.check_period_dates(fiscalyears, field_names)
@classmethod
def check_period_dates(cls, fiscalyears, field_names=None):
pool = Pool()
Period = pool.get('account.period')
if field_names and not (field_names & {'start_date', 'end_date'}):
return
periods = [p for f in fiscalyears for p in f.periods]
Period.check_fiscalyear_dates(periods, field_names={'fiscalyear'})
@classmethod
def check_modification(
cls, mode, fiscalyears, values=None, external=False):
pool = Pool()
Move = pool.get('account.move')
super().check_modification(
mode, fiscalyears, values=values, external=external)
if mode == 'write' and 'move_sequence' in values:
for fiscalyear in fiscalyears:
if sequence := fiscalyear.move_sequence:
if sequence.id != values['move_sequence']:
if Move.search([
('period.fiscalyear', '=', fiscalyear.id),
('state', '=', 'posted'),
], limit=1):
raise AccessError(
gettext('account.'
'msg_change_fiscalyear_move_sequence',
fiscalyear=fiscalyear.rec_name))
@classmethod
def on_modification(cls, mode, records, field_names=None):
super().on_modification(mode, records, field_names=field_names)
cls._find_cache.clear()
@classmethod
def create_period(cls, fiscalyears, interval=1, end_day=31):
'''
Create periods for the fiscal years with month interval
'''
Period = Pool().get('account.period')
to_create = []
for fiscalyear in fiscalyears:
period_start_date = fiscalyear.start_date
while period_start_date < fiscalyear.end_date:
month_offset = 1 if period_start_date.day < end_day else 0
period_end_date = (period_start_date
+ relativedelta(months=interval - month_offset)
+ relativedelta(day=end_day))
if period_end_date > fiscalyear.end_date:
period_end_date = fiscalyear.end_date
name = period_start_date.strftime('%Y-%m')
if name != period_end_date.strftime('%Y-%m'):
name += ' - ' + period_end_date.strftime('%Y-%m')
to_create.append({
'name': name,
'start_date': period_start_date,
'end_date': period_end_date,
'fiscalyear': fiscalyear.id,
'type': 'standard',
})
period_start_date = period_end_date + relativedelta(days=1)
if to_create:
Period.create(to_create)
@classmethod
@ModelView.button_action('account.act_create_periods')
def create_periods(cls, fiscalyears):
pass
@classmethod
def find(cls, company, date=None, test_state=True):
'''
Return the fiscal year for the company at the date or the current date
or raise FiscalYearNotFoundError.
If test_state is true, it searches on non-closed fiscal years
'''
pool = Pool()
Lang = pool.get('ir.lang')
Date = pool.get('ir.date')
Company = pool.get('company.company')
company_id = int(company) if company is not None else None
if not date:
with Transaction().set_context(company=company_id):
date = Date.today()
key = (company_id, date)
fiscalyear = cls._find_cache.get(key, -1)
if fiscalyear is not None and fiscalyear < 0:
clause = [
('start_date', '<=', date),
('end_date', '>=', date),
('company', '=', company_id),
]
fiscalyears = cls.search(
clause, order=[('start_date', 'DESC')], limit=1)
if fiscalyears:
fiscalyear, = fiscalyears
else:
fiscalyear = None
cls._find_cache.set(key, int(fiscalyear) if fiscalyear else None)
elif fiscalyear is not None:
fiscalyear = cls(fiscalyear)
found = fiscalyear and (not test_state or fiscalyear.state == 'open')
if not found:
lang = Lang.get()
if company is not None and not isinstance(company, Company):
company = Company(company)
if not fiscalyear:
raise FiscalYearNotFoundError(
gettext('account.msg_no_fiscalyear_date',
date=lang.strftime(date),
company=company.rec_name if company else ''))
else:
raise FiscalYearNotFoundError(
gettext('account.msg_no_open_fiscalyear_date',
date=lang.strftime(date),
fiscalyear=fiscalyear.rec_name,
company=company.rec_name if company else ''))
else:
return fiscalyear
def get_deferral(self, account):
'Computes deferrals for accounts'
pool = Pool()
Currency = pool.get('currency.currency')
Deferral = pool.get('account.account.deferral')
if not account.type:
return
if not account.deferral:
if not Currency.is_zero(self.company.currency, account.balance):
raise FiscalYearCloseError(
gettext('account'
'.msg_close_fiscalyear_account_balance_not_zero',
account=account.rec_name))
else:
deferral = Deferral()
deferral.account = account
deferral.fiscalyear = self
deferral.debit = account.debit
deferral.credit = account.credit
deferral.line_count = account.line_count
deferral.amount_second_currency = account.amount_second_currency
return deferral
@classmethod
@ModelView.button
@Workflow.transition('closed')
def close(cls, fiscalyears):
'''
Close a fiscal year
'''
pool = Pool()
Period = pool.get('account.period')
Account = pool.get('account.account')
Deferral = pool.get('account.account.deferral')
# Prevent create new fiscal year or period
cls.lock()
Period.lock()
deferrals = []
for fiscalyear in fiscalyears:
if cls.search([
('end_date', '<=', fiscalyear.start_date),
('state', '=', 'open'),
('company', '=', fiscalyear.company.id),
]):
raise FiscalYearCloseError(
gettext('account.msg_close_fiscalyear_earlier',
fiscalyear=fiscalyear.rec_name))
periods = Period.search([
('fiscalyear', '=', fiscalyear.id),
])
Period.close(periods)
with Transaction().set_context(fiscalyear=fiscalyear.id,
date=None, cumulate=True, journal=None):
accounts = Account.search([
('company', '=', fiscalyear.company.id),
])
for account in accounts:
deferral = fiscalyear.get_deferral(account)
if deferral:
deferrals.append(deferral)
Deferral.save(deferrals)
@classmethod
@ModelView.button
@Workflow.transition('open')
def reopen(cls, fiscalyears):
'''
Reopen a fiscal year
'''
Deferral = Pool().get('account.account.deferral')
for fiscalyear in fiscalyears:
if cls.search([
('start_date', '>=', fiscalyear.end_date),
('state', '!=', 'open'),
('company', '=', fiscalyear.company.id),
]):
raise FiscalYearReOpenError(
gettext('account.msg_reopen_fiscalyear_later',
fiscalyear=fiscalyear.rec_name))
deferrals = Deferral.search([
('fiscalyear', '=', fiscalyear.id),
])
Deferral.delete(deferrals)
@classmethod
@ModelView.button
@Workflow.transition('locked')
def lock_(cls, fiscalyears):
pool = Pool()
Period = pool.get('account.period')
periods = Period.search([
('fiscalyear', 'in', [f.id for f in fiscalyears]),
])
Period.lock_(periods)
class BalanceNonDeferralStart(ModelView):
__name__ = 'account.fiscalyear.balance_non_deferral.start'
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year',
required=True, domain=[('state', '=', 'open')])
company = fields.Function(fields.Many2One('company.company', 'Company'),
'on_change_with_company')
journal = fields.Many2One('account.journal', 'Journal', required=True,
domain=[
('type', '=', 'situation'),
],
context={
'company': Eval('company', -1),
},
depends={'company'})
period = fields.Many2One('account.period', 'Period', required=True,
domain=[
('fiscalyear', '=', Eval('fiscalyear', -1)),
('type', '=', 'adjustment'),
])
credit_account = fields.Many2One('account.account', 'Credit Account',
required=True,
domain=[
('type', '!=', None),
('closed', '!=', True),
('company', '=', Eval('company', -1)),
('deferral', '=', True),
])
debit_account = fields.Many2One('account.account', 'Debit Account',
required=True,
domain=[
('type', '!=', None),
('closed', '!=', True),
('company', '=', Eval('company', -1)),
('deferral', '=', True),
])
@fields.depends('fiscalyear')
def on_change_with_company(self, name=None):
return self.fiscalyear.company if self.fiscalyear else None
class BalanceNonDeferral(Wizard):
__name__ = 'account.fiscalyear.balance_non_deferral'
start = StateView('account.fiscalyear.balance_non_deferral.start',
'account.fiscalyear_balance_non_deferral_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('OK', 'balance', 'tryton-ok', default=True),
])
balance = StateAction('account.act_move_form')
def get_move_line(self, account):
pool = Pool()
Line = pool.get('account.move.line')
# Don't use account.balance because we need the non-commulated balance
balance = account.debit - account.credit
if account.company.currency.is_zero(balance):
return
line = Line()
line.account = account
if balance >= 0:
line.credit = abs(balance)
line.debit = 0
else:
line.credit = 0
line.debit = abs(balance)
return line
def get_counterpart_line(self, amount):
pool = Pool()
Line = pool.get('account.move.line')
if self.start.fiscalyear.company.currency.is_zero(amount):
return
line = Line()
if amount >= 0:
line.credit = abs(amount)
line.debit = 0
line.account = self.start.credit_account
else:
line.credit = 0
line.debit = abs(amount)
line.account = self.start.debit_account
return line
def create_move(self):
pool = Pool()
Account = pool.get('account.account')
Move = pool.get('account.move')
with Transaction().set_context(fiscalyear=self.start.fiscalyear.id,
date=None, cumulate=False):
accounts = Account.search([
('company', '=', self.start.fiscalyear.company.id),
('deferral', '=', False),
('type', '!=', None),
('closed', '!=', True),
])
lines = []
for account in accounts:
line = self.get_move_line(account)
if line:
lines.append(line)
if not lines:
return
amount = sum(l.debit - l.credit for l in lines)
counter_part_line = self.get_counterpart_line(amount)
if counter_part_line:
lines.append(counter_part_line)
move = Move()
move.period = self.start.period
move.journal = self.start.journal
move.date = self.start.period.start_date
move.origin = self.start.fiscalyear
move.lines = lines
move.save()
return move
def do_balance(self, action):
move = self.create_move()
if move:
action['views'].reverse()
return action, {'res_id': move.id if move else None}
class CreatePeriodsStart(ModelView):
__name__ = 'account.fiscalyear.create_periods.start'
frequency = fields.Selection([
('monthly', "Monthly"),
('quarterly', "Quarterly"),
('other', "Other"),
], "Frequency", sort=False, required=True)
interval = fields.Integer("Interval", required=True,
states={
'invisible': Eval('frequency') != 'other',
},
help="The length of each period, in months.")
end_day = fields.Integer("End Day", required=True,
help="The day of the month on which periods end.\n"
"Months with fewer days will end on the last day.")
@classmethod
def default_frequency(cls):
return 'monthly'
@classmethod
def default_end_day(cls):
return 31
@classmethod
def frequency_intervals(cls):
return {
'monthly': 1,
'quarterly': 3,
'other': None,
}
@fields.depends('frequency', 'interval')
def on_change_frequency(self):
if self.frequency:
self.interval = self.frequency_intervals()[self.frequency]
class CreatePeriods(Wizard):
__name__ = 'account.fiscalyear.create_periods'
start = StateView('account.fiscalyear.create_periods.start',
'account.fiscalyear_create_periods_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'create_periods', 'tryton-ok', default=True),
])
create_periods = StateTransition()
def transition_create_periods(self):
self.model.create_period(
[self.record], self.start.interval, self.start.end_day)
return 'end'
def month_delta(d1, d2):
month_offset = 1 if d1.day < d2.day else 0
return (d1.year - d2.year) * 12 + d1.month - d2.month - month_offset
class RenewFiscalYearStart(ModelView):
__name__ = 'account.fiscalyear.renew.start'
name = fields.Char("Name", required=True)
company = fields.Many2One('company.company', "Company", required=True)
previous_fiscalyear = fields.Many2One(
'account.fiscalyear', "Previous Fiscalyear", required=True,
domain=[
('company', '=', Eval('company', -1)),
],
help="Used as reference for fiscalyear configuration.")
start_date = fields.Date("Start Date", required=True)
end_date = fields.Date("End Date", required=True)
reset_sequences = fields.Boolean("Reset Sequences",
help="If checked, new sequences will be created.")
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_previous_fiscalyear(cls):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
fiscalyears = FiscalYear.search([
('company', '=', cls.default_company() or -1),
],
order=[('end_date', 'DESC')], limit=1)
if fiscalyears:
fiscalyear, = fiscalyears
return fiscalyear.id
@classmethod
def default_reset_sequences(cls):
return True
@fields.depends('previous_fiscalyear')
def on_change_previous_fiscalyear(self):
if self.previous_fiscalyear:
fiscalyear = self.previous_fiscalyear
months = month_delta(
fiscalyear.end_date, fiscalyear.start_date) + 1
self.start_date = fiscalyear.start_date + relativedelta(
months=months, day=fiscalyear.start_date.day)
self.end_date = fiscalyear.end_date + relativedelta(
months=months, day=fiscalyear.end_date.day)
self.name = fiscalyear.name.replace(
str(fiscalyear.end_date.year),
str(self.end_date.year)).replace(
str(fiscalyear.start_date.year),
str(self.start_date.year))
class RenewFiscalYear(Wizard):
__name__ = 'account.fiscalyear.renew'
start = StateView('account.fiscalyear.renew.start',
'account.fiscalyear_renew_start_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'create_', 'tryton-ok', default=True),
])
create_ = StateAction('account.act_fiscalyear_form')
def fiscalyear_defaults(self):
pool = Pool()
Sequence = pool.get('ir.sequence.strict')
defaults = {
'name': self.start.name,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'periods': [],
}
previous_sequence = self.start.previous_fiscalyear.move_sequence
sequence, = Sequence.copy([previous_sequence],
default={
'name': lambda data: data['name'].replace(
self.start.previous_fiscalyear.name,
self.start.name)
})
if self.start.reset_sequences:
sequence.number_next = 1
else:
sequence.number_next = previous_sequence.number_next
sequence.save()
defaults['move_sequence'] = sequence.id
return defaults
def create_fiscalyear(self):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
fiscalyear, = FiscalYear.copy(
[self.start.previous_fiscalyear],
default=self.fiscalyear_defaults())
periods = [
p for p in self.start.previous_fiscalyear.periods
if p.type == 'standard']
if periods:
months = month_delta(fiscalyear.end_date, fiscalyear.start_date)
months += 1
interval = months / len(periods)
end_day = max(
p.end_date.day
for p in self.start.previous_fiscalyear.periods
if p.type == 'standard')
if interval.is_integer():
FiscalYear.create_period([fiscalyear], interval, end_day)
return fiscalyear
def do_create_(self, action):
fiscalyear = self.create_fiscalyear()
fiscalyear.save()
action['views'].reverse()
return action, {'res_id': fiscalyear.id}