first commit
This commit is contained in:
313
modules/account/journal.py
Normal file
313
modules/account/journal.py
Normal file
@@ -0,0 +1,313 @@
|
||||
# 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 sql.aggregate import Sum
|
||||
from sql.conditionals import Coalesce
|
||||
|
||||
from trytond import backend
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
DeactivableMixin, MatchMixin, ModelSQL, ModelView, Unique, Workflow,
|
||||
fields, sequence_ordered)
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools import (
|
||||
grouped_slice, is_full_text, lstrip_wildcard, reduce_ids,
|
||||
sqlite_apply_types)
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
STATES = {
|
||||
'readonly': Eval('state') == 'closed',
|
||||
}
|
||||
|
||||
|
||||
class Journal(
|
||||
DeactivableMixin, MatchMixin,
|
||||
sequence_ordered('matching_sequence', "Matching Sequence"),
|
||||
ModelSQL, ModelView):
|
||||
__name__ = 'account.journal'
|
||||
name = fields.Char('Name', size=None, required=True, translate=True)
|
||||
code = fields.Char('Code', size=None)
|
||||
type = fields.Selection([
|
||||
('general', "General"),
|
||||
('revenue', "Revenue"),
|
||||
('expense', "Expense"),
|
||||
('cash', "Cash"),
|
||||
('situation', "Situation"),
|
||||
('write-off', "Write-Off"),
|
||||
], 'Type', required=True)
|
||||
debit = fields.Function(Monetary(
|
||||
"Debit", currency='currency', digits='currency'),
|
||||
'get_debit_credit_balance')
|
||||
credit = fields.Function(Monetary(
|
||||
"Credit", currency='currency', digits='currency'),
|
||||
'get_debit_credit_balance')
|
||||
balance = fields.Function(Monetary(
|
||||
"Balance", currency='currency', digits='currency'),
|
||||
'get_debit_credit_balance')
|
||||
|
||||
currency = fields.Function(fields.Many2One(
|
||||
'currency.currency', "Currency"), 'get_currency')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._order.insert(0, ('name', 'ASC'))
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
_, operator, operand, *extra = clause
|
||||
if operator.startswith('!') or operator.startswith('not '):
|
||||
bool_op = 'AND'
|
||||
else:
|
||||
bool_op = 'OR'
|
||||
code_value = operand
|
||||
if operator.endswith('like') and is_full_text(operand):
|
||||
code_value = lstrip_wildcard(operand)
|
||||
return [bool_op,
|
||||
('code', operator, code_value, *extra),
|
||||
(cls._rec_name, operator, operand, *extra),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_currency(cls, journals, name):
|
||||
pool = Pool()
|
||||
Company = pool.get('company.company')
|
||||
company_id = Transaction().context.get('company')
|
||||
if company_id is not None and company_id >= 0:
|
||||
company = Company(company_id)
|
||||
currency_id = company.currency.id
|
||||
else:
|
||||
currency_id = None
|
||||
return dict.fromkeys([j.id for j in journals], currency_id)
|
||||
|
||||
@classmethod
|
||||
def get_debit_credit_balance(cls, journals, names):
|
||||
pool = Pool()
|
||||
MoveLine = pool.get('account.move.line')
|
||||
Move = pool.get('account.move')
|
||||
Account = pool.get('account.account')
|
||||
AccountType = pool.get('account.account.type')
|
||||
Company = pool.get('company.company')
|
||||
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)
|
||||
|
||||
company_id = Transaction().context.get('company')
|
||||
if not company_id:
|
||||
return result
|
||||
company = Company(company_id)
|
||||
|
||||
line = MoveLine.__table__()
|
||||
move = Move.__table__()
|
||||
account = Account.__table__()
|
||||
account_type = AccountType.__table__()
|
||||
where = ((move.date >= context.get('start_date'))
|
||||
& (move.date <= context.get('end_date'))
|
||||
& ~Coalesce(account_type.receivable, False)
|
||||
& ~Coalesce(account_type.payable, False)
|
||||
& (move.company == company.id))
|
||||
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])
|
||||
query = line.join(move, 'LEFT', condition=line.move == move.id
|
||||
).join(account, 'LEFT', condition=line.account == account.id
|
||||
).join(account_type, 'LEFT',
|
||||
condition=account.type == account_type.id
|
||||
).select(
|
||||
move.journal,
|
||||
Sum(line.debit).as_('debit'),
|
||||
Sum(line.credit).as_('credit'),
|
||||
where=where & red_sql,
|
||||
group_by=move.journal)
|
||||
if backend.name == 'sqlite':
|
||||
sqlite_apply_types(query, [None, 'NUMERIC', 'NUMERIC'])
|
||||
cursor.execute(*query)
|
||||
for journal_id, debit, credit in cursor:
|
||||
result['debit'][journal_id] = company.currency.round(debit)
|
||||
result['credit'][journal_id] = company.currency.round(credit)
|
||||
result['balance'][journal_id] = company.currency.round(
|
||||
debit - credit)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def find(cls, pattern):
|
||||
for journal in cls.search(
|
||||
[],
|
||||
order=[
|
||||
('matching_sequence', 'ASC'),
|
||||
('id', 'ASC'),
|
||||
]):
|
||||
if journal.match(pattern):
|
||||
return journal
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, journals, values=None, external=False):
|
||||
pool = Pool()
|
||||
Move = pool.get('account.move')
|
||||
super().check_modification(
|
||||
mode, journals, values=values, external=external)
|
||||
if mode == 'write' and 'type' in values:
|
||||
for sub_journals in grouped_slice(journals):
|
||||
moves = Move.search([
|
||||
('journal', 'in', [j.id for j in sub_journals]),
|
||||
('state', '=', 'posted')
|
||||
], order=[], limit=1)
|
||||
if moves:
|
||||
move, = moves
|
||||
raise AccessError(gettext(
|
||||
'account.msg_journal_account_moves',
|
||||
journal=move.journal.rec_name))
|
||||
|
||||
|
||||
class JournalCashContext(ModelView):
|
||||
__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(Workflow, ModelSQL, ModelView):
|
||||
__name__ = 'account.journal.period'
|
||||
journal = fields.Many2One(
|
||||
'account.journal', 'Journal', required=True, ondelete='CASCADE',
|
||||
states=STATES,
|
||||
context={
|
||||
'company': Eval('company', None),
|
||||
},
|
||||
depends=['company'])
|
||||
period = fields.Many2One('account.period', 'Period', required=True,
|
||||
ondelete='CASCADE', states=STATES)
|
||||
company = fields.Function(fields.Many2One(
|
||||
'company.company', "Company"),
|
||||
'on_change_with_company', searcher='search_company')
|
||||
icon = fields.Function(fields.Char('Icon'), 'get_icon')
|
||||
state = fields.Selection([
|
||||
('open', 'Open'),
|
||||
('closed', 'Closed'),
|
||||
], 'State', readonly=True, required=True, sort=False)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
t = cls.__table__()
|
||||
cls._sql_constraints += [
|
||||
('journal_period_uniq', Unique(t, t.journal, t.period),
|
||||
'account.msg_journal_period_unique'),
|
||||
]
|
||||
cls._transitions |= set((
|
||||
('open', 'closed'),
|
||||
('closed', 'open'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'close': {
|
||||
'invisible': Eval('state') != 'open',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'reopen': {
|
||||
'invisible': Eval('state') != 'closed',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@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'))
|
||||
|
||||
@fields.depends('period')
|
||||
def on_change_with_company(self, name=None):
|
||||
return self.period.company if self.period else None
|
||||
|
||||
@classmethod
|
||||
def search_company(cls, name, clause):
|
||||
return [('period.' + clause[0], *clause[1:])]
|
||||
|
||||
@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-account-open',
|
||||
'closed': 'tryton-account-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:
|
||||
raise AccessError(
|
||||
gettext('account.msg_modify_delete_journal_period_moves',
|
||||
journal_period=period.rec_name))
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, records, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, records, values=values, external=external)
|
||||
if mode == 'create':
|
||||
for record in records:
|
||||
if record.period.state != 'open':
|
||||
raise AccessError(gettext(
|
||||
'account.msg_create_journal_period_closed_period',
|
||||
period=record.period.rec_name))
|
||||
elif mode in {'write', 'delete'}:
|
||||
if values != {'state': 'closed'} and values != {'state': 'open'}:
|
||||
cls._check(records)
|
||||
if mode == 'write' and values.get('state') == 'open':
|
||||
for record in records:
|
||||
if record.period.state != 'open':
|
||||
raise AccessError(gettext(
|
||||
'account.'
|
||||
'msg_open_journal_period_closed_period',
|
||||
journal_period=record.rec_name,
|
||||
period=record.period.rec_name))
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('closed')
|
||||
def close(cls, periods):
|
||||
'''
|
||||
Close journal - period
|
||||
'''
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('open')
|
||||
def reopen(cls, periods):
|
||||
"Open journal - period"
|
||||
pass
|
||||
Reference in New Issue
Block a user