# -*- 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 def get_values_wfedit(cls, reconciliation): """ get values for 'to_write' in wf-edit """ 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])] return values @classmethod def get_values_wfcheck(cls, reconciliation): """ get values for 'to_write' in wf-check """ Line = Pool().get('cashbook.line') values = {} if reconciliation.predecessor: 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]) return values @classmethod @ModelView.button @Workflow.transition('edit') def wfedit(cls, reconciliations): """ edit """ Recon = Pool().get('cashbook.recon') to_write = [] for reconciliation in reconciliations: to_write.extend([ [reconciliation], cls.get_values_wfedit(reconciliation), ]) 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' """ Recon = Pool().get('cashbook.recon') cls.check_lines_not_checked(reconciliations) to_write = [] for reconciliation in reconciliations: 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, )) to_write.extend([ [reconciliation], cls.get_values_wfcheck(reconciliation), ]) 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