#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 trytond.model import Workflow, ModelView, ModelSQL, fields from trytond.pyson import Eval, If, Bool from trytond.transaction import Transaction from trytond import backend from trytond.pool import Pool __all__ = ['Statement', 'Line'] _STATES = {'readonly': Eval('state') != 'draft'} _DEPENDS = ['state'] class Statement(Workflow, ModelSQL, ModelView): 'Account Statement' __name__ = 'account.statement' company = fields.Many2One('company.company', 'Company', required=True, select=True, states=_STATES, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=_DEPENDS) journal = fields.Many2One('account.statement.journal', 'Journal', required=True, select=True, domain=[ ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }, depends=['state']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') date = fields.Date('Date', required=True, states=_STATES, depends=_DEPENDS, select=True) start_balance = fields.Numeric('Start Balance', required=True, digits=(16, Eval('currency_digits', 2)), states=_STATES, depends=['state', 'currency_digits']) end_balance = fields.Numeric('End Balance', required=True, digits=(16, Eval('currency_digits', 2)), states=_STATES, depends=['state', 'currency_digits']) balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'on_change_with_balance') lines = fields.One2Many('account.statement.line', 'statement', 'Transactions', states={ 'readonly': (Eval('state') != 'draft') | ~Eval('journal'), }, depends=['state', 'journal']) state = fields.Selection([ ('draft', 'Draft'), ('validated', 'Validated'), ('cancel', 'Canceled'), ('posted', 'Posted'), ], 'State', readonly=True, select=True) @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._error_messages.update({ 'wrong_end_balance': 'End Balance must be "%s".', 'delete_cancel': ('Statement "%s" must be cancelled before ' 'deletion.'), 'paid_invoice_draft_statement': ('There are paid invoices on ' 'draft statements, do you want to proceed?'), }) cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancel'), ('validated', 'posted'), ('validated', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancel', }, 'validate_statement': { 'invisible': Eval('state') != 'draft', }, 'post': { 'invisible': Eval('state') != 'validated', }, 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'validated']), }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().cursor # Migration from 1.8: new field company table = TableHandler(cursor, cls, module_name) company_exist = table.column_exist('company') super(Statement, cls).__register__(module_name) # Migration from 1.8: fill new field company if not company_exist: offset = 0 limit = cursor.IN_MAX statements = True while statements: statements = cls.search([], offset=offset, limit=limit) offset += limit for statement in statements: cls.write([statement], { 'company': statement.journal.company.id, }) table = TableHandler(cursor, cls, module_name) table.not_null_action('company', action='add') @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @fields.depends('journal', 'state', 'lines') def on_change_journal(self): res = {} if not self.journal: return res statements = self.search([ ('journal', '=', self.journal.id), ], order=[ ('date', 'DESC'), ], limit=1) if not statements: return res statement, = statements res['start_balance'] = statement.end_balance return res @fields.depends('journal') def on_change_with_currency_digits(self, name=None): if self.journal: return self.journal.currency.digits return 2 @classmethod def get_rec_name(cls, statements, name): Lang = Pool().get('ir.lang') lang, = Lang.search([ ('code', '=', Transaction().language), ]) res = {} for statement in statements: res[statement.id] = (statement.journal.name + ' ' + Lang.currency(lang, statement.start_balance, statement.journal.currency, symbol=False, grouping=True) + ' - ' + Lang.currency(lang, statement.end_balance, statement.journal.currency, symbol=False, grouping=True)) return res @classmethod def search_rec_name(cls, name, clause): return [('journal',) + tuple(clause[1:])] def get_end_balance(self, name): end_balance = self.start_balance for line in self.lines: end_balance += line.amount return end_balance @fields.depends('start_balance', 'end_balance') def on_change_with_balance(self, name=None): return ((getattr(self, 'end_balance', 0) or 0) - (getattr(self, 'start_balance', 0) or 0)) @fields.depends('lines', 'journal') def on_change_lines(self): pool = Pool() Currency = pool.get('currency.currency') Line = pool.get('account.statement.line') res = { 'lines': {}, } if self.journal and self.lines: invoices = set() for line in self.lines: if getattr(line, 'invoice', None): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: with Transaction().set_context(date=invoice.currency_date): if invoice.type in ('out_invoice', 'in_credit_note'): sign = -1 else: sign = 1 invoice_id2amount_to_pay[invoice.id] = sign * ( Currency.compute(invoice.currency, invoice.amount_to_pay, self.journal.currency)) line_offset = 0 for index, line in enumerate(self.lines or []): if getattr(line, 'invoice', None) and line.id: amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] res['lines'].setdefault('update', []) if (not self.journal.currency.is_zero(amount_to_pay) and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): res['lines']['update'].append({ 'id': line.id, 'amount': (amount_to_pay.copy_sign(line.amount) if abs(line.amount) > abs(amount_to_pay) else line.amount) }) if abs(line.amount) > abs(amount_to_pay): res['lines'].setdefault('add', []) vals = {} for field_name, field in Line._fields.iteritems(): try: value = getattr(line, field_name) except AttributeError: continue if (value and field._type in ('many2one', 'one2one')): vals[field_name] = value.id if value.id >= 0: vals[field_name + '.rec_name'] = \ value.rec_name else: vals[field_name] = value del vals['id'] vals['amount'] = line.amount + amount_to_pay vals['invoice'] = None del vals['invoice.rec_name'] line_offset += 1 res['lines']['add'].append((index + line_offset, vals)) invoice_id2amount_to_pay[line.invoice.id] = 0 else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: res['lines']['update'].append({ 'id': line.id, 'invoice': None, }) return res @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancel': cls.raise_user_error('delete_cancel', (statement.rec_name,)) super(Statement, cls).delete(statements) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, statements): pass @classmethod @ModelView.button @Workflow.transition('validated') def validate_statement(cls, statements): pool = Pool() Lang = pool.get('ir.lang') Line = pool.get('account.statement.line') statement_lines = [] for statement in statements: computed_end_balance = statement.start_balance for line in statement.lines: computed_end_balance += line.amount if computed_end_balance != statement.end_balance: lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format(lang, '%.' + str(statement.journal.currency.digits) + 'f', computed_end_balance, True) cls.raise_user_error('wrong_end_balance', error_args=(amount,)) for line in statement.lines: line.create_move() statement_lines.append(line.id) cls.write(statements, { 'state': 'validated', }) common_lines = Line.search([ ('statement.state', '=', 'draft'), ('invoice.state', '=', 'paid'), ]) if common_lines: warning_key = '_'.join(str(l.id) for l in common_lines) cls.raise_user_warning(warning_key, 'paid_invoice_draft_statement') Line.write(common_lines, { 'invoice': None, }) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.post_move(lines) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.delete_move(lines) class Line(ModelSQL, ModelView): 'Account Statement Line' __name__ = 'account.statement.line' statement = fields.Many2One('account.statement', 'Statement', required=True, ondelete='CASCADE') date = fields.Date('Date', required=True) amount = fields.Numeric('Amount', required=True, digits=(16, Eval('_parent_statement', {}).get('currency_digits', 2))) party = fields.Many2One('party.party', 'Party') account = fields.Many2One('account.account', 'Account', required=True, domain=[ ('company', '=', Eval('_parent_statement', {}).get('company', 0)), ('kind', '!=', 'view'), ]) description = fields.Char('Description') move = fields.Many2One('account.move', 'Account Move', readonly=True) invoice = fields.Many2One('account.invoice', 'Invoice', domain=[ If(Bool(Eval('party')), [('party', '=', Eval('party'))], []), If(Bool(Eval('party')), [('account', '=', Eval('account'))], []), If(Eval('_parent_statement', {}).get('state') == 'draft', ('state', '=', 'posted'), ('state', '!=', '')), ], depends=['party', 'account']) @classmethod def __setup__(cls): super(Line, cls).__setup__() cls._error_messages.update({ 'debit_credit_account_statement_journal': ('Please provide ' 'debit and credit account on statement journal "%s".'), 'same_debit_credit_account': ('Account "%(account)s" in ' 'statement line "%(line)s" is the same as the one ' 'configured as credit or debit on journal "%(journal)s".'), 'amount_greater_invoice_amount_to_pay': ('Amount "%s" is ' 'greater than the amount to pay of invoice.'), }) cls._sql_constraints += [ ('check_statement_line_amount', 'CHECK(amount != 0)', 'Amount should be a positive or negative value.'), ] @staticmethod def default_amount(): return Decimal(0) @fields.depends('amount', 'party', 'invoice') def on_change_party(self): res = {} if self.party: if self.amount: if self.amount > Decimal("0.0"): account = self.party.account_receivable else: account = self.party.account_payable res['account'] = account.id res['account.rec_name'] = account.rec_name if self.invoice: if self.party: if self.invoice.party != self.party: res['invoice'] = None else: res['invoice'] = None return res @fields.depends('amount', 'party', 'account', 'invoice', '_parent_statement.journal') def on_change_amount(self): Currency = Pool().get('currency.currency') res = {} if self.party: if self.account and self.account not in ( self.party.account_receivable, self.party.account_payable): # The user has entered a non-default value, we keep it. pass elif self.amount: if self.amount > Decimal("0.0"): account = self.party.account_receivable else: account = self.party.account_payable res['account'] = account.id res['account.rec_name'] = account.rec_name if self.invoice: if self.amount and self.statement and self.statement.journal: invoice = self.invoice journal = self.statement.journal with Transaction().set_context(date=invoice.currency_date): amount_to_pay = Currency.compute(invoice.currency, invoice.amount_to_pay, journal.currency) if abs(self.amount) > amount_to_pay: res['invoice'] = None else: res['invoice'] = None return res @fields.depends('account', 'invoice') def on_change_account(self): res = {} if self.invoice: if self.account: if self.invoice.account != self.account: res['invoice'] = None else: res['invoice'] = None return res @fields.depends('party', 'account', 'invoice') def on_change_invoice(self): changes = {} if self.invoice: if not self.party: changes['party'] = self.invoice.party.id changes['party.rec_name'] = self.invoice.party.rec_name if not self.account: changes['account'] = self.invoice.account.id changes['account.rec_name'] = self.invoice.account.rec_name return changes def get_rec_name(self, name): return self.statement.rec_name @classmethod def search_rec_name(cls, name, clause): return [('statement.rec_name',) + tuple(clause[1:])] @classmethod def copy(cls, lines, default=None): if default is None: default = {} default = default.copy() default.setdefault('move', None) default.setdefault('invoice', None) return super(Line, cls).copy(lines, default=default) def create_move(self): ''' Create move for the statement line and return move if created. ''' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') Invoice = pool.get('account.invoice') Currency = pool.get('currency.currency') MoveLine = pool.get('account.move.line') Lang = pool.get('ir.lang') if self.move: return period_id = Period.find(self.statement.company.id, date=self.date) move_lines = self._get_move_lines() move = Move( period=period_id, journal=self.statement.journal.journal, date=self.date, origin=self, lines=move_lines, ) move.save() self.write([self], { 'move': move.id, }) if self.invoice: with Transaction().set_context(date=self.invoice.currency_date): amount_to_pay = Currency.compute(self.invoice.currency, self.invoice.amount_to_pay, self.statement.journal.currency) if abs(amount_to_pay) < abs(self.amount): lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format(lang, '%.' + str(self.statement.journal.currency.digits) + 'f', self.amount, True) self.raise_user_error('amount_greater_invoice_amount_to_pay', error_args=(amount,)) with Transaction().set_context(date=self.invoice.currency_date): amount = Currency.compute(self.statement.journal.currency, self.amount, self.statement.company.currency) reconcile_lines = self.invoice.get_reconcile_lines_for_amount( amount) for move_line in move.lines: if move_line.account == self.invoice.account: Invoice.write([self.invoice], { 'payment_lines': [('add', [move_line.id])], }) break if reconcile_lines[1] == Decimal('0.0'): lines = reconcile_lines[0] + [move_line] MoveLine.reconcile(lines) return move @classmethod def post_move(cls, lines): Move = Pool().get('account.move') Move.post([l.move for l in lines if l.move]) @classmethod def delete_move(cls, lines): Move = Pool().get('account.move') Move.delete([l.move for l in lines if l.move]) def _get_move_lines(self): ''' Return the move lines for the statement line ''' pool = Pool() MoveLine = pool.get('account.move.line') Currency = Pool().get('currency.currency') zero = Decimal("0.0") with Transaction().set_context(date=self.date): amount = Currency.compute(self.statement.journal.currency, self.amount, self.statement.company.currency) if self.statement.journal.currency != self.statement.company.currency: second_currency = self.statement.journal.currency.id amount_second_currency = abs(self.amount) else: amount_second_currency = None second_currency = None move_lines = [] move_lines.append(MoveLine( description=self.description, debit=amount < zero and -amount or zero, credit=amount >= zero and amount or zero, account=self.account, party=self.party, second_currency=second_currency, amount_second_currency=amount_second_currency, )) journal = self.statement.journal.journal if self.amount >= zero: account = journal.credit_account else: account = journal.debit_account if not account: self.raise_user_error('debit_credit_account_statement_journal', (journal.rec_name,)) if self.account == account: self.raise_user_error('same_debit_credit_account', { 'account': self.account.rec_name, 'line': self.rec_name, 'journal': journal, }) move_lines.append(MoveLine( description=self.description, debit=amount >= zero and amount or zero, credit=amount < zero and -amount or zero, account=account, party=self.party, second_currency=second_currency, amount_second_currency=amount_second_currency, )) return move_lines