471 lines
17 KiB
Python
471 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# This file is part of the cashbook-module from m-ds for Tryton.
|
|
# The COPYRIGHT file at the top level of this repository contains the
|
|
# full copyright notices and license terms.
|
|
|
|
from trytond.model import Workflow, ModelView, ModelSQL, fields
|
|
from trytond.transaction import Transaction
|
|
from trytond.pyson import Eval, If, Or, Bool
|
|
from trytond.pool import Pool
|
|
from trytond.report import Report
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
from decimal import Decimal
|
|
from sql.operators import Equal, Between
|
|
from sql import Literal, Null
|
|
from datetime import timedelta
|
|
from .book import sel_state_book
|
|
|
|
|
|
sel_reconstate = [
|
|
('edit', 'Edit'),
|
|
('check', 'Check'),
|
|
('done', 'Done'),
|
|
]
|
|
|
|
STATES = {
|
|
'readonly': Or(
|
|
Eval('state', '') != 'edit',
|
|
Eval('state_cashbook', '') != 'open',
|
|
),
|
|
}
|
|
DEPENDS=['state', 'state_cashbook']
|
|
|
|
|
|
class Reconciliation(Workflow, ModelSQL, ModelView):
|
|
'Cashbook Reconciliation'
|
|
__name__ = 'cashbook.recon'
|
|
|
|
cashbook = fields.Many2One(string='Cashbook', required=True, select=True,
|
|
model_name='cashbook.book', ondelete='CASCADE', readonly=True)
|
|
date = fields.Date(string='Date', required=True, select=True,
|
|
states=STATES, depends=DEPENDS)
|
|
feature = fields.Function(fields.Char(string='Feature', readonly=True,
|
|
states={'invisible': True}), 'on_change_with_feature')
|
|
|
|
date_from = fields.Date(string='Start Date',
|
|
required=True,
|
|
domain=[
|
|
If(Eval('date_to') & Eval('date_from'),
|
|
('date_from', '<=', Eval('date_to')),
|
|
()),
|
|
],
|
|
states=STATES, depends=DEPENDS+['date_to'])
|
|
date_to = fields.Date(string='End Date',
|
|
required=True,
|
|
domain=[
|
|
If(Eval('date_to') & Eval('date_from'),
|
|
('date_from', '<=', Eval('date_to')),
|
|
()),
|
|
],
|
|
states=STATES, depends=DEPENDS+['date_from'])
|
|
start_amount = fields.Numeric(string='Start Amount', required=True,
|
|
readonly=True, digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits'])
|
|
end_amount = fields.Numeric(string='End Amount', required=True,
|
|
readonly=True, digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits'])
|
|
|
|
lines = fields.One2Many(string='Lines', field='reconciliation',
|
|
model_name='cashbook.line', states=STATES,
|
|
depends=DEPENDS+['date_from', 'date_to', 'cashbook'],
|
|
add_remove=[
|
|
('cashbook', '=', Eval('cashbook')),
|
|
('state', 'in', ['check', 'recon', 'done']),
|
|
('date', '>=', Eval('date_from')),
|
|
('date', '<=', Eval('date_to')),
|
|
],
|
|
domain=[
|
|
('date', '>=', Eval('date_from')),
|
|
('date', '<=', Eval('date_to')),
|
|
])
|
|
|
|
currency = fields.Function(fields.Many2One(model_name='currency.currency',
|
|
string="Currency"), 'on_change_with_currency')
|
|
currency_digits = fields.Function(fields.Integer(string='Currency Digits'),
|
|
'on_change_with_currency_digits')
|
|
predecessor = fields.Function(fields.Many2One(string='Predecessor', readonly=True,
|
|
model_name='cashbook.recon'),
|
|
'on_change_with_predecessor')
|
|
|
|
state = fields.Selection(string='State', required=True, readonly=True,
|
|
select=True, selection=sel_reconstate)
|
|
state_string = state.translated('state')
|
|
state_cashbook = fields.Function(fields.Selection(string='State of Cashbook',
|
|
readonly=True, states={'invisible': True}, selection=sel_state_book),
|
|
'on_change_with_state_cashbook')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Reconciliation, cls).__setup__()
|
|
cls._order.insert(0, ('date_from', 'DESC'))
|
|
cls._transitions |= set((
|
|
('edit', 'check'),
|
|
('check', 'done'),
|
|
('check', 'edit'),
|
|
))
|
|
cls._buttons.update({
|
|
'wfedit': {
|
|
'invisible': Eval('state', '') != 'check',
|
|
'depends': ['state'],
|
|
},
|
|
'wfcheck': {
|
|
'invisible': Eval('state') != 'edit',
|
|
'depends': ['state'],
|
|
},
|
|
'wfdone': {
|
|
'invisible': Eval('state') != 'check',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
def check_overlap_dates(self):
|
|
""" deny overlap of date_from/date_to between records of same cashbook
|
|
allow: date_to=date_from
|
|
"""
|
|
Recon = Pool().get('cashbook.recon')
|
|
|
|
query = [
|
|
('cashbook.id', '=', self.cashbook.id),
|
|
('id', '!=', self.id),
|
|
['OR',
|
|
[ # 'start' is inside of other record
|
|
('date_from', '<=', self.date_from),
|
|
('date_to', '>', self.date_from),
|
|
],
|
|
[ # 'end' is inside of other record
|
|
('date_from', '<', self.date_to),
|
|
('date_to', '>=', self.date_to),
|
|
],
|
|
[ # enclose other record
|
|
('date_from', '>=', self.date_from),
|
|
('date_to', '<=', self.date_to),
|
|
],
|
|
],
|
|
]
|
|
|
|
if Recon.search_count(query) > 0:
|
|
raise UserError(gettext('cashbook.msg_recon_err_overlap'))
|
|
|
|
@classmethod
|
|
def check_lines_not_checked(cls, reconciliations):
|
|
""" deny lines in date-range not 'checked', w/o records at date-limit
|
|
"""
|
|
Line = Pool().get('cashbook.line')
|
|
|
|
for reconciliation in reconciliations:
|
|
if Line.search_count([
|
|
('date', '>', reconciliation.date_from),
|
|
('date', '<', reconciliation.date_to),
|
|
('cashbook.id', '=', reconciliation.cashbook.id),
|
|
('state', 'not in', ['check', 'recon']),
|
|
]) > 0:
|
|
raise UserError(gettext(
|
|
'cashbook.mds_recon_deny_line_not_check',
|
|
bookname = reconciliation.cashbook.rec_name,
|
|
reconame = reconciliation.rec_name,
|
|
datefrom = Report.format_date(reconciliation.date_from),
|
|
dateto = Report.format_date(reconciliation.date_to),
|
|
))
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('edit')
|
|
def wfedit(cls, reconciliations):
|
|
""" edit
|
|
"""
|
|
Recon = Pool().get('cashbook.recon')
|
|
|
|
to_write = []
|
|
for reconciliation in reconciliations:
|
|
values = {
|
|
'start_amount': Decimal('0.0'),
|
|
'end_amount': Decimal('0.0'),
|
|
}
|
|
|
|
# unlink lines from reconciliation
|
|
if len(reconciliation.lines) > 0:
|
|
values['lines'] = [('remove', [x.id for x in reconciliation.lines])]
|
|
|
|
to_write.extend([[reconciliation], values])
|
|
|
|
if len(to_write) > 0:
|
|
Recon.write(*to_write)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('check')
|
|
def wfcheck(cls, reconciliations):
|
|
""" checked: add lines of book in date-range to reconciliation,
|
|
state of lines must be 'checked'
|
|
"""
|
|
pool = Pool()
|
|
Line = pool.get('cashbook.line')
|
|
Recon = pool.get('cashbook.recon')
|
|
|
|
cls.check_lines_not_checked(reconciliations)
|
|
|
|
to_write = []
|
|
for reconciliation in reconciliations:
|
|
values = {}
|
|
|
|
if reconciliation.predecessor:
|
|
# predecessor must be 'done'
|
|
if reconciliation.predecessor.state != 'done':
|
|
raise UserError(gettext(
|
|
'cashbook.msg_recon_predecessor_not_done',
|
|
recname_p = reconciliation.predecessor.rec_name,
|
|
recname_c = reconciliation.rec_name,
|
|
))
|
|
|
|
# check if current.date_from == predecessor.date_to
|
|
if reconciliation.predecessor.date_to != reconciliation.date_from:
|
|
raise UserError(gettext(
|
|
'cashbook.msg_recon_date_from_to_mismatch',
|
|
datefrom = Report.format_date(reconciliation.date_from),
|
|
dateto = Report.format_date(reconciliation.predecessor.date_to),
|
|
recname = reconciliation.rec_name,
|
|
))
|
|
values['start_amount'] = reconciliation.predecessor.end_amount
|
|
else :
|
|
values['start_amount'] = Decimal('0.0')
|
|
values['end_amount'] = values['start_amount']
|
|
|
|
# add 'checked'-lines to reconciliation
|
|
lines = Line.search([
|
|
('date', '>=', reconciliation.date_from),
|
|
('date', '<=', reconciliation.date_to),
|
|
('cashbook.id', '=', reconciliation.cashbook.id),
|
|
('reconciliation', '=', None),
|
|
('state', 'in', ['check', 'recon']),
|
|
])
|
|
if len(lines) > 0:
|
|
values['lines'] = [('add', [x.id for x in lines])]
|
|
|
|
# add amounts of new lines
|
|
values['end_amount'] += sum([x.credit - x.debit for x in lines])
|
|
# add amounts of already linked lines
|
|
values['end_amount'] += sum([x.credit - x.debit for x in reconciliation.lines])
|
|
|
|
to_write.extend([[reconciliation], values])
|
|
|
|
if len(to_write) > 0:
|
|
Recon.write(*to_write)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
def wfdone(cls, reconciliations):
|
|
""" is done
|
|
"""
|
|
Line = Pool().get('cashbook.line')
|
|
|
|
to_wfdone_line = []
|
|
to_wfrecon_line = []
|
|
for reconciliation in reconciliations:
|
|
to_wfrecon_line.extend([
|
|
x for x in reconciliation.lines \
|
|
if x.state == 'check'
|
|
])
|
|
to_wfdone_line.extend([
|
|
x for x in reconciliation.lines \
|
|
if x.state == 'recon'
|
|
])
|
|
|
|
# deny if there are lines not linked to reconciliation
|
|
if Line.search_count([
|
|
('cashbook.id', '=', reconciliation.cashbook.id),
|
|
('reconciliation', '=', None),
|
|
['OR',
|
|
[ # lines inside of date-range
|
|
('date', '>', reconciliation.date_from),
|
|
('date', '<', reconciliation.date_to),
|
|
],
|
|
# lines at from-date must relate to a reconciliation
|
|
('date', '=', reconciliation.date_from),
|
|
],
|
|
]) > 0:
|
|
raise UserError(gettext(
|
|
'cashbook.msg_recon_lines_no_linked',
|
|
date_from = Report.format_date(reconciliation.date_from),
|
|
date_to = Report.format_date(reconciliation.date_to),
|
|
))
|
|
|
|
if len(to_wfrecon_line) > 0:
|
|
Line.wfrecon(to_wfrecon_line)
|
|
to_wfdone_line.extend(to_wfrecon_line)
|
|
if len(to_wfdone_line) > 0:
|
|
Line.wfdone(to_wfdone_line)
|
|
|
|
def get_rec_name(self, name):
|
|
""" short + name
|
|
"""
|
|
return '%(from)s - %(to)s | %(start_amount)s %(symbol)s - %(end_amount)s %(symbol)s [%(num)s]' % {
|
|
'from': Report.format_date(self.date_from, None) if self.date_from is not None else '-',
|
|
'to': Report.format_date(self.date_to, None) if self.date_to is not None else '-',
|
|
'start_amount': Report.format_number(self.start_amount or 0.0, None),
|
|
'end_amount': Report.format_number(self.end_amount or 0.0, None),
|
|
'symbol': getattr(self.currency, 'symbol', '-'),
|
|
'num': len(self.lines),
|
|
}
|
|
|
|
@classmethod
|
|
def default_date_from(cls):
|
|
""" 1st day of current month
|
|
"""
|
|
return Pool().get('ir.date').today().replace(day=1)
|
|
|
|
@classmethod
|
|
def default_date_to(cls):
|
|
""" last day of current month
|
|
"""
|
|
IrDate = Pool().get('ir.date')
|
|
|
|
dt1 = IrDate.today().replace(day=28) + timedelta(days=5)
|
|
dt1 = dt1.replace(day=1) - timedelta(days=1)
|
|
return dt1
|
|
|
|
@classmethod
|
|
def default_start_amount(cls):
|
|
return Decimal('0.0')
|
|
|
|
@classmethod
|
|
def default_end_amount(cls):
|
|
return Decimal('0.0')
|
|
|
|
@classmethod
|
|
def default_state(cls):
|
|
return 'edit'
|
|
|
|
@classmethod
|
|
def default_date(cls):
|
|
""" today
|
|
"""
|
|
IrDate = Pool().get('ir.date')
|
|
return IrDate.today()
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.btype')
|
|
def on_change_with_feature(self, name=None):
|
|
""" get feature-set
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.btype.feature
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.id', 'date_from')
|
|
def on_change_with_predecessor(self, name=None):
|
|
""" get predecessor
|
|
"""
|
|
Recon = Pool().get('cashbook.recon')
|
|
|
|
if self.cashbook:
|
|
if self.date_from is not None:
|
|
reconciliations = Recon.search([
|
|
('cashbook.id', '=', self.cashbook.id),
|
|
('date_from', '<', self.date_from),
|
|
], order=[('date_from', 'DESC')], limit=1)
|
|
if len(reconciliations) > 0:
|
|
return reconciliations[0].id
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.currency')
|
|
def on_change_with_currency(self, name=None):
|
|
""" currency of cashbook
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.currency.id
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.currency')
|
|
def on_change_with_currency_digits(self, name=None):
|
|
""" currency-digits of cashbook
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.currency.digits
|
|
else:
|
|
return 2
|
|
|
|
@fields.depends('cashbook', '_parent_cashbook.state')
|
|
def on_change_with_state_cashbook(self, name=None):
|
|
""" get state of cashbook
|
|
"""
|
|
if self.cashbook:
|
|
return self.cashbook.state
|
|
|
|
@classmethod
|
|
def validate(cls, reconciliations):
|
|
""" deny overlap of dates
|
|
"""
|
|
super(Reconciliation, cls).validate(reconciliations)
|
|
|
|
for reconciliation in reconciliations:
|
|
reconciliation.check_overlap_dates()
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
""" add debit/credit
|
|
"""
|
|
pool = Pool()
|
|
Recon = pool.get('cashbook.recon')
|
|
Line = pool.get('cashbook.line')
|
|
Cashbook = pool.get('cashbook.book')
|
|
|
|
for values in vlist:
|
|
id_cashbook = values.get('cashbook', -1)
|
|
|
|
# set date_from to date_to of predecessor
|
|
recons = Recon.search([
|
|
('cashbook.id', '=', id_cashbook),
|
|
], order=[('date_to', 'DESC')], limit=1)
|
|
if len(recons) > 0:
|
|
values['date_from'] = recons[0].date_to
|
|
elif id_cashbook != -1:
|
|
values['date_from'] = Cashbook(id_cashbook).start_date
|
|
|
|
# set date_to to day of last 'checked'-booking in selected cashbook
|
|
lines = Line.search([
|
|
('cashbook.id', '=', id_cashbook),
|
|
('state', '=', 'check'),
|
|
('reconciliation', '=', None),
|
|
], order=[('date', 'DESC')], limit=1)
|
|
if len(lines) > 0:
|
|
values['date_to'] = lines[0].date
|
|
|
|
return super(Reconciliation, cls).create(vlist)
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
""" deny update if cashbook.line!='open',
|
|
add or update debit/credit
|
|
"""
|
|
actions = iter(args)
|
|
for reconciliations, values in zip(actions, actions):
|
|
# deny write if chashbook is not open
|
|
for reconciliation in reconciliations:
|
|
if reconciliation.cashbook.state != 'open':
|
|
raise UserError(gettext(
|
|
'cashbook.msg_book_deny_write',
|
|
bookname = reconciliation.cashbook.rec_name,
|
|
state_txt = reconciliation.cashbook.state_string,
|
|
))
|
|
super(Reconciliation, cls).write(*args)
|
|
|
|
@classmethod
|
|
def delete(cls, reconciliations):
|
|
""" deny delete if book is not 'open' or wf is not 'edit'
|
|
"""
|
|
for reconciliation in reconciliations:
|
|
if reconciliation.cashbook.state == 'closed':
|
|
raise UserError(gettext(
|
|
'cashbook.msg_line_deny_delete1',
|
|
linetxt = reconciliation.rec_name,
|
|
bookname = reconciliation.cashbook.rec_name,
|
|
bookstate = reconciliation.cashbook.state_string,
|
|
))
|
|
if reconciliation.state != 'edit':
|
|
raise UserError(gettext(
|
|
'cashbook.msg_recon_deny_delete2',
|
|
recontxt = reconciliation.rec_name,
|
|
reconstate = reconciliation.state_string,
|
|
))
|
|
|
|
super(Reconciliation, cls).delete(reconciliations)
|
|
|
|
# end Type
|