# -*- 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 ModelView, ModelSQL, Workflow, fields, Check from trytond.pool import Pool from trytond.pyson import Eval, If, Or, Bool from trytond.transaction import Transaction from trytond.report import Report from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.modules.currency.ir import rate_decimal from decimal import Decimal from sql import Cast, Literal from sql.functions import DatePart from sql.conditionals import Case from .book import sel_state_book sel_payee = [ ('cashbook.book', 'Cashbook'), ('party.party', 'Party') ] sel_linetype = [ ('edit', 'Edit'), ('check', 'Checked'), ('done', 'Done'), ] sel_bookingtype = [ ('in', 'Revenue'), ('out', 'Expense'), ('spin', 'Revenue Splitbooking'), ('spout', 'Expense Splitbooking'), ('mvin', 'Transfer from'), ('mvout', 'Transfer to'), ] STATES = { 'readonly': Or( Eval('state', '') != 'edit', Eval('state_cashbook', '') != 'open', ), } DEPENDS=['state', 'state_cashbook'] class Line(Workflow, ModelSQL, ModelView): 'Cashbook Line' __name__ = 'cashbook.line' cashbook = fields.Many2One(string='Cashbook', required=True, select=True, model_name='cashbook.book', ondelete='CASCADE', readonly=True, domain=[('btype', '!=', None)]) date = fields.Date(string='Date', required=True, select=True, states=STATES, depends=DEPENDS) month = fields.Function(fields.Integer(string='Month', readonly=True), 'on_change_with_month', searcher='search_month') number = fields.Char(string='Number', readonly=True) description = fields.Text(string='Description', select=True, states=STATES, depends=DEPENDS) descr_short = fields.Function(fields.Char(string='Description', readonly=True), 'on_change_with_descr_short', searcher='search_descr_short') category = fields.Many2One(string='Category', model_name='cashbook.category', ondelete='RESTRICT', states={ 'readonly': Or( STATES['readonly'], Bool(Eval('bookingtype')) == False, ), 'required': Eval('bookingtype', '').in_(['in', 'out']), 'invisible': ~Eval('bookingtype', '').in_(['in', 'out']), }, depends=DEPENDS+['bookingtype'], domain=[ If( Eval('bookingtype', '').in_(['in', 'mvin']), ('cattype', '=', 'in'), ('cattype', '=', 'out'), )]) category_view = fields.Function(fields.Char(string='Category', readonly=True), 'on_change_with_category_view', searcher='search_category_view') bookingtype = fields.Selection(string='Type', required=True, help='Type of Booking', selection=sel_bookingtype, states=STATES, depends=DEPENDS) bookingtype_string = bookingtype.translated('bookingtype') amount = fields.Numeric(string='Amount', digits=(16, Eval('currency_digits', 2)), required=True, states={ 'readonly': Or( STATES['readonly'], Eval('bookingtype', '').in_(['spin', 'spout']), ), }, depends=DEPENDS+['currency_digits', 'bookingtype']) debit = fields.Numeric(string='Debit', digits=(16, Eval('currency_digits', 2)), required=True, readonly=True, depends=['currency_digits']) credit = fields.Numeric(string='Credit', digits=(16, Eval('currency_digits', 2)), required=True, readonly=True, depends=['currency_digits']) # party or cashbook as counterpart booktransf = fields.Many2One(string='Source/Dest', ondelete='RESTRICT', model_name='cashbook.book', domain=[ ('owner.id', '=', Eval('owner_cashbook', -1)), ('id', '!=', Eval('cashbook', -1)), ], states={ 'readonly': STATES['readonly'], 'invisible': ~Eval('bookingtype', '').in_(['mvin', 'mvout']), 'required': Eval('bookingtype', '').in_(['mvin', 'mvout']), }, depends=DEPENDS+['bookingtype', 'owner_cashbook', 'cashbook']) party = fields.Many2One(string='Party', model_name='party.party', ondelete='RESTRICT', states={ 'readonly': STATES['readonly'], 'invisible': ~Eval('bookingtype', '').in_(['in', 'out', 'spin', 'spout']), }, depends=DEPENDS+['bookingtype']) payee = fields.Function(fields.Reference(string='Payee', readonly=True, selection=sel_payee), 'on_change_with_payee', searcher='search_payee') amount_2nd_currency = fields.Numeric(string='Amount Second Currency', digits=(16, Eval('currency2nd_digits', 2)), states={ 'readonly': STATES['readonly'], 'required': Bool(Eval('currency2nd')), 'invisible': ~Bool(Eval('currency2nd')), }, depends=DEPENDS+['currency2nd_digits', 'currency2nd']) rate_2nd_currency = fields.Function(fields.Numeric(string='Rate', help='Exchange rate between the currencies of the participating cashbooks.', digits=(rate_decimal * 2, rate_decimal), states={ 'readonly': STATES['readonly'], 'required': Bool(Eval('currency2nd')), 'invisible': ~Bool(Eval('currency2nd')), }, depends=DEPENDS+['currency2nd_digits', 'currency2nd']), 'on_change_with_rate_2nd_currency', setter='set_rate_2nd_currency') # link to lines created by this record reference = fields.Many2One(string='Reference', readonly=True, select=True, states={ 'invisible': ~Bool(Eval('reference')), }, model_name='cashbook.line', ondelete='CASCADE', help='The current row was created by and is controlled by the reference row.') references = fields.One2Many(string='References', model_name='cashbook.line', help='The rows are created and managed by the current record.', states={ 'invisible': ~Bool(Eval('references')), }, field='reference', readonly=True) splitlines = fields.One2Many(string='Split booking lines', model_name='cashbook.split', help='Rows with different categories form the total sum of the booking', states={ 'invisible': ~Eval('bookingtype' '').in_(['spin', 'spout']), 'readonly': Or( ~Eval('bookingtype' '').in_(['spin', 'spout']), STATES['readonly'], ), 'required': Eval('bookingtype' '').in_(['spin', 'spout']), }, field='line', depends=DEPENDS+['bookingtype']) reconciliation = fields.Many2One(string='Reconciliation', readonly=True, model_name='cashbook.recon', ondelete='SET NULL', domain=[('cashbook.id', '=', Eval('cashbook'))], depends=['cashbook'], states={ 'invisible': ~Bool(Eval('reconciliation')), }) balance = fields.Function(fields.Numeric(string='Balance', digits=(16, Eval('currency_digits', 2)), help='Balance of the cash book up to the current line, if the default sorting applies.', readonly=True, depends=['currency_digits']), 'on_change_with_balance') currency = fields.Function(fields.Many2One(model_name='currency.currency', string="Currency", readonly=True), 'on_change_with_currency') currency_digits = fields.Function(fields.Integer(string='Currency Digits', readonly=True), 'on_change_with_currency_digits') currency2nd = fields.Function(fields.Many2One(model_name='currency.currency', string="2nd Currency", readonly=True), 'on_change_with_currency2nd') currency2nd_digits = fields.Function(fields.Integer(string='2nd Currency Digits', readonly=True), 'on_change_with_currency2nd_digits') state = fields.Selection(string='State', required=True, readonly=True, select=True, selection=sel_linetype) 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', searcher='search_state_cashbook') owner_cashbook = fields.Function(fields.Many2One(string='Owner', readonly=True, states={'invisible': True}, model_name='res.user'), 'on_change_with_owner_cashbook') #image = fields.Binary... @classmethod def __register__(cls, module_name): super(Line, cls).__register__(module_name) table = cls.__table_handler__(module_name) table.drop_constraint('amount_val') @classmethod def __setup__(cls): super(Line, cls).__setup__() cls._order.insert(0, ('date', 'ASC')) cls._order.insert(0, ('state', 'ASC')) t = cls.__table__() cls._sql_constraints.extend([ ('state_val', Check(t, t.state.in_(['edit', 'check', 'done'])), 'cashbook.msg_line_wrong_state_value'), ]) cls._transitions |= set(( ('edit', 'check'), ('check', 'done'), ('check', 'edit'), )) cls._buttons.update({ 'wfedit': { 'invisible': Eval('state', '') != 'check', 'readonly': Bool(Eval('reference')), 'depends': ['state', 'reference'], }, 'wfcheck': { 'invisible': Eval('state') != 'edit', 'depends': ['state'], }, 'wfdone': { 'invisible': Eval('state') != 'check', 'depends': ['state'], }, }) @classmethod @ModelView.button @Workflow.transition('edit') def wfedit(cls, lines): """ edit line """ pool = Pool() Line2 = pool.get('cashbook.line') to_delete_line = [] for line in lines: if line.reference: if Transaction().context.get('line.allow.wfedit', False) == False: raise UserError(gettext( 'cashbook.msg_line_denywf_by_reference', recname = line.reference.rec_name, cbook = line.reference.cashbook.rec_name, )) # delete references to_delete_line.extend(list(line.references)) if len(to_delete_line) > 0: with Transaction().set_context({ 'line.allow.wfedit': True, }): Line2.wfedit(to_delete_line) Line2.delete(to_delete_line) @classmethod @ModelView.button @Workflow.transition('check') def wfcheck(cls, lines): """ line is checked """ pool = Pool() Recon = pool.get('cashbook.recon') Line2 = pool.get('cashbook.line') to_create_line = [] to_write_line = [] for line in lines: # deny if date is in range of existing reconciliation # allow cashbook-line at range-limits if Recon.search_count([ ('state', 'in', ['check', 'done']), ('cashbook.id', '=', line.cashbook.id), ('date_from', '<', line.date), ('date_to', '>', line.date), ]) > 0: raise UserError(gettext( 'cashbook.msg_line_err_write_to_reconciled', datetxt = Report.format_date(line.date), )) # deny if date is at reconciliation limits and two # reconciliations exist if Recon.search_count([ ('state', 'in', ['check', 'done']), ('cashbook.id', '=', line.cashbook.id), ['OR', ('date_from', '=', line.date), ('date_to', '=', line.date), ] ]) > 1: raise UserError(gettext( 'cashbook.msg_line_err_write_to_reconciled', datetxt = Report.format_date(line.date), )) if line.reference is None: if line.bookingtype in ['mvout', 'mvin']: # in case of 'mvin' or 'mvout' - add counterpart values = { 'cashbook': line.booktransf.id, 'bookingtype': 'mvin' if line.bookingtype == 'mvout' else 'mvout', 'date': line.date, 'description': line.description, 'booktransf': line.cashbook.id, 'reference': line.id, } values.update(line.get_amount_by_second_currency(line.booktransf.currency)) values.update(cls.get_debit_credit(values)) to_create_line.append(values) elif line.bookingtype in ['spout', 'spin']: # splitbooking can have a transfer - add counterpart for sp_line in line.splitlines: if sp_line.splittype != 'tr': continue values = { 'cashbook': sp_line.booktransf.id, 'date': line.date, 'description': sp_line.description, 'booktransf': line.cashbook.id, 'reference': line.id, } if line.bookingtype.endswith('out'): values['bookingtype'] = 'mvin' else : values['bookingtype'] = 'mvout' values.update(line.get_amount_by_second_currency( sp_line.booktransf.currency, amount = sp_line.amount, )) values.update(cls.get_debit_credit(values)) to_create_line.append(values) # add number to line if line.cashbook.number_atcheck == True: if len(line.number or '') == 0: to_write_line.extend([ [line], { 'number': line.cashbook.number_sequ.get() }]) if len(to_write_line) > 0: Line2.write(*to_write_line) if len(to_create_line) > 0: new_lines = Line2.create(to_create_line) Line2.wfcheck(new_lines) @classmethod @ModelView.button @Workflow.transition('done') def wfdone(cls, lines): """ line is done """ Line2 = Pool().get('cashbook.line') to_write_line = [] for line in lines: # add number to line if len(line.number or '') == 0: to_write_line.extend([ [line], { 'number': line.cashbook.number_sequ.get() }]) if len(to_write_line) > 0: Line2.write(*to_write_line) @classmethod def default_state(cls): """ default: edit """ return 'edit' @classmethod def default_date(cls): """ default: today """ IrDate = Pool().get('ir.date') return IrDate.today() @classmethod def default_cashbook(cls): """ get default from context """ context = Transaction().context return context.get('cashbook', None) @classmethod def search_rec_name(cls, name, clause): """ search in description +... """ return cls.search_payee(name, clause) + [ ('description',) + tuple(clause[1:]), ('category.rec_name',) + tuple(clause[1:]), ('splitlines.description',) + tuple(clause[1:]), ('splitlines.category.rec_name',) + tuple(clause[1:]), ] def get_rec_name(self, name): """ short + name """ credit = self.credit if self.credit is not None else Decimal('0.0') debit = self.debit if self.debit is not None else Decimal('0.0') return '%(date)s|%(type)s|%(amount)s %(symbol)s|%(desc)s [%(category)s]' % { 'date': Report.format_date(self.date), 'desc': (self.description or '-')[:40], 'amount': Report.format_number(credit - debit, None), 'symbol': getattr(self.currency, 'symbol', '-'), 'category': self.category_view \ if self.bookingtype in ['in', 'out'] \ else getattr(self.booktransf, 'rec_name', '-'), 'type': gettext('cashbook.msg_line_bookingtype_%s' % self.bookingtype), } def get_amount_by_second_currency(self, to_currency, amount=None): """ get amount, calculate credit/debit from currency of current cashbook to 'to_currency' """ Currency = Pool().get('currency.currency') values = { 'amount': amount if amount is not None else self.amount, } if to_currency.id != self.cashbook.currency.id: with Transaction().set_context({ 'date': self.date, }): values['amount'] = Currency.compute( self.cashbook.currency, values['amount'], to_currency) return values @staticmethod def order_state(tables): """ edit = 0, check/done = 1 """ Line = Pool().get('cashbook.line') tab_line = Line.__table__() table, _ = tables[None] query = tab_line.select( Case( (tab_line.state == 'edit', 1), (tab_line.state.in_(['check', 'done']), 0), else_ = 2), where=tab_line.id==table.id ) return [query] @staticmethod def order_category_view(tables): """ order: name """ table, _ = tables[None] Category = Pool().get('cashbook.category') tab_cat = Category.__table__() tab2 = tab_cat.select(tab_cat.name, where=tab_cat.id==table.category ) return [tab2] @staticmethod def order_descr_short(tables): """ order by 'description' """ table, _ = tables[None] return [table.description] @classmethod def search_payee(cls, names, clause): """ search in payee for party or cashbook """ return ['OR', ('party.rec_name',) + tuple(clause[1:]), ('booktransf.rec_name',) + tuple(clause[1:]), ] @classmethod def search_category_view(cls, name, clause): """ search in category """ return [('category.rec_name',) + tuple(clause[1:])] @classmethod def search_month(cls, names, clause): """ search in month """ pool = Pool() Line = pool.get('cashbook.line') IrDate = pool.get('ir.date') tab_line = Line.__table__() Operator = fields.SQL_OPERATORS[clause[1]] dt1 = IrDate.today() query = tab_line.select(tab_line.id, where=Operator( Literal(12 * dt1.year + dt1.month) - \ (Literal(12) * DatePart('year', tab_line.date) + DatePart('month', tab_line.date)), clause[2]), ) return [('id', 'in', query)] @classmethod def search_state_cashbook(cls, names, clause): """ search in state of cashbook """ return [('cashbook.state',) + tuple(clause[1:])] @classmethod def search_descr_short(cls, names, clause): """ search in description """ return [('description',) + tuple(clause[1:])] @fields.depends('amount', 'splitlines') def on_change_splitlines(self): """ update amount if splitlines change """ self.amount = sum([x.amount for x in self.splitlines if x.amount is not None]) @fields.depends('bookingtype', 'category', 'splitlines') def on_change_bookingtype(self): """ clear category if not valid type """ types = { 'in': ['in', 'mvin', 'spin'], 'out': ['out', 'mvout', 'spout'], } if self.bookingtype: if self.category: if not self.bookingtype in types.get(self.category.cattype, ''): self.category = None if self.bookingtype.startswith('sp'): # split booking self.category = None self.booktransf = None for spline in self.splitlines: if not self.bookingtype in types.get(getattr(spline.category, 'cattype', '-'), ''): spline.category = None elif self.bookingtype.startswith('mv'): # transfer self.splitlines = [] self.category = None else : # category self.splitlines = [] self.booktransf = None @fields.depends('currency', 'booktransf', '_parent_booktransf.currency', \ 'amount', 'amount_2nd_currency', 'rate_2nd_currency') def on_change_amount(self): """ update amount_2nd_currency """ self.on_change_rate_2nd_currency() @fields.depends('booktransf', '_parent_booktransf.currency', \ 'currency', 'amount', 'amount_2nd_currency', 'rate_2nd_currency') def on_change_booktransf(self): """ update amount_2nd_currency """ Currency = Pool().get('currency.currency') if self.booktransf: if self.currency: if self.amount is not None: if self.booktransf.currency.id != self.currency.id: self.amount_2nd_currency = Currency.compute( self.currency, self.amount, self.booktransf.currency ) self.rate_2nd_currency = self.on_change_with_rate_2nd_currency() return self.amount_2nd_currency = None @fields.depends('currency', 'booktransf', '_parent_booktransf.currency', \ 'amount', 'amount_2nd_currency', 'rate_2nd_currency') def on_change_rate_2nd_currency(self): """ update amount_2nd_currency by rate """ if (self.amount is None) or (self.rate_2nd_currency is None): return if self.currency: if self.booktransf: if self.currency.id != self.booktransf.currency.id: self.amount_2nd_currency = self.booktransf.currency.round( self.amount * self.rate_2nd_currency ) @classmethod def set_rate_2nd_currency(cls, lines, name, value): """ compute amount_2nd_currency, write to db """ Line2 = Pool().get('cashbook.line') to_write = [] if not name == 'rate_2nd_currency': return for line in lines: if line.booktransf is None: continue if line.cashbook.currency.id == line.booktransf.currency.id: continue to_write.extend([ [line], { 'amount_2nd_currency': line.booktransf.currency.round( line.amount * value), }]) if len(to_write) > 0: Line2.write(*to_write) @fields.depends('amount', 'amount_2nd_currency', 'currency2nd') def on_change_with_rate_2nd_currency(self, name=None): """ get current rate from amount """ Rate = Pool().get('currency.currency.rate') if (self.amount is not None) and \ (self.amount_2nd_currency is not None) and \ (self.currency2nd is not None): if self.amount != Decimal('0.0'): exp = Decimal(Decimal(1) / 10 ** Rate.rate.digits[1]) return (self.amount_2nd_currency / self.amount).quantize(exp) @fields.depends('description') def on_change_with_descr_short(self, name=None): """ to speed up list-view """ if self.description: return self.description[:50].replace('\n', '; ') @fields.depends('party', 'booktransf', 'bookingtype') def on_change_with_payee(self, name=None): """ get party or cashbook """ if self.bookingtype: if self.bookingtype in ['in', 'out', 'spin', 'spout']: if self.party: return 'party.party,%d' % self.party.id elif self.bookingtype in ['mvin', 'mvout']: if self.booktransf: return 'cashbook.book,%d' % self.booktransf.id @fields.depends('category') def on_change_with_category_view(self, name=None): """ show optimizef form of category for list-view """ Configuration = Pool().get('cashbook.configuration') if self.category: cfg1 = Configuration.get_singleton() if getattr(cfg1, 'catnamelong', True) == True: return self.category.rec_name else : return self.category.name @fields.depends('date') def on_change_with_month(self, name=None): """ get difference of month to current date """ IrDate = Pool().get('ir.date') if self.date is not None: dt1 = IrDate.today() return (12 * dt1.year + dt1.month) - \ (12 * self.date.year + self.date.month) @fields.depends('cashbook', '_parent_cashbook.owner') def on_change_with_owner_cashbook(self, name=None): """ get current owner """ if self.cashbook: return self.cashbook.owner.id @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 @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 of cashbook """ if self.cashbook: return self.cashbook.currency.digits else: return 2 @fields.depends('currency', 'booktransf', '_parent_booktransf.currency') def on_change_with_currency2nd(self, name=None): """ currency of transfer-target """ if self.booktransf: if self.currency: if self.currency.id != self.booktransf.currency.id: return self.booktransf.currency.id @fields.depends('currency', 'booktransf', '_parent_booktransf.currency') def on_change_with_currency2nd_digits(self, name=None): """ currency of transfer-target """ if self.booktransf: return self.booktransf.currency.digits else: return 2 @fields.depends('id', 'date', 'cashbook', \ '_parent_cashbook.id', 'reconciliation', \ '_parent_reconciliation.start_amount',\ '_parent_reconciliation.state') def on_change_with_balance(self, name=None): """ compute balance until current line, with current sort order, try to use a reconciliation as start to speed up calculation """ pool = Pool() Reconciliation = pool.get('cashbook.recon') Line = pool.get('cashbook.line') def get_from_last_recon(line2): """ search last reconciliation in state 'done', generate query """ query2 = [] end_amount = None recons = Reconciliation.search([ ('cashbook.id', '=', self.cashbook.id), ('date_to', '<=', line2.date), ('state', '=', 'done'), ], order=[('date_from', 'DESC')], limit=1) if len(recons) > 0: query2.append([ ('date', '>=', recons[0].date_to), ('date', '<=', line2.date), ['OR', ('reconciliation', '=', None), ('reconciliation.id', '!=', recons[0]), ], ]) end_amount = recons[0].end_amount return (query2, end_amount) if self.cashbook: query = [ ('cashbook.id', '=', self.cashbook.id), ] balance = Decimal('0.0') # get existing reconciliation, starting before current line # this will speed up calculation of by-line-balance if self.date is not None: if self.reconciliation: if self.reconciliation.state == 'done': query.append( ('reconciliation.id', '=', self.reconciliation.id), ) balance = self.reconciliation.start_amount else : (query2, balance2) = get_from_last_recon(self) query.extend(query2) if balance2 is not None: balance = balance2 else : (query2, balance2) = get_from_last_recon(self) query.extend(query2) if balance2 is not None: balance = balance2 lines = Line.search(query) for line in lines: balance += line.credit - line.debit if line.id == self.id: break return balance @classmethod def clear_by_bookingtype(cls, values, line=None): """ clear some fields by value of bookingtype """ values2 = {} values2.update(values) bookingtype = values2.get('bookingtype', getattr(line, 'bookingtype', None)) if (bookingtype in ['in', 'out', 'mvin', 'mvout']) and \ ('splitlines' not in values2.keys()): if line: if len(line.splitlines) > 0: values2['splitlines'] = [('delete', [x.id for x in line.splitlines])] if bookingtype in ['in', 'out']: values2['booktransf'] = None if bookingtype in ['spin', 'spout']: values2['category'] = None values2['booktransf'] = None if bookingtype in ['mvin', 'mvout']: values2['category'] = None return values2 @classmethod def get_debit_credit(cls, values): """ compute debit/credit from amount """ if isinstance(values, dict): type_ = values.get('bookingtype', None) amount = values.get('amount', None) else : type_ = getattr(values, 'bookingtype', None) amount = getattr(values, 'amount', None) if type_: if amount is not None: if type_ in ['in', 'mvin', 'spin']: return { 'debit': Decimal('0.0'), 'credit': amount, } elif type_ in ['out', 'mvout', 'spout']: return { 'debit': amount, 'credit': Decimal('0.0'), } else : raise ValueError('invalid "bookingtype"') return {} @classmethod def add_2nd_currency(cls, values): """ add second currency amount if missing """ pool = Pool() Currency = pool.get('currency.currency') Cashbook = pool.get('cashbook.book') cashbook = values.get('cashbook', None) booktransf = values.get('booktransf', None) amount = values.get('amount', None) amount_2nd_currency = values.get('amount_2nd_currency', None) if cashbook: if booktransf: if amount is not None: if amount_2nd_currency is None: cashbook = Cashbook(cashbook) booktransf = Cashbook(booktransf) if cashbook.currency.id != booktransf.currency.id: values['amount_2nd_currency'] = Currency.compute( cashbook.currency, amount, booktransf.currency, ) return values @classmethod def update_amount_by_splitlines(cls, lines): """ update amounts from split-lines """ Line2 = Pool().get('cashbook.line') to_write = [] for line in lines: to_write.extend([ [line], { 'amount': sum([x.amount for x in line.splitlines]), }]) if len(to_write) > 0: Line2.write(*to_write) @classmethod def validate(cls, lines): """ deny date before 'start_date' of cashbook """ super(Line, cls).validate(lines) types = { 'in': ['in', 'mvin', 'spin'], 'out': ['out', 'mvout', 'spout'], } for line in lines: if line.date < line.cashbook.start_date: raise UserError(gettext( 'cashbook.msg_line_date_before_book', datebook = Report.format_date(line.cashbook.start_date), recname = line.rec_name, )) # line: category <--> bookingtype? if line.category: if not line.bookingtype in types[line.category.cattype]: raise UserError(gettext( 'cashbook.msg_line_invalid_category', recname = line.rec_name, booktype = line.bookingtype_string, )) # splitline: category <--> bookingtype? for spline in line.splitlines: if spline.splittype != 'cat': continue if not line.bookingtype in types[spline.category.cattype]: raise UserError(gettext( 'cashbook.msg_line_split_invalid_category', recname = line.rec_name, splitrecname = spline.rec_name, booktype = line.bookingtype_string, )) @classmethod def check_permission_write(cls, lines, values={}): """ deny update if cashbook.line!='open', """ for line in lines: # deny write if cashbook is not open if line.cashbook.state != 'open': raise UserError(gettext( 'cashbook.msg_book_deny_write', bookname = line.cashbook.rec_name, state_txt = line.cashbook.state_string, )) # deny write if reconciliation is 'check' or 'done' if line.reconciliation: if line.reconciliation.state == 'done': raise UserError(gettext( 'cashbook.msg_line_deny_write_by_reconciliation', recname = line.rec_name, reconame = line.reconciliation.rec_name, )) # deny write if line is not 'Edit' if line.state != 'edit': # allow state-update, if its the only action if not ((len(set({'state', 'reconciliation', 'number'}).intersection(values.keys())) > 0) \ and (len(values.keys()) == 1)): raise UserError(gettext( 'cashbook.msg_line_deny_write', recname = line.rec_name, state_txt = line.state_string, )) @classmethod def check_permission_delete(cls, lines): """ deny delete if book is not 'open' or wf is not 'edit' """ for line in lines: if line.cashbook.state == 'closed': raise UserError(gettext( 'cashbook.msg_line_deny_delete1', linetxt = line.rec_name, bookname = line.cashbook.rec_name, bookstate = line.cashbook.state_string, )) if line.state != 'edit': raise UserError(gettext( 'cashbook.msg_line_deny_delete2', linetxt = line.rec_name, linestate = line.state_string, )) @classmethod def copy(cls, lines, default=None): """ reset values """ if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('state', cls.default_state()) return super(Line, cls).copy(moves, default=default) @classmethod def create(cls, vlist): """ add debit/credit """ vlist = [x.copy() for x in vlist] for values in vlist: values.update(cls.get_debit_credit(values)) values.update(cls.clear_by_bookingtype(values)) values.update(cls.add_2nd_currency(values)) # deny add to reconciliation if state is not 'check' or 'done' if values.get('reconciliation', None): if not values.get('state', '-') in ['check', 'done']: date_txt = '-' if values.get('date', None): date_txt = Report.format_date(values.get('date', None)) raise UserError(gettext( 'cashbook.msg_line_deny_recon_by_state', recname = '%(date)s|%(descr)s' % { 'date': date_txt, 'descr': values.get('description', '-'), }, )) return super(Line, cls).create(vlist) @classmethod def write(cls, *args): """ deny update if cashbook.line!='open', add or update debit/credit """ actions = iter(args) to_write = [] for lines, values in zip(actions, actions): cls.check_permission_write(lines, values) for line in lines: if line.reconciliation: # deny state-change to 'edit' if line is linked to reconciliation if values.get('state', '-') == 'edit': raise UserError(gettext( 'cashbook.msg_line_deny_stateedit_with_recon', recname = line.rec_name, )) # deny add to reconciliation if state is not 'check' or 'done' if values.get('reconciliation', None): for line in lines: if not line.state in ['check', 'done']: raise UserError(gettext( 'cashbook.msg_line_deny_recon_by_state', recname = line.rec_name )) # update debit / credit if len(set(values.keys()).intersection(set({'amount', 'bookingtype'}))) > 0: for line in lines: values2 = {} values2.update(values) values2.update(cls.clear_by_bookingtype(values, line)) values2.update(cls.get_debit_credit({ x:values.get(x, getattr(line, x)) for x in ['amount', 'bookingtype'] })) to_write.extend([lines, values2]) else : to_write.extend([lines, values]) super(Line, cls).write(*to_write) @classmethod def delete(cls, lines): """ deny delete if book is not 'open' or wf is not 'edit' """ cls.check_permission_delete(lines) return super(Line, cls).delete(lines) # end Line class LineContext(ModelView): 'Line Context' __name__ = 'cashbook.line.context' cashbook = fields.Many2One(string='Cashbook', required=True, model_name='cashbook.book', domain=[('btype', '!=', None)], states={ 'readonly': Eval('num_cashbook', 0) < 2, }, depends=['num_cashbook']) date_from = fields.Date(string='Start Date', depends=['date_to'], help='Limits the date range for the displayed entries.', domain=[ If(Eval('date_to') & Eval('date_from'), ('date_from', '<=', Eval('date_to')), ()), ]) date_to = fields.Date(string='End Date', depends=['date_from'], help='Limits the date range for the displayed entries.', domain=[ If(Eval('date_to') & Eval('date_from'), ('date_from', '<=', Eval('date_to')), ()), ]) checked = fields.Boolean(string='Checked', help='Show account lines in Checked-state.') done = fields.Boolean(string='Done', help='Show account lines in Done-state.') num_cashbook = fields.Function(fields.Integer(string='Number of Cashbook', readonly=True, states={'invisible': True}), 'on_change_with_num_cashbook') @classmethod def default_cashbook(cls): """ get default from context """ context = Transaction().context return context.get('cashbook', None) @classmethod def default_date_from(cls): """ get default from context """ context = Transaction().context return context.get('date_from', None) @classmethod def default_date_to(cls): """ get default from context """ context = Transaction().context return context.get('date_to', None) @classmethod def default_checked(cls): """ get default from context """ context = Transaction().context return context.get('checked', False) @classmethod def default_num_cashbook(cls): """ get default from context """ CashBook = Pool().get('cashbook.book') with Transaction().set_context({ '_check_access': True, }): return CashBook.search_count([('btype', '!=', None)]) @classmethod def default_done(cls): """ get default from context """ context = Transaction().context return context.get('done', False) def on_change_with_num_cashbook(self, name=None): """ get number of accessible cashbooks, depends on user-permissions """ LineContext = Pool().get('cashbook.line.context') return LineContext.default_num_cashbook() # end LineContext