448 lines
16 KiB
Python
448 lines
16 KiB
Python
# 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 decimal import Decimal
|
|
|
|
from sql import Null
|
|
from sql.aggregate import Sum
|
|
|
|
from trytond.model import (
|
|
ModelView, ModelSQL, Workflow, DeactivableMixin, fields, Unique)
|
|
from trytond import backend
|
|
from trytond.pyson import Eval, Bool
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool
|
|
from trytond.tools import reduce_ids, grouped_slice
|
|
from trytond.tools.multivalue import migrate_property
|
|
from trytond.modules.company.model import (
|
|
CompanyMultiValueMixin, CompanyValueMixin)
|
|
|
|
__all__ = ['JournalType', 'Journal', 'JournalSequence', 'JournalAccount',
|
|
'JournalCashContext',
|
|
'JournalPeriod']
|
|
|
|
STATES = {
|
|
'readonly': Eval('state') == 'close',
|
|
}
|
|
DEPENDS = ['state']
|
|
|
|
|
|
class JournalType(ModelSQL, ModelView):
|
|
'Journal Type'
|
|
__name__ = 'account.journal.type'
|
|
name = fields.Char('Name', size=None, required=True, translate=True)
|
|
code = fields.Char('Code', size=None, required=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(JournalType, cls).__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('code_uniq', Unique(t, t.code), 'The code must be unique.'),
|
|
]
|
|
cls._order.insert(0, ('code', 'ASC'))
|
|
|
|
|
|
class Journal(
|
|
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
|
|
'Journal'
|
|
__name__ = 'account.journal'
|
|
name = fields.Char('Name', size=None, required=True, translate=True)
|
|
code = fields.Char('Code', size=None)
|
|
type = fields.Selection('get_types', 'Type', required=True)
|
|
sequence = fields.MultiValue(fields.Many2One(
|
|
'ir.sequence', "Sequence",
|
|
domain=[
|
|
('code', '=', 'account.journal'),
|
|
('company', 'in', [
|
|
Eval('context', {}).get('company', -1), None]),
|
|
],
|
|
states={
|
|
'required': Bool(Eval('context', {}).get('company', -1)),
|
|
}))
|
|
sequences = fields.One2Many(
|
|
'account.journal.sequence', 'journal', "Sequences")
|
|
credit_account = fields.MultiValue(fields.Many2One(
|
|
'account.account', "Default Credit Account",
|
|
domain=[
|
|
('kind', '!=', 'view'),
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
],
|
|
states={
|
|
'required': ((Eval('type').in_(['cash', 'write-off']))
|
|
& (Eval('context', {}).get('company', -1) != -1)),
|
|
'invisible': ~Eval('context', {}).get('company', -1),
|
|
},
|
|
depends=['type']))
|
|
debit_account = fields.MultiValue(fields.Many2One(
|
|
'account.account', "Default Debit Account",
|
|
domain=[
|
|
('kind', '!=', 'view'),
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
],
|
|
states={
|
|
'required': ((Eval('type').in_(['cash', 'write-off']))
|
|
& (Eval('context', {}).get('company', -1) != -1)),
|
|
'invisible': ~Eval('context', {}).get('company', -1),
|
|
},
|
|
depends=['type']))
|
|
accounts = fields.One2Many(
|
|
'account.journal.account', 'journal', "Accounts")
|
|
debit = fields.Function(fields.Numeric('Debit',
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits']), 'get_debit_credit_balance')
|
|
credit = fields.Function(fields.Numeric('Credit',
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits']), 'get_debit_credit_balance')
|
|
balance = fields.Function(fields.Numeric('Balance',
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits']), 'get_debit_credit_balance')
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
|
'get_currency_digits')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Journal, cls).__setup__()
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
JournalSequence = pool.get('account.journal.sequence')
|
|
TableHandler = backend.get('TableHandler')
|
|
sql_table = cls.__table__()
|
|
journal_sequence = JournalSequence.__table__()
|
|
|
|
super(Journal, cls).__register__(module_name)
|
|
|
|
cursor = Transaction().connection.cursor()
|
|
table = TableHandler(cls, module_name)
|
|
|
|
# Migration from 1.0 sequence Many2One change into MultiValue
|
|
if table.column_exist('sequence'):
|
|
query = journal_sequence.insert(
|
|
[journal_sequence.journal, journal_sequence.sequence],
|
|
sql_table.select(sql_table.id, sql_table.sequence))
|
|
cursor.execute(*query)
|
|
table.drop_column('sequence')
|
|
|
|
@classmethod
|
|
def multivalue_model(cls, field):
|
|
pool = Pool()
|
|
if field in {'credit_account', 'debit_account'}:
|
|
return pool.get('account.journal.account')
|
|
return super(Journal, cls).multivalue_model(field)
|
|
|
|
@classmethod
|
|
def default_sequence(cls, **pattern):
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_types():
|
|
Type = Pool().get('account.journal.type')
|
|
types = Type.search([])
|
|
return [(x.code, x.name) for x in types]
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
return [bool_op,
|
|
('code',) + tuple(clause[1:]),
|
|
(cls._rec_name,) + tuple(clause[1:]),
|
|
]
|
|
|
|
@classmethod
|
|
def get_currency_digits(cls, journals, name):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id:
|
|
company = Company(company_id)
|
|
digits = company.currency.digits
|
|
else:
|
|
digits = 2
|
|
return dict.fromkeys([j.id for j in journals], digits)
|
|
|
|
@classmethod
|
|
def get_debit_credit_balance(cls, journals, names):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
Move = pool.get('account.move')
|
|
context = Transaction().context
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
result = {}
|
|
ids = [j.id for j in journals]
|
|
for name in ['debit', 'credit', 'balance']:
|
|
result[name] = dict.fromkeys(ids, 0)
|
|
|
|
line = MoveLine.__table__()
|
|
move = Move.__table__()
|
|
where = ((move.date >= context['start_date'])
|
|
& (move.date <= context['end_date']))
|
|
for sub_journals in grouped_slice(journals):
|
|
sub_journals = list(sub_journals)
|
|
red_sql = reduce_ids(move.journal, [j.id for j in sub_journals])
|
|
accounts = None
|
|
for journal in sub_journals:
|
|
credit_account = (journal.credit_account.id
|
|
if journal.credit_account else None)
|
|
debit_account = (journal.debit_account.id
|
|
if journal.debit_account else None)
|
|
clause = ((move.journal == journal.id)
|
|
& (((line.credit != Null)
|
|
& (line.account == credit_account))
|
|
| ((line.debit != Null)
|
|
& (line.account == debit_account))))
|
|
if accounts is None:
|
|
accounts = clause
|
|
else:
|
|
accounts |= clause
|
|
|
|
query = line.join(move, 'LEFT', condition=line.move == move.id
|
|
).select(move.journal, Sum(line.debit), Sum(line.credit),
|
|
where=where & red_sql & accounts,
|
|
group_by=move.journal)
|
|
cursor.execute(*query)
|
|
for journal_id, debit, credit in cursor.fetchall():
|
|
# SQLite uses float for SUM
|
|
if not isinstance(debit, Decimal):
|
|
debit = Decimal(str(debit))
|
|
if not isinstance(credit, Decimal):
|
|
credit = Decimal(str(credit))
|
|
result['debit'][journal_id] = debit
|
|
result['credit'][journal_id] = credit
|
|
result['balance'][journal_id] = debit - credit
|
|
return result
|
|
|
|
|
|
class JournalSequence(ModelSQL, CompanyValueMixin):
|
|
"Journal Sequence"
|
|
__name__ = 'account.journal.sequence'
|
|
journal = fields.Many2One(
|
|
'account.journal', "Journal", ondelete='CASCADE', select=True)
|
|
sequence = fields.Many2One(
|
|
'ir.sequence', "Sequence",
|
|
domain=[
|
|
('code', '=', 'account.journal'),
|
|
('company', 'in', [Eval('company', -1), None]),
|
|
],
|
|
depends=['company'])
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
exist = TableHandler.table_exist(cls._table)
|
|
|
|
super(JournalSequence, cls).__register__(module_name)
|
|
|
|
if not exist:
|
|
cls._migrate_property([], [], [])
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.append('sequence')
|
|
value_names.append('sequence')
|
|
fields.append('company')
|
|
migrate_property(
|
|
'account.journal', field_names, cls, value_names,
|
|
parent='journal', fields=fields)
|
|
|
|
|
|
class JournalAccount(ModelSQL, CompanyValueMixin):
|
|
"Journal Account"
|
|
__name__ = 'account.journal.account'
|
|
journal = fields.Many2One(
|
|
'account.journal', "Journal", ondelete='CASCADE', select=True)
|
|
credit_account = fields.Many2One(
|
|
'account.account', "Default Credit Account",
|
|
domain=[
|
|
('kind', '!=', 'view'),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['company'])
|
|
debit_account = fields.Many2One(
|
|
'account.account', "Default Debit Account",
|
|
domain=[
|
|
('kind', '!=', 'view'),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['company'])
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
exist = TableHandler.table_exist(cls._table)
|
|
|
|
super(JournalAccount, cls).__register__(module_name)
|
|
|
|
if not exist:
|
|
cls._migrate_property([], [], [])
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.extend(['credit_account', 'debit_account'])
|
|
value_names.extend(['credit_account', 'debit_account'])
|
|
fields.append('company')
|
|
migrate_property(
|
|
'account.journal', field_names, cls, value_names,
|
|
parent='journal', fields=fields)
|
|
|
|
|
|
class JournalCashContext(ModelView):
|
|
'Journal Cash Context'
|
|
__name__ = 'account.journal.open_cash.context'
|
|
start_date = fields.Date('Start Date', required=True)
|
|
end_date = fields.Date('End Date', required=True)
|
|
|
|
@classmethod
|
|
def default_start_date(cls):
|
|
return Pool().get('ir.date').today()
|
|
default_end_date = default_start_date
|
|
|
|
|
|
class JournalPeriod(
|
|
DeactivableMixin, Workflow, ModelSQL, ModelView):
|
|
'Journal - Period'
|
|
__name__ = 'account.journal.period'
|
|
journal = fields.Many2One('account.journal', 'Journal', required=True,
|
|
ondelete='CASCADE', states=STATES, depends=DEPENDS)
|
|
period = fields.Many2One('account.period', 'Period', required=True,
|
|
ondelete='CASCADE', states=STATES, depends=DEPENDS)
|
|
icon = fields.Function(fields.Char('Icon'), 'get_icon')
|
|
state = fields.Selection([
|
|
('open', 'Open'),
|
|
('close', 'Close'),
|
|
], 'State', readonly=True, required=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(JournalPeriod, cls).__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('journal_period_uniq', Unique(t, t.journal, t.period),
|
|
'You can only open one journal per period.'),
|
|
]
|
|
|
|
cls._error_messages.update({
|
|
'modify_del_journal_period': ('You can not modify/delete '
|
|
'journal - period "%s" because it has moves.'),
|
|
'create_journal_period': ('You can not create a '
|
|
'journal - period on closed period "%s".'),
|
|
'open_journal_period': ('You can not open '
|
|
'journal - period "%(journal_period)s" because period '
|
|
'"%(period)s" is closed.'),
|
|
})
|
|
cls._transitions |= set((
|
|
('open', 'close'),
|
|
('close', 'open'),
|
|
))
|
|
cls._buttons.update({
|
|
'close': {
|
|
'invisible': Eval('state') != 'open',
|
|
'depends': ['state'],
|
|
},
|
|
'reopen': {
|
|
'invisible': Eval('state') != 'close',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
cls.active.states = STATES
|
|
cls.active.depends = DEPENDS
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
|
|
super(JournalPeriod, cls).__register__(module_name)
|
|
|
|
table = TableHandler(cls, module_name)
|
|
# Migration from 4.2: remove name column
|
|
table.drop_column('name')
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'open'
|
|
|
|
def get_rec_name(self, name):
|
|
return '%s - %s' % (self.journal.rec_name, self.period.rec_name)
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
return [bool_op,
|
|
[('journal.rec_name',) + tuple(clause[1:])],
|
|
[('period.rec_name',) + tuple(clause[1:])],
|
|
]
|
|
|
|
def get_icon(self, name):
|
|
return {
|
|
'open': 'tryton-open',
|
|
'close': 'tryton-close',
|
|
}.get(self.state)
|
|
|
|
@classmethod
|
|
def _check(cls, periods):
|
|
Move = Pool().get('account.move')
|
|
for period in periods:
|
|
moves = Move.search([
|
|
('journal', '=', period.journal.id),
|
|
('period', '=', period.period.id),
|
|
], limit=1)
|
|
if moves:
|
|
cls.raise_user_error('modify_del_journal_period', (
|
|
period.rec_name,))
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
Period = Pool().get('account.period')
|
|
for vals in vlist:
|
|
if vals.get('period'):
|
|
period = Period(vals['period'])
|
|
if period.state != 'open':
|
|
cls.raise_user_error('create_journal_period', (
|
|
period.rec_name,))
|
|
return super(JournalPeriod, cls).create(vlist)
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
actions = iter(args)
|
|
for journal_periods, values in zip(actions, actions):
|
|
if (values != {'state': 'close'}
|
|
and values != {'state': 'open'}):
|
|
cls._check(journal_periods)
|
|
if values.get('state') == 'open':
|
|
for journal_period in journal_periods:
|
|
if journal_period.period.state != 'open':
|
|
cls.raise_user_error('open_journal_period', {
|
|
'journal_period': journal_period.rec_name,
|
|
'period': journal_period.period.rec_name,
|
|
})
|
|
super(JournalPeriod, cls).write(*args)
|
|
|
|
@classmethod
|
|
def delete(cls, periods):
|
|
cls._check(periods)
|
|
super(JournalPeriod, cls).delete(periods)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('close')
|
|
def close(cls, periods):
|
|
'''
|
|
Close journal - period
|
|
'''
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('open')
|
|
def reopen(cls, periods):
|
|
"Open journal - period"
|
|
pass
|