# 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 collections import defaultdict from decimal import Decimal from itertools import chain, combinations, groupby, islice from dateutil.relativedelta import relativedelta from sql import Literal, Null, Window from sql.aggregate import Sum from sql.conditionals import Case, Coalesce from sql.functions import Abs, CharLength, Round from sql.operators import Exists from trytond.config import config from trytond.i18n import gettext from trytond.model import ( Check, DeactivableMixin, Index, ModelSQL, ModelView, dualmethod, fields) from trytond.model.exceptions import AccessError from trytond.modules.currency.fields import Monetary from trytond.pool import Pool from trytond.pyson import Bool, Eval, If, PYSONEncoder from trytond.report import Report from trytond.rpc import RPC from trytond.tools import firstline, grouped_slice, reduce_ids from trytond.transaction import Transaction, check_access from trytond.wizard import ( Button, StateAction, StateTransition, StateView, Wizard) from .exceptions import ( AccountMissing, CancelDelegatedWarning, CancelWarning, DelegateLineError, GroupLineError, JournalMissing, PeriodNotFoundError, PostError, ReconciliationDeleteWarning, ReconciliationError, RescheduleLineError) import logging logger = logging.getLogger(__name__) _MOVE_STATES = { 'readonly': Eval('state') == 'posted', } _LINE_STATES = { 'readonly': Eval('state') == 'valid', } class DescriptionOriginMixin: __slots__ = () description_used = fields.Function( fields.Char("Description", states=_MOVE_STATES), 'on_change_with_description_used', setter='set_description_used', searcher='search_description_used') @fields.depends('description', 'origin') def on_change_with_description_used(self, name=None): description = self.description if not description and hasattr(self.origin, 'description'): description = firstline(self.origin.description or '') return description @classmethod def set_description_used(cls, moves, name, value): moves = [m for m in moves if m.description_used != value] if moves: cls.write(moves, {'description': value}) @classmethod def search_description_used(cls, name, clause): pool = Pool() domain = ['OR', [ ('description', '!=', None), ('description', '!=', ''), ('description', *clause[1:]), ], ] for origin, _ in cls.get_origin(): if not origin: continue Model = pool.get(origin) if 'description' in Model._fields: domain.append( ('origin.description', *clause[1:3], origin, *clause[3:])) return domain class Move(DescriptionOriginMixin, ModelSQL, ModelView): 'Account Move' __name__ = 'account.move' _rec_name = 'number' number = fields.Char('Number', required=True, readonly=True) post_number = fields.Char('Post Number', readonly=True, help='Also known as Folio Number.') company = fields.Many2One('company.company', 'Company', required=True, states=_MOVE_STATES) period = fields.Many2One('account.period', 'Period', required=True, domain=[ ('company', '=', Eval('company', -1)), If(Eval('state') == 'draft', ('state', '=', 'open'), ()), If(Eval('date'), [ ('start_date', '<=', Eval('date')), ('end_date', '>=', Eval('date')), ], []), ], states=_MOVE_STATES) journal = fields.Many2One('account.journal', 'Journal', required=True, states={ 'readonly': Eval('number') & Eval('journal'), }, context={ 'company': Eval('company', -1), }, depends={'company'}) date = fields.Date('Effective Date', required=True, states=_MOVE_STATES) post_date = fields.Date('Post Date', readonly=True) description = fields.Char('Description', states=_MOVE_STATES) ext_ref = fields.Char('Ext. Ref') origin = fields.Reference('Origin', selection='get_origin', states=_MOVE_STATES) state = fields.Selection([ ('draft', 'Draft'), ('posted', 'Posted'), ], 'State', required=True, readonly=True, sort=False) lines = fields.One2Many('account.move.line', 'move', 'Lines', states=_MOVE_STATES, depends={'company'}, context={ 'journal': Eval('journal'), 'period': Eval('period'), 'date': Eval('date'), }) @classmethod def __setup__(cls): cls.number.search_unaccented = False cls.post_number.search_unaccented = False super(Move, cls).__setup__() t = cls.__table__() cls.create_date.select = True cls._check_modify_exclude = ['lines'] cls._order.insert(0, ('date', 'DESC')) cls._order.insert(1, ('number', 'DESC')) cls._buttons.update({ 'post': { 'invisible': Eval('state') == 'posted', 'depends': ['state'], }, }) cls.__rpc__.update({ 'post': RPC( readonly=False, instantiate=0, fresh_session=True), }) cls._sql_indexes.update({ Index(t, (t.period, Index.Equality())), Index(t, (t.date, Index.Range()), (t.number, Index.Range())), Index( t, (t.journal, Index.Equality()), (t.period, Index.Equality())), }) @classmethod def order_number(cls, tables): table, _ = tables[None] return [CharLength(table.number), table.number] @classmethod def order_post_number(cls, tables): table, _ = tables[None] return [CharLength(table.post_number), table.post_number] @staticmethod def default_company(): return Transaction().context.get('company') @classmethod def default_period(cls): Period = Pool().get('account.period') company = cls.default_company() try: period = Period.find(company) except PeriodNotFoundError: return None return period.id @staticmethod def default_state(): return 'draft' @classmethod def default_date(cls): pool = Pool() Period = pool.get('account.period') Date = pool.get('ir.date') today = Date.today() period_id = cls.default_period() if period_id: period = Period(period_id) start_date = period.start_date end_date = period.end_date if start_date <= today <= end_date: return today elif start_date >= today: return start_date else: return end_date @fields.depends('date', 'period', 'company', 'lines') def on_change_date(self): pool = Pool() Period = pool.get('account.period') context = Transaction().context if self.date: company = self.company or context.get('company') if (not self.period or not ( self.period.start_date <= self.date <= self.period.end_date)): try: self.period = Period.find(company, date=self.date) except PeriodNotFoundError: self.period = None for line in (self.lines or []): line.date = self.date @fields.depends( 'period', 'date', 'company', 'journal', methods=['on_change_date']) def on_change_period(self): pool = Pool() Date = pool.get('ir.date') Line = pool.get('account.move.line') context = Transaction().context company_id = ( self.company.id if self.company else context.get('company')) with Transaction().set_context(company=company_id): today = Date.today() if self.period: start_date = self.period.start_date end_date = self.period.end_date if (not self.date or not (start_date <= self.date <= end_date)): if start_date <= today <= end_date: self.date = today else: lines = Line.search([ ('company', '=', company_id), ('move.journal', '=', self.journal), ('period', '=', self.period), ], order=[('date', 'DESC')], limit=1) if lines: line, = lines self.date = line.date elif start_date >= today: self.date = start_date else: self.date = end_date self.on_change_date() @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' return ['account.fiscalyear', 'account.move', 'stock.move'] @classmethod def get_origin(cls): Model = Pool().get('ir.model') get_name = Model.get_name models = cls._get_origin() return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod def check_modify(cls, moves): 'Check posted moves for modifications.' for move in moves: if move.state == 'posted': raise AccessError( gettext('account.msg_modify_posted_moved', move=move.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, ('post_number',) + tuple(clause[1:]), (cls._rec_name,) + tuple(clause[1:]), ] @classmethod def write(cls, *args): actions = iter(args) args = [] for moves, values in zip(actions, actions): keys = list(values.keys()) for key in cls._check_modify_exclude: if key in keys: keys.remove(key) if len(keys): cls.check_modify(moves) args.extend((moves, values)) super(Move, cls).write(*args) @classmethod def create(cls, vlist): pool = Pool() Journal = pool.get('account.journal') context = Transaction().context journals = {} default_company = cls.default_company() vlist = [x.copy() for x in vlist] for vals in vlist: if not vals.get('number'): journal_id = vals.get('journal', context.get('journal')) company_id = vals.get('company', default_company) if journal_id: if journal_id not in journals: journal = journals[journal_id] = Journal(journal_id) else: journal = journals[journal_id] sequence = journal.get_multivalue( 'sequence', company=company_id) if sequence: with Transaction().set_context(company=company_id): vals['number'] = sequence.get() return super().create(vlist) @classmethod def delete(cls, moves): MoveLine = Pool().get('account.move.line') cls.check_modify(moves) MoveLine.delete([l for m in moves for l in m.lines]) super(Move, cls).delete(moves) @classmethod def copy(cls, moves, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('post_number', None) default.setdefault('state', cls.default_state()) default.setdefault('post_date', None) return super().copy(moves, default=default) @classmethod def validate_move(cls, moves): ''' Validate balanced move ''' pool = Pool() MoveLine = pool.get('account.move.line') line = MoveLine.__table__() transaction = Transaction() cursor = transaction.connection.cursor() for company, moves in groupby(moves, key=lambda m: m.company): currency = company.currency for sub_moves in grouped_slice(list(moves)): red_sql = reduce_ids(line.move, [m.id for m in sub_moves]) valid_move_query = line.select( line.move, where=red_sql, group_by=line.move, having=Abs(Round( Sum(line.debit - line.credit), currency.digits)) < abs(currency.rounding)) cursor.execute(*line.update( [line.state], ['valid'], where=line.move.in_(valid_move_query))) draft_move_query = line.select( line.move, where=red_sql, group_by=line.move, having=Abs(Round( Sum(line.debit - line.credit), currency.digits)) >= abs(currency.rounding)) cursor.execute(*line.update( [line.state], ['draft'], where=line.move.in_(draft_move_query))) Transaction().counter += 1 for cache in Transaction().cache.values(): if MoveLine.__name__ in cache: cache_cls = cache[MoveLine.__name__] cache_cls.clear() def _cancel_default(self, reversal=False): 'Return default dictionary to cancel move' pool = Pool() Date = pool.get('ir.date') Period = pool.get('account.period') Warning = pool.get('res.user.warning') default = { 'origin': str(self), } if self.period.state == 'closed': key = '%s.cancel' % self if Warning.check(key): raise CancelWarning(key, gettext('account.msg_move_cancel_closed_period', move=self.rec_name)) with Transaction().set_context(company=self.company.id): date = Date.today() period = Period.find(self.company, date=date) default.update({ 'date': date, 'period': period.id, }) if reversal: default['lines.debit'] = lambda data: data['credit'] default['lines.credit'] = lambda data: data['debit'] else: default['lines.debit'] = lambda data: data['debit'] * -1 default['lines.credit'] = lambda data: data['credit'] * -1 default['lines.amount_second_currency'] = ( lambda data: data['amount_second_currency'] * -1 if data['amount_second_currency'] else data['amount_second_currency']) default['lines.tax_lines.amount'] = lambda data: data['amount'] * -1 default['lines.origin'] = ( lambda data: 'account.move.line,%s' % data['id']) return default def cancel(self, default=None, reversal=False): 'Return a cancel move' if default is None: default = {} else: default = default.copy() default.update(self._cancel_default(reversal=reversal)) cancel_move, = self.copy([self], default=default) return cancel_move @dualmethod @ModelView.button def post(cls, moves): pool = Pool() Date = pool.get('ir.date') Line = pool.get('account.move.line') move = cls.__table__() line = Line.__table__() cursor = Transaction().connection.cursor() to_reconcile = [] for company, c_moves in groupby(moves, lambda m: m.company): currency = company.currency for sub_moves in grouped_slice(list(c_moves)): sub_moves_ids = [m.id for m in sub_moves] cursor.execute(*move.select( move.id, where=reduce_ids(move.id, sub_moves_ids) & ~Exists(line.select( line.move, where=line.move == move.id)))) try: move_id, = cursor.fetchone() except TypeError: pass else: raise PostError( gettext('account.msg_post_empty_move', move=cls(move_id).rec_name)) cursor.execute(*line.select( line.move, where=reduce_ids(line.move, sub_moves_ids), group_by=line.move, having=Abs(Round( Sum(line.debit - line.credit), currency.digits)) >= abs(currency.rounding))) try: move_id, = cursor.fetchone() except TypeError: pass else: raise PostError( gettext('account.msg_post_unbalanced_move', move=cls(move_id).rec_name)) cursor.execute(*line.select( line.id, where=reduce_ids(line.move, sub_moves_ids) & (line.debit == Decimal(0)) & (line.credit == Decimal(0)) & ((line.amount_second_currency == Null) | (line.amount_second_currency == Decimal(0))) )) to_reconcile.extend(l for l, in cursor) for move in moves: move.state = 'posted' if not move.post_number: with Transaction().set_context(company=move.company.id): move.post_date = Date.today() move.post_number = move.period.post_move_sequence_used.get() cls.save(moves) def keyfunc(line): # Set party last to avoid compare party instance and None # party will always be None for the same account return line.account, line.party to_reconcile = Line.browse(sorted( [l for l in Line.browse(to_reconcile) if l.account.reconcile], key=keyfunc)) to_reconcile = [list(l) for _, l in groupby(to_reconcile, keyfunc)] if to_reconcile: Line.reconcile(*to_reconcile) class Reconciliation(ModelSQL, ModelView): 'Account Move Reconciliation Lines' __name__ = 'account.move.reconciliation' _rec_name = 'number' number = fields.Char("Number", required=True) company = fields.Many2One('company.company', "Company", required=True) lines = fields.One2Many( 'account.move.line', 'reconciliation', 'Lines', domain=[ ('move.company', '=', Eval('company', -1)), ]) date = fields.Date( "Date", required=True, help='Highest date of the reconciled lines.') delegate_to = fields.Many2One( 'account.move.line', "Delegate To", ondelete="RESTRICT", domain=[ ('move.company', '=', Eval('company', -1)), ], help="The line to which the reconciliation status is delegated.") @classmethod def __setup__(cls): cls.number.search_unaccented = False super().__setup__() t = cls.__table__() cls._sql_indexes.add(Index(t, (t.date, Index.Range()))) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table = cls.__table_handler__(module_name) sql_table = cls.__table__() pool = Pool() Move = pool.get('account.move') Line = pool.get('account.move.line') move = Move.__table__() line = Line.__table__() line_h = Line.__table_handler__(module_name) company_exist = table.column_exist('company') # Migration from 6.6: rename name to number if table.column_exist('name') and not table.column_exist('number'): table.column_rename('name', 'number') super(Reconciliation, cls).__register__(module_name) # Migration from 5.8: add company field if not company_exist and line_h.column_exist('reconciliation'): value = (line .join(move, condition=line.move == move.id) .select( move.company, where=line.reconciliation == sql_table.id, group_by=move.company)) cursor.execute(*sql_table.update([sql_table.company], [value])) @classmethod def order_number(cls, tables): table, _ = tables[None] return [CharLength(table.number), table.number] @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def create(cls, vlist): pool = Pool() Configuration = pool.get('account.configuration') configuration = Configuration(1) vlist = [x.copy() for x in vlist] default_company = cls.default_company() for vals in vlist: if 'number' not in vals: vals['number'] = configuration.get_multivalue( 'reconciliation_sequence', company=vals.get('company', default_company)).get() return super(Reconciliation, cls).create(vlist) @classmethod def write(cls, moves, values, *args): raise AccessError(gettext('account.msg_write_reconciliation')) @classmethod def validate(cls, reconciliations): super(Reconciliation, cls).validate(reconciliations) cls.check_lines(reconciliations) @classmethod def delete(cls, reconciliations): pool = Pool() Warning = pool.get('res.user.warning') for reconciliation in reconciliations: if reconciliation.delegate_to: key = Warning.format('delete.delegated', [reconciliation]) if Warning.check(key): raise ReconciliationDeleteWarning(key, gettext('account.msg_reconciliation_delete_delegated', reconciliation=reconciliation.rec_name, line=reconciliation.delegate_to.rec_name, move=reconciliation.delegate_to.move.rec_name)) for line in reconciliation.lines: if line.move.journal.type == 'write-off': key = Warning.format('delete.write-off', [reconciliation]) if Warning.check(key): raise ReconciliationDeleteWarning(key, gettext( 'account.msg_reconciliation_delete_write_off', reconciliation=reconciliation.rec_name, line=line.rec_name, move=line.move.rec_name)) super().delete(reconciliations) @classmethod def check_lines(cls, reconciliations): Lang = Pool().get('ir.lang') for reconciliation in reconciliations: debit = Decimal(0) credit = Decimal(0) account = None if reconciliation.lines: party = reconciliation.lines[0].party for line in reconciliation.lines: if line.state != 'valid': raise ReconciliationError( gettext('account.msg_reconciliation_line_not_valid', line=line.rec_name)) debit += line.debit credit += line.credit if not account: account = line.account elif account.id != line.account.id: raise ReconciliationError( gettext('account' '.msg_reconciliation_different_accounts', line=line.rec_name, account1=line.account.rec_name, account2=account.rec_name)) if not account.reconcile: raise ReconciliationError( gettext('account' '.msg_reconciliation_account_not_reconcile', line=line.rec_name, account=line.account.rec_name)) if line.party != party: raise ReconciliationError( gettext('account' '.msg_reconciliation_different_parties', line=line.rec_name, party1=line.party.rec_name, party2=party.rec_name)) # if not reconciliation.company.currency.is_zero(debit - credit): # lang = Lang.get() # debit = lang.currency(debit, reconciliation.company.currency) # credit = lang.currency(credit, reconciliation.company.currency) # raise ReconciliationError( # gettext('account.msg_reconciliation_unbalanced', # debit=debit, # credit=credit)) class MoveLineMixin: __slots__ = () @classmethod def get_move_origin(cls): Move = Pool().get('account.move') return Move.get_origin() @classmethod def get_move_states(cls): pool = Pool() Move = pool.get('account.move') return Move.fields_get(['state'])['state']['selection'] def get_move_field(self, name): field = getattr(self.__class__, name) if name.startswith('move_'): name = name[5:] value = getattr(self.move, name) if isinstance(value, ModelSQL): if field._type == 'reference': return str(value) return value.id return value @classmethod def set_move_field(cls, lines, name, value): pool = Pool() Move = pool.get('account.move') if name.startswith('move_'): name = name[5:] if not value: return moves = {line.move for line in lines} moves = Move.browse(moves) Move.write(moves, { name: value, }) @classmethod def search_move_field(cls, name, clause): nested = clause[0][len(name):] if name.startswith('move_'): name = name[5:] return [('move.' + name + nested, *clause[1:])] @staticmethod def _order_move_field(name): def order_field(cls, tables): pool = Pool() Move = pool.get('account.move') field = Move._fields[name] table, _ = tables[None] move_tables = tables.get('move') if move_tables is None: move = Move.__table__() move_tables = { None: (move, move.id == table.move), } tables['move'] = move_tables return field.convert_order(name, move_tables, Move) return classmethod(order_field) def get_amount(self, name): sign = -1 if self.account.type.statement == 'income' else 1 logger.info("GET_AMOUNT:%s",self.amount_second_currency) if self.amount_second_currency is not None: return self.amount_second_currency * sign else: return (self.debit - self.credit) * sign def get_amount_currency(self, name): if self.second_currency: currency = self.second_currency else: currency = self.account.currency return currency.id @classmethod def get_payable_receivable_balance(cls, lines, name): pool = Pool() Account = pool.get('account.account') AccountType = pool.get('account.account.type') Move = pool.get('account.move') transaction = Transaction() database = transaction.database context = transaction.context cursor = transaction.connection.cursor() line = cls.__table__() account = Account.__table__() account_type = AccountType.__table__() move = Move.__table__() balances = dict.fromkeys(map(int, lines)) for company, lines in groupby(lines, lambda l: l.company): for sub_lines in grouped_slice(lines): sub_lines = list(sub_lines) where = account.company == company.id where_type = Literal(False) if context.get('receivable', True): where_type |= account_type.receivable if context.get('payable', True): where_type |= account_type.payable where &= where_type if not context.get('reconciled'): if hasattr(cls, 'reconciliation'): where &= line.reconciliation == Null elif hasattr(cls, 'reconciled'): where &= ~line.reconciled currency = Coalesce(line.second_currency, company.currency.id) sign = Case( (account_type.statement == 'income', -1), else_=1) balance = sign * Case( (line.amount_second_currency != Null, line.amount_second_currency), else_=line.debit - line.credit) if database.has_window_functions(): balance = Sum(balance, window=Window( [line.party, currency], order_by=[ Coalesce(line.maturity_date, move.date).asc, move.date.desc, line.id.desc, ])) party_where = Literal(False) parties = {l.party for l in sub_lines} if None in parties: party_where |= line.party == parties.discard(None) party_where |= reduce_ids(line.party, map(int, parties)) where &= party_where query = (line .join(move, condition=line.move == move.id) .join(account, condition=line.account == account.id) .join( account_type, condition=account.type == account_type.id) .select( line.id.as_('id'), balance.as_('balance'), where=where)) cursor.execute(*query.select( query.id, query.balance, where=reduce_ids(query.id, [l.id for l in sub_lines]))) balances.update(cursor) return balances def get_rec_name(self, name): if self.debit > self.credit: return self.account.rec_name else: return '(%s)' % self.account.rec_name @classmethod def search_rec_name(cls, name, clause): return [('account.rec_name',) + tuple(clause[1:])] class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): 'Account Move Line' __name__ = 'account.move.line' _states = { 'readonly': Eval('move_state') == 'posted', } debit = Monetary( "Debit", currency='currency', digits='currency', required=True, states=_states, depends={'credit', 'tax_lines', 'journal'}) credit = Monetary( "Credit", currency='currency', digits='currency', required=True, states=_states, depends={'debit', 'tax_lines', 'journal'}) account = fields.Many2One('account.account', 'Account', required=True, domain=[ ('company', '=', Eval('company', -1)), ('type', '!=', None), ('closed', '!=', True), ['OR', ('start_date', '=', None), ('start_date', '<=', Eval('date', None)), ], ['OR', ('end_date', '=', None), ('end_date', '>=', Eval('date', None)), ], ], context={ 'company': Eval('company', -1), 'period': Eval('period', -1), }, states=_states, depends={'company', 'period'}) move = fields.Many2One( 'account.move', "Move", required=True, ondelete='CASCADE', states={ 'required': False, 'readonly': (((Eval('state') == 'valid') | _states['readonly']) & Bool(Eval('move'))), }) journal = fields.Function(fields.Many2One( 'account.journal', 'Journal', states=_states, context={ 'company': Eval('company', -1), }, depends={'company'}), 'get_move_field', setter='set_move_field', searcher='search_move_field') period = fields.Function(fields.Many2One('account.period', 'Period', states=_states), 'get_move_field', setter='set_move_field', searcher='search_move_field') company = fields.Function(fields.Many2One( 'company.company', "Company", states=_states), 'get_move_field', setter='set_move_field', searcher='search_move_field') date = fields.Function(fields.Date('Effective Date', required=True, states=_states), 'on_change_with_date', setter='set_move_field', searcher='search_move_field') origin = fields.Reference( "Origin", selection='get_origin', states=_states) move_origin = fields.Function( fields.Reference("Move Origin", selection='get_move_origin'), 'get_move_field', searcher='search_move_field') description = fields.Char('Description', states=_states) ext_ref = fields.Char('Ext. Ref') move_description_used = fields.Function( fields.Char("Move Description", states=_states), 'get_move_field', setter='set_move_field', searcher='search_move_field') amount_second_currency = Monetary( "Amount Second Currency", currency='second_currency', digits='second_currency', states={ 'required': Bool(Eval('second_currency')), 'readonly': _states['readonly'], }, help='The amount expressed in a second currency.') second_currency = fields.Many2One('currency.currency', 'Second Currency', help='The second currency.', domain=[ If(~Eval('second_currency_required'), ('id', '!=', Eval('currency', -1)), ('id', '=', Eval('second_currency_required', -1))), ], states={ 'required': (Bool(Eval('amount_second_currency')) | Bool(Eval('second_currency_required'))), 'readonly': _states['readonly'] }) second_currency_required = fields.Function( fields.Many2One('currency.currency', "Second Currency Required"), 'on_change_with_second_currency_required') party = fields.Many2One( 'party.party', "Party", states={ 'required': Eval('party_required', False), 'invisible': ~Eval('party_required', False), 'readonly': _states['readonly'], }, context={ 'company': Eval('company', -1), }, depends={'company'}, ondelete='RESTRICT') party_required = fields.Function(fields.Boolean('Party Required'), 'on_change_with_party_required') maturity_date = fields.Date( "Maturity Date", states={ 'invisible': ~Eval('has_maturity_date'), }, depends=['has_maturity_date'], help="Set a date to make the line payable or receivable.") has_maturity_date = fields.Function( fields.Boolean("Has Maturity Date"), 'on_change_with_has_maturity_date') state = fields.Selection([ ('draft', 'Draft'), ('valid', 'Valid'), ], 'State', readonly=True, required=True, sort=False) reconciliation = fields.Many2One( 'account.move.reconciliation', 'Reconciliation', readonly=True, ondelete='SET NULL') reconciliations_delegated = fields.One2Many( 'account.move.reconciliation', 'delegate_to', "Reconciliations Delegated", readonly=True) tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines') move_state = fields.Function( fields.Selection('get_move_states', "Move State"), 'on_change_with_move_state', searcher='search_move_field') currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency', searcher='search_currency') amount = fields.Function(Monetary( "Amount", currency='amount_currency', digits='amount_currency'), 'get_amount') amount_currency = fields.Function(fields.Many2One('currency.currency', 'Amount Currency'), 'get_amount_currency') delegated_amount = fields.Function(Monetary( "Delegated Amount", currency='amount_currency', digits='amount_currency', states={ 'invisible': ( ~Eval('reconciliation', False) | ~Eval('delegated_amount', 0)), }), 'get_delegated_amount') payable_receivable_balance = fields.Function( Monetary( "Payable/Receivable Balance", currency='amount_currency', digits='amount_currency'), 'get_payable_receivable_balance') journal = fields.Function(fields.Many2One('account.journal',"Journal"),'get_journal') rate = fields.Function(fields.Numeric("Rate",digits=(1,6)),'get_rate') del _states @classmethod def __setup__(cls): super(Line, cls).__setup__() cls.__access__.add('move') cls._check_modify_exclude = { 'maturity_date', 'reconciliation', 'tax_lines'} cls._reconciliation_modify_disallow = { 'account', 'debit', 'credit', 'party', } table = cls.__table__() cls._sql_constraints += [ ('credit_debit', Check(table, table.credit * table.debit == 0), 'account.msg_line_debit_credit'), ('second_currency_sign', Check(table, Coalesce(table.amount_second_currency, 0) * (table.debit - table.credit) >= 0), 'account.msg_line_second_currency_sign'), ] cls.__rpc__.update({ 'on_write': RPC(instantiate=0), }) # Do not cache default_date nor default_move cls.__rpc__['default_get'].cache = None cls._order[0] = ('id', 'DESC') cls._sql_indexes.update({ Index( table, (table.account, Index.Equality()), (table.party, Index.Equality())), Index(table, (table.reconciliation, Index.Equality())), # Index for General Ledger Index( table, (table.move, Index.Equality()), (table.account, Index.Equality())), # Index for account.account.party Index( table, (table.account, Index.Equality()), (table.party, Index.Equality()), (table.id, Index.Equality()), where=table.party != Null), # Index for receivable/payable balance Index( table, (table.account, Index.Equality()), (table.party, Index.Equality()), where=table.reconciliation == Null), }) @classmethod def default_date(cls): ''' Return the date of the last line for journal, period or the starting date of the period or today ''' pool = Pool() Period = pool.get('account.period') Date = pool.get('ir.date') context = Transaction().context date = Date.today() lines = cls.search([ ('company', '=', context.get('company')), ('move.journal', '=', context.get('journal')), ('period', '=', context.get('period')), ], order=[('date', 'DESC')], limit=1) if lines: line, = lines date = line.date elif context.get('period'): period = Period(context['period']) if period.start_date >= date: date = period.start_date else: date = period.end_date if context.get('date'): date = context['date'] return date @classmethod def default_move(cls): transaction = Transaction() context = transaction.context if context.get('journal') and context.get('period'): lines = cls.search([ ('company', '=', context.get('company')), ('move.journal', '=', context['journal']), ('move.period', '=', context['period']), ('create_uid', '=', transaction.user), ('state', '=', 'draft'), ], order=[('id', 'DESC')], limit=1) if lines: line, = lines return line.move.id def get_rate(self,name=None): if self.amount_second_currency: return round((self.credit if self.credit else self.debit) / abs(self.amount_second_currency),6) @fields.depends( 'debit', 'credit', 'date','second_currency','amount_second_currency') def on_change_amount_second_currency(self): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') if self.second_currency != None and self.amount_second_currency != None and (not self.credit and not self.debit): tdate = Date.today() if self.date: tdate = self.date rate = Currency._get_rate([self.second_currency],tdate) if rate: rate = rate[self.second_currency.id] if self.amount_second_currency > 0: self.debit = round(rate * self.amount_second_currency,2) elif self.amount_second_currency > 0: self.credit = round(rate * abs(self.amount_second_currency),2) self.rate = self.get_rate() @fields.depends( 'debit', 'credit', 'date','second_currency','amount_second_currency') def on_change_second_currency(self): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') if self.second_currency != None and self.amount_second_currency != None and (not self.credit and not self.debit): tdate = Date.today() if self.date: tdate = self.date rate = Currency._get_rate([self.second_currency],tdate) if rate: rate = rate[self.second_currency.id] if self.amount_second_currency > 0: self.debit = round(rate * self.amount_second_currency,2) elif self.amount_second_currency > 0: self.credit = round(rate * abs(self.amount_second_currency),2) self.rate = self.get_rate() @fields.depends( 'move', 'debit', 'credit', '_parent_move.lines', '_parent_move.company') def on_change_move(self): if self.move: if not self.debit and not self.credit: total = sum((l.debit or 0) - (l.credit or 0) for l in getattr(self.move, 'lines', [])) self.debit = -total if total < 0 else Decimal(0) self.credit = total if total > 0 else Decimal(0) self.company = self.move.company def get_journal(self,name): return self.move.journal @classmethod def default_company(cls): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_debit(): return Decimal(0) @staticmethod def default_credit(): return Decimal(0) @fields.depends('account') def on_change_with_currency(self, name=None): return self.account.currency if self.account else None @classmethod def search_currency(cls, name, clause): return [('account.company.' + clause[0], * clause[1:])] @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' return ['account.move.line','forex.forex', 'stock.move'] @classmethod def get_origin(cls): Model = Pool().get('ir.model') get_name = Model.get_name models = cls._get_origin() return [(None, '')] + [(m, get_name(m)) for m in models] @property def origin_rec_name(self): if self.origin: return self.origin.rec_name elif self.move_origin: return self.move_origin.rec_name else: return '' @fields.depends('debit', 'credit', 'amount_second_currency') def on_change_debit(self): if self.debit: self.credit = Decimal(0) self._amount_second_currency_sign() @fields.depends('debit', 'credit', 'amount_second_currency') def on_change_credit(self): if self.credit: self.debit = Decimal(0) self._amount_second_currency_sign() @fields.depends('amount_second_currency', 'debit', 'credit') def on_change_amount_second_currency(self): self._amount_second_currency_sign() def _amount_second_currency_sign(self): 'Set correct sign to amount_second_currency' if self.amount_second_currency: logger.info("_AMOUNT:%s",self.amount_second_currency) self.amount_second_currency = \ self.amount_second_currency.copy_sign( (self.debit or 0) - (self.credit or 0)) @fields.depends('account') def on_change_account(self): if self.account: if self.account.second_currency: self.second_currency = self.account.second_currency if not self.account.party_required: self.party = None @fields.depends('account') def on_change_with_second_currency_required(self, name=None): if self.account and self.account.second_currency: return self.account.second_currency.id @fields.depends('account') def on_change_with_party_required(self, name=None): if self.account: return self.account.party_required return False @fields.depends('move', '_parent_move.date') def on_change_with_date(self, name=None): if self.move: return self.move.date @fields.depends('move', '_parent_move.state') def on_change_with_move_state(self, name=None): if self.move: return self.move.state @classmethod def order_maturity_date(self, tables): pool = Pool() Move = pool.get('account.move') table, _ = tables[None] if 'move' not in tables: move = Move.__table__() tables['move'] = { None: (move, table.move == move.id), } else: move, _ = tables['move'][None] return [Coalesce(table.maturity_date, move.date)] @fields.depends('account') def on_change_with_has_maturity_date(self, name=None): if self.account: type_ = self.account.type return type_.receivable or type_.payable order_journal = MoveLineMixin._order_move_field('journal') order_period = MoveLineMixin._order_move_field('period') order_company = MoveLineMixin._order_move_field('company') order_date = MoveLineMixin._order_move_field('date') order_move_origin = MoveLineMixin._order_move_field('origin') order_move_state = MoveLineMixin._order_move_field('state') def get_delegated_amount(self, name): def final_delegated_line(line): if not line.reconciliation or not line.reconciliation.delegate_to: return line return final_delegated_line(line.reconciliation.delegate_to) final_delegation = final_delegated_line(self) if final_delegation == self: return None elif final_delegation.reconciliation: return final_delegation.amount_currency.round(0) else: return final_delegation.amount @classmethod def query_get(cls, table): ''' Return SQL clause and fiscal years for account move line depending of the context. table is the SQL instance of account.move.line table ''' pool = Pool() FiscalYear = pool.get('account.fiscalyear') Move = pool.get('account.move') Period = pool.get('account.period') move = Move.__table__() period = Period.__table__() fiscalyear = FiscalYear.__table__() context = Transaction().context company = context.get('company') fiscalyear_ids = [] where = Literal(True) if context.get('posted'): where &= move.state == 'posted' if context.get('journal'): where &= move.journal == context['journal'] date = context.get('date') from_date, to_date = context.get('from_date'), context.get('to_date') fiscalyear_id = context.get('fiscalyear') period_ids = context.get('periods') if date: fiscalyears = FiscalYear.search([ ('start_date', '<=', date), ('end_date', '>=', date), ('company', '=', company), ], order=[('start_date', 'DESC')], limit=1) if fiscalyears: fiscalyear_id = fiscalyears[0].id else: fiscalyear_id = -1 fiscalyear_ids = list(map(int, fiscalyears)) where &= period.fiscalyear == fiscalyear_id where &= move.date <= date elif fiscalyear_id or period_ids is not None or from_date or to_date: if fiscalyear_id: fiscalyear_ids = [fiscalyear_id] where &= fiscalyear.id == fiscalyear_id if period_ids is not None: where &= move.period.in_(period_ids or [None]) if from_date: where &= move.date >= from_date if to_date: where &= move.date <= to_date else: where &= fiscalyear.state == 'open' where &= fiscalyear.company == company fiscalyears = FiscalYear.search([ ('state', '=', 'open'), ('company', '=', company), ]) fiscalyear_ids = list(map(int, fiscalyears)) # Use LEFT JOIN to allow database optimization # if no joined table is used in the where clause. return (table.move.in_(move .join(period, 'LEFT', condition=move.period == period.id) .join(fiscalyear, 'LEFT', condition=period.fiscalyear == fiscalyear.id) .select(move.id, where=where)), fiscalyear_ids) @classmethod def on_write(cls, lines): return list(set(l.id for line in lines for l in line.move.lines)) @classmethod def validate_fields(cls, lines, field_names): super(Line, cls).validate(lines) cls.check_account(lines, field_names) @classmethod def check_account(cls, lines, field_names=None): if field_names and not (field_names & {'account', 'party'}): return for line in lines: if not line.account.type or line.account.closed: raise AccessError( gettext('account.msg_line_closed_account', account=line.account.rec_name)) if bool(line.party) != bool(line.account.party_required): error = 'party_set' if line.party else 'party_required' raise AccessError( gettext('account.msg_line_%s' % error, account=line.account.rec_name, line=line.rec_name)) @classmethod def check_journal_period_modify(cls, period, journal): ''' Check if the lines can be modified or created for the journal - period and if there is no journal - period, create it ''' JournalPeriod = Pool().get('account.journal.period') journal_periods = JournalPeriod.search([ ('journal', '=', journal.id), ('period', '=', period.id), ], limit=1) if journal_periods: journal_period, = journal_periods if journal_period.state == 'closed': raise AccessError( gettext('account.msg_modify_line_closed_journal_period', journal_period=journal_period.rec_name)) else: JournalPeriod.lock() JournalPeriod.create([{ 'journal': journal.id, 'period': period.id, }]) @classmethod def check_modify(cls, lines, modified_fields=None): ''' Check if the lines can be modified ''' if (modified_fields is not None and modified_fields <= cls._check_modify_exclude): return journal_period_done = [] for line in lines: if line.move.state == 'posted': raise AccessError( gettext('account.msg_modify_line_posted_move', line=line.rec_name, move=line.move.rec_name)) journal_period = (line.journal.id, line.period.id) if journal_period not in journal_period_done: cls.check_journal_period_modify(line.period, line.journal) journal_period_done.append(journal_period) @classmethod def check_reconciliation(cls, lines, modified_fields=None): if (modified_fields is not None and not modified_fields & cls._reconciliation_modify_disallow): return for line in lines: if line.reconciliation: raise AccessError( gettext('account.msg_modify_line_reconciled', line=line.rec_name)) @classmethod def view_attributes(cls): attributes = super().view_attributes() view_ids = cls._view_reconciliation_muted() if Transaction().context.get('view_id') in view_ids: attributes.append( ('/tree', 'visual', If(Bool(Eval('reconciliation')), 'muted', ''))) return attributes @classmethod def _view_reconciliation_muted(cls): pool = Pool() ModelData = pool.get('ir.model.data') return {ModelData.get_id( 'account', 'move_line_view_list_payable_receivable')} @classmethod def delete(cls, lines): Move = Pool().get('account.move') cls.check_modify(lines) cls.check_reconciliation(lines) moves = [x.move for x in lines] super(Line, cls).delete(lines) Move.validate_move(moves) @classmethod def write(cls, *args): Move = Pool().get('account.move') actions = iter(args) args = [] moves = [] all_lines = [] for lines, values in zip(actions, actions): cls.check_modify(lines, set(values.keys())) cls.check_reconciliation(lines, set(values.keys())) moves.extend((x.move for x in lines)) all_lines.extend(lines) args.extend((lines, values)) super(Line, cls).write(*args) Transaction().timestamp = {} Move.validate_move(list(set(l.move for l in all_lines) | set(moves))) @classmethod def create(cls, vlist): pool = Pool() Move = pool.get('account.move') def move_fields(move_name): for fname, field in cls._fields.items(): if (isinstance(field, fields.Function) and field.setter == 'set_move_field'): if move_name and fname.startswith('move_'): fname = fname[5:] yield fname moves = {} context = Transaction().context vlist = [x.copy() for x in vlist] for vals in vlist: if not vals.get('move'): move_values = {} for fname in move_fields(move_name=True): move_values[fname] = vals.get(fname) or context.get(fname) key = tuple(sorted(move_values.items())) move = moves.get(key) if move is None: move = Move(**move_values) move.save() moves[key] = move vals['move'] = move.id else: # prevent default value for field with set_move_field for fname in move_fields(move_name=False): vals.setdefault(fname, None) lines = super(Line, cls).create(vlist) period_and_journals = set((line.period, line.journal) for line in lines) for period, journal in period_and_journals: cls.check_journal_period_modify(period, journal) # Re-browse for cache alignment moves = Move.browse(list(set(line.move for line in lines))) Move.check_modify(moves) Move.validate_move(moves) return lines @classmethod def copy(cls, lines, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('move', None) default.setdefault('reconciliation', None) default.setdefault('reconciliations_delegated', []) return super(Line, cls).copy(lines, default=default) @classmethod def view_toolbar_get(cls): pool = Pool() Template = pool.get('account.move.template') toolbar = super(Line, cls).view_toolbar_get() # Add a wizard entry for each templates context = Transaction().context company = context.get('company') journal = context.get('journal') period = context.get('period') if company and journal and period: templates = Template.search([ ('company', '=', company), ('journal', '=', journal), ]) for template in templates: action = toolbar['action'] # Use template id for action id to auto-select the template action.append({ 'name': template.name, 'type': 'ir.action.wizard', 'wiz_name': 'account.move.template.create', 'id': template.id, }) return toolbar @classmethod def reconcile( cls, *lines_list, date=None, writeoff=None, description=None, delegate_to=None): """ Reconcile each list of lines together. The writeoff keys are: date, method and description. """ pool = Pool() Reconciliation = pool.get('account.move.reconciliation') Move = pool.get('account.move') Lang = pool.get('ir.lang') lang = Lang.get() delegate_to = delegate_to.id if delegate_to else None logger.info("RECONCILE:%s",lines_list) reconciliations = [] to_post = [] for lines in lines_list: if not lines: continue for line in lines: if line.reconciliation: raise AccessError( gettext('account.msg_line_already_reconciled', line=line.rec_name)) lines = list(lines) reconcile_account = None reconcile_party = None amount = Decimal(0) amount_second_currency = Decimal(0) second_currencies = set() posted = True for line in lines: posted &= line.move.state == 'posted' amount += line.debit - line.credit if not reconcile_account: reconcile_account = line.account if not reconcile_party: reconcile_party = line.party if line.amount_second_currency is not None: amount_second_currency += line.amount_second_currency second_currencies.add(line.second_currency) company = reconcile_account.company try: second_currency, = second_currencies except ValueError: amount_second_currency = None second_currency = None if second_currency: writeoff_amount = amount_second_currency writeoff_currency = second_currency else: writeoff_amount = amount writeoff_currency = company.currency if writeoff_amount: if not writeoff: raise ReconciliationError(gettext( 'account.msg_reconciliation_write_off_missing', amount=lang.currency( writeoff_amount, writeoff_currency))) move = cls._get_writeoff_move( reconcile_account, reconcile_party, writeoff_amount, writeoff_currency, writeoff, date=date, description=description) move.save() if posted: to_post.append(move) for line in move.lines: if line.account == reconcile_account: lines.append(line) amount += line.debit - line.credit if second_currency and amount: move = cls._get_exchange_move( reconcile_account, reconcile_party, amount, date,lines) move.save() if posted: to_post.append(move) for line in move.lines: if line.account == reconcile_account: lines.append(line) amount += line.debit - line.credit assert not amount, f"{amount} must be zero" reconciliations.append({ 'company': reconcile_account.company, 'lines': [('add', [x.id for x in lines])], 'date': max(l.date for l in lines), 'delegate_to': delegate_to, }) if to_post: Move.post(to_post) return Reconciliation.create(reconciliations) @classmethod def _get_writeoff_move( cls, reconcile_account, reconcile_party, amount, currency, writeoff, date=None, description=None): pool = Pool() Currency = pool.get('currency.currency') Date = pool.get('ir.date') Period = pool.get('account.period') Move = pool.get('account.move') company = reconcile_account.company if not date: with Transaction().set_context(company=company.id): date = Date.today() period = Period.find(reconcile_account.company, date=date) if amount >= 0: account = writeoff.debit_account else: account = writeoff.credit_account journal = writeoff.journal logger.info("WRITEOFF:%s",currency) if currency != company.currency: amount_second_currency = amount second_currency = currency with Transaction().set_context(date=date): amount = Currency.compute( currency, amount, company.currency) else: amount_second_currency = None second_currency = None move = Move() move.company = company move.journal = journal move.period = period move.date = date move.description = description lines = [] line = cls() lines.append(line) line.account = reconcile_account line.party = reconcile_party line.debit = -amount if amount < 0 else 0 line.credit = amount if amount > 0 else 0 logger.info("WRITEOFFMOVE:%s",second_currency) if second_currency: line.amount_second_currency = ( amount_second_currency).copy_sign( line.debit - line.credit) line.second_currency = second_currency line = cls() lines.append(line) line.account = account line.party = ( reconcile_party if account.party_required else None) line.debit = amount if amount > 0 else 0 line.credit = -amount if amount < 0 else 0 if account.second_currency: with Transaction().set_context(date=date): line.amount_second_currency = Currency.compute( company.currency, amount, reconcile_account.second_currency).copy_sign( line.debit - line.credit) move.lines = lines return move @classmethod def _get_exchange_move(cls, account, party, amount, date=None, lines_list=None,): pool = Pool() Configuration = pool.get('account.configuration') Date = pool.get('ir.date') Move = pool.get('account.move') Period = pool.get('account.period') configuration = Configuration(1) company = account.company if not date: with Transaction().set_context(company=company.id): date = Date.today() period_id = Period.find(company.id, date=date) move = Move() move.company = company move.journal = configuration.get_multivalue( 'currency_exchange_journal', company=company.id) if not move.journal: raise JournalMissing(gettext( 'account.' 'msg_reconciliation_currency_exchange_journal_missing', company=company.rec_name)) move.period = period_id move.date = date lines = [] line = cls() lines.append(line) line.account = account line.party = party line.debit = -amount if amount < 0 else 0 line.credit = amount if amount > 0 else 0 logger.info("LINES_LIST:%s",lines_list) if lines_list: if len(lines_list)>1: line.revaluate = lines_list[1].id line.lot = lines_list[1].lot.id move.date = lines_list[1].move.date line = cls() lines.append(line) line.debit = amount if amount > 0 else 0 line.credit = -amount if amount < 0 else 0 if lines_list: if len(lines_list)>1: line.revaluate = lines_list[1].id line.lot = lines_list[1].lot.id if line.credit: line.account = configuration.get_multivalue( 'currency_exchange_credit_account', company=company.id) if not line.account: raise AccountMissing(gettext( 'account.' 'msg_reconciliation_currency_exchange_' 'credit_account_missing', company=company.rec_name)) else: line.account = configuration.get_multivalue( 'currency_exchange_debit_account', company=company.id) if not line.account: raise AccountMissing(gettext( 'account.' 'msg_reconciliation_currency_exchange_' 'debit_account_missing', company=company.rec_name)) move.lines = lines return move class LineReceivablePayableContext(ModelView): "Receivable/Payable Line Context" __name__ = 'account.move.line.receivable_payable.context' reconciled = fields.Boolean("Reconciled") receivable = fields.Boolean("Receivable") payable = fields.Boolean("Payable") @classmethod def default_reconciled(cls): return Transaction().context.get('reconciled', False) @classmethod def default_receivable(cls): return Transaction().context.get('receivable', True) @classmethod def default_payable(cls): return Transaction().context.get('payable', True) class WriteOff(DeactivableMixin, ModelSQL, ModelView): 'Reconcile Write Off' __name__ = 'account.move.reconcile.write_off' company = fields.Many2One('company.company', "Company", required=True) name = fields.Char("Name", required=True, translate=True) journal = fields.Many2One('account.journal', "Journal", required=True, domain=[('type', '=', 'write-off')], context={ 'company': Eval('company', -1), }, depends={'company'}) credit_account = fields.Many2One('account.account', "Credit Account", required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('company')), ]) debit_account = fields.Many2One('account.account', "Debit Account", required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('company')), ]) @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def default_company(cls): return Transaction().context.get('company') class OpenJournalAsk(ModelView): 'Open Journal Ask' __name__ = 'account.move.open_journal.ask' journal = fields.Many2One('account.journal', 'Journal', required=True) period = fields.Many2One('account.period', 'Period', required=True, domain=[ ('state', '!=', 'closed'), ]) @classmethod def default_period(cls): pool = Pool() Period = pool.get('account.period') company = Transaction().context.get('company') try: period = Period.find(company) except PeriodNotFoundError: return None return period.id class OpenJournal(Wizard): 'Open Journal' __name__ = 'account.move.open_journal' start = StateTransition() ask = StateView('account.move.open_journal.ask', 'account.open_journal_ask_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'open_', 'tryton-ok', default=True), ]) open_ = StateAction('account.act_move_line_form') def transition_start(self): if (self.model and self.model.__name__ == 'account.journal.period' and self.record): return 'open_' return 'ask' def default_ask(self, fields): if (self.model and self.model.__name__ == 'account.journal.period' and self.record): return { 'journal': self.record.journal.id, 'period': self.record.period.id, } return {} def do_open_(self, action): JournalPeriod = Pool().get('account.journal.period') if (self.model and self.model.__name__ == 'account.journal.period' and self.record): journal = self.record.journal period = self.record.period else: journal = self.ask.journal period = self.ask.period journal_periods = JournalPeriod.search([ ('journal', '=', journal.id), ('period', '=', period.id), ], limit=1) if not journal_periods: journal_period, = JournalPeriod.create([{ 'journal': journal.id, 'period': period.id, }]) else: journal_period, = journal_periods action['name'] += ' (%s)' % journal_period.rec_name action['pyson_domain'] = PYSONEncoder().encode([ ('move.journal', '=', journal.id), ('period', '=', period.id), ('company', '=', period.company.id), ]) action['pyson_context'] = PYSONEncoder().encode({ 'move.journal': journal.id, 'period': period.id, 'company': period.company.id, }) return action, {} def transition_open_(self): return 'end' class OpenAccount(Wizard): 'Open Account' __name__ = 'account.move.open_account' start_state = 'open_' open_ = StateAction('account.act_move_line_form') def do_open_(self, action): pool = Pool() FiscalYear = pool.get('account.fiscalyear') context = Transaction().context company_id = self.record.company.id if self.record else -1 date = context.get('date') fiscalyear = context.get('fiscalyear') if date: fiscalyears = FiscalYear.search([ ('start_date', '<=', date), ('end_date', '>=', date), ('company', '=', company_id), ], order=[('start_date', 'DESC')], limit=1) elif fiscalyear: fiscalyears = [FiscalYear(fiscalyear)] else: fiscalyears = FiscalYear.search([ ('state', '=', 'open'), ('company', '=', company_id), ]) periods = [p for f in fiscalyears for p in f.periods] action['pyson_domain'] = [ ('period', 'in', [p.id for p in periods]), ('account', '=', self.record.id if self.record else None), ('state', '=', 'valid'), ] if Transaction().context.get('posted'): action['pyson_domain'].append(('move.state', '=', 'posted')) if Transaction().context.get('date'): action['pyson_domain'].append(('move.date', '<=', Transaction().context['date'])) if self.record: action['name'] += ' (%s)' % self.record.rec_name action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain']) action['pyson_context'] = PYSONEncoder().encode({ 'fiscalyear': Transaction().context.get('fiscalyear'), }) return action, {} class ReconcileLinesWriteOff(ModelView): 'Reconcile Lines Write-Off' __name__ = 'account.move.reconcile_lines.writeoff' company = fields.Many2One('company.company', "Company", readonly=True) writeoff = fields.Many2One('account.move.reconcile.write_off', "Write Off", required=True, domain=[ ('company', '=', Eval('company')), ]) date = fields.Date('Date', required=True) amount = Monetary( "Amount", currency='currency', digits='currency', readonly=True) currency = fields.Many2One('currency.currency', "Currency", readonly=True) description = fields.Char('Description') @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() class ReconcileLines(Wizard): 'Reconcile Lines' __name__ = 'account.move.reconcile_lines' start = StateTransition() writeoff = StateView('account.move.reconcile_lines.writeoff', 'account.reconcile_lines_writeoff_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Reconcile', 'reconcile', 'tryton-ok', default=True), ]) reconcile = StateTransition() @classmethod def check_access(cls, models=None): transaction = Transaction() if transaction.user: context = transaction.context models = set() if models is None else models.copy() models.update({'account.move.line', 'account.general_ledger.line'}) model = context.get('active_model') if model and model not in models: raise AccessError(gettext( 'ir.msg_access_wizard_model_error', wizard=cls.__name__, model=model)) super().check_access() def get_writeoff(self): "Return writeoff amount and company" company = currency = None amount = Decimal(0) amount_second_currency = Decimal(0) second_currencies = set() for line in self.records: amount += line.debit - line.credit if line.amount_second_currency is not None: amount_second_currency += line.amount_second_currency second_currencies.add(line.second_currency) if not company: company = line.account.company currency = company.currency try: second_currency, = second_currencies except ValueError: second_currency = None if second_currency: amount = amount_second_currency currency = second_currency return amount, currency, company def transition_start(self): amount, currency, company = self.get_writeoff() if not company: return 'end' if currency.is_zero(amount): return 'reconcile' return 'writeoff' def default_writeoff(self, fields): amount, currency, company = self.get_writeoff() return { 'amount': amount, 'currency': currency.id, 'company': company.id, } def transition_reconcile(self): self.model.reconcile( self.records, writeoff=getattr(self.writeoff, 'writeoff', None), date=getattr(self.writeoff, 'date', None), description=getattr(self.writeoff, 'description', None)) return 'end' class UnreconcileLines(Wizard): 'Unreconcile Lines' __name__ = 'account.move.unreconcile_lines' start_state = 'unreconcile' unreconcile = StateTransition() @classmethod def check_access(cls, models=None): transaction = Transaction() if transaction.user: context = transaction.context models = set() if models is None else models.copy() models.update({'account.move.line', 'account.general_ledger.line'}) model = context.get('active_model') if model and model not in models: raise AccessError(gettext( 'ir.msg_access_wizard_model_error', wizard=cls.__name__, model=model)) super().check_access() def transition_unreconcile(self): self.make_unreconciliation(self.records) return 'end' @classmethod def make_unreconciliation(cls, lines): pool = Pool() Reconciliation = pool.get('account.move.reconciliation') reconciliations = [x.reconciliation for x in lines if x.reconciliation] if reconciliations: Reconciliation.delete(reconciliations) class Reconcile(Wizard): 'Reconcile' __name__ = 'account.reconcile' start = StateView( 'account.reconcile.start', 'account.reconcile_start_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Reconcile", 'next_', 'tryton-ok', default=True), ]) next_ = StateTransition() show = StateView('account.reconcile.show', 'account.reconcile_show_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Skip', 'next_', 'tryton-forward', validate=False), Button('Reconcile', 'reconcile', 'tryton-ok', default=True), ]) reconcile = StateTransition() def get_accounts(self): 'Return a list of account id to reconcile' pool = Pool() Rule = pool.get('ir.rule') Line = pool.get('account.move.line') line = Line.__table__() Account = pool.get('account.account') AccountType = pool.get('account.account.type') account = Account.__table__() account_type = AccountType.__table__() cursor = Transaction().connection.cursor() account_rule = Rule.query_get(Account.__name__) if self.model and self.model.__name__ == 'account.move.line': lines = [l for l in self.records if not l.reconciliation] return list({l.account for l in lines if l.account.reconcile}) balance = line.debit - line.credit cursor.execute(*line.join(account, condition=line.account == account.id) .join(account_type, condition=account.type == account_type.id) .select( account.id, where=((line.reconciliation == Null) & account.reconcile & account.id.in_(account_rule)), group_by=[account.id, account_type.receivable, account_type.payable], having=(( Sum(Case((balance > 0, 1), else_=0)) > 0) & (Sum(Case((balance < 0, 1), else_=0)) > 0) | Case((account_type.receivable & ~account_type.payable, Sum(balance) < 0), else_=False) | Case((account_type.payable & ~account_type.receivable, Sum(balance) > 0), else_=False) ))) return [a for a, in cursor] def get_parties(self, account, party=None): 'Return a list party to reconcile for the account' pool = Pool() Line = pool.get('account.move.line') line = Line.__table__() cursor = Transaction().connection.cursor() if self.model and self.model.__name__ == 'account.move.line': lines = [l for l in self.records if not l.reconciliation] return list({l.party for l in lines if l.account == account}) where = ((line.reconciliation == Null) & (line.account == account.id)) if party: where &= (line.party == party.id) cursor.execute(*line.select(line.party, where=where, group_by=line.party)) return [p for p, in cursor] def get_currencies(self, account, party, currency=None, _balanced=False): "Return a list of currencies to reconcile for the account and party" pool = Pool() Line = pool.get('account.move.line') line = Line.__table__() cursor = Transaction().connection.cursor() if self.model and self.model.__name__ == 'account.move.line': lines = [l for l in self.records if not l.reconciliation] return list({ (l.second_currency or l.currency).id for l in lines if l.account == account and l.party == party}) balance = Case( (line.second_currency != Null, line.amount_second_currency), else_=line.debit - line.credit) if _balanced: having = Sum(balance) == 0 else: having = (( Sum(Case((balance > 0, 1), else_=0)) > 0) & (Sum(Case((balance < 0, 1), else_=0)) > 0) | Case((account.type.receivable, Sum(balance) < 0), else_=False) | Case((account.type.payable, Sum(balance) > 0), else_=False) ) where = ((line.reconciliation == Null) & (line.account == account.id) & (line.party == (party.id if party else None))) currency_expr = Coalesce(line.second_currency, account.currency.id) if currency: where &= (currency_expr == currency.id) cursor.execute(*line.select( currency_expr, where=where, group_by=line.second_currency, having=having)) return [p for p, in cursor] @check_access def transition_next_(self): pool = Pool() Line = pool.get('account.move.line') def next_account(): accounts = list(self.show.accounts) if not accounts: return account = accounts.pop() self.show.account = account self.show.accounts = accounts self.show.parties = self.get_parties(account) return account def next_party(): parties = list(self.show.parties) if not parties: return party = parties.pop() self.show.party = party self.show.parties = parties self.show.currencies = self.get_currencies( self.show.account, party) return party, def next_currency(): currencies = list(self.show.currencies) if not currencies: return currency = currencies.pop() self.show.currency = currency self.show.currencies = currencies return currency if getattr(self.show, 'accounts', None) is None: self.show.accounts = self.get_accounts() if not (next_account() and next_party()): return 'end' if getattr(self.show, 'parties', None) is None: self.show.parties = self.get_parties(self.show.account) if not next_party(): return 'end' if getattr(self.show, 'currencies', None) is None: self.show.currencies = self.get_currencies( self.show.account, self.show.party) while True: while not next_currency(): while not next_party(): if not next_account(): return 'end' if self.start.automatic or self.start.only_balanced: lines = self._default_lines() if lines and self.start.automatic: while lines: Line.reconcile(lines) lines = self._default_lines() continue elif not lines and self.start.only_balanced: continue return 'show' def default_show(self, fields): pool = Pool() Date = pool.get('ir.date') defaults = {} defaults['accounts'] = [a.id for a in self.show.accounts] defaults['account'] = self.show.account.id defaults['company'] = self.show.account.company.id defaults['parties'] = [p.id for p in self.show.parties] defaults['party'] = self.show.party.id if self.show.party else None defaults['currencies'] = [c.id for c in self.show.currencies] defaults['currency'] = ( self.show.currency.id if self.show.currency else None) defaults['lines'] = list(map(int, self._default_lines())) defaults['write_off_amount'] = None with Transaction().set_context(company=self.show.account.company.id): defaults['date'] = Date.today() return defaults def _all_lines(self): 'Return all lines to reconcile for the current state' pool = Pool() Line = pool.get('account.move.line') return Line.search([ ('account', '=', self.show.account.id), ('party', '=', self.show.party.id if self.show.party else None), ('reconciliation', '=', None), ['OR', [ ('currency', '=', self.show.currency.id), ('second_currency', '=', None), ], ('second_currency', '=', self.show.currency.id), ], ], order=[]) def _line_sort_key(self, line): return [line.maturity_date or line.date] def _default_lines(self): 'Return the larger list of lines which can be reconciled' if self.model and self.model.__name__ == 'account.move.line': requested = { l for l in self.records if l.account == self.show.account and l.party == self.show.party and (l.second_currency or l.currency) == self.show.currency} else: requested = None currency = self.show.currency def balance(line): if line.second_currency == currency: return line.amount_second_currency elif line.currency == currency: return line.debit - line.credit else: return 0 all_lines = self._all_lines() amount = sum(map(balance, all_lines)) if currency.is_zero(amount): return all_lines chunk = config.getint('account', 'reconciliation_chunk', default=10) # Combination is exponential so it must be limited to small number default = [] for lines in grouped_slice( sorted(all_lines, key=self._line_sort_key), chunk): lines = list(lines) best = None for n in range(len(lines), 1, -1): for comb_lines in combinations(lines, n): if requested and not requested.intersection(comb_lines): continue amount = sum(balance(l) for l in comb_lines) if currency.is_zero(amount): best = comb_lines break if best: break if best: default.extend(best) if not default and requested: return requested else: return default def transition_reconcile(self): pool = Pool() Line = pool.get('account.move.line') if self.show.lines: Line.reconcile(self.show.lines, date=self.show.date, writeoff=self.show.write_off, description=self.show.description) if self.get_currencies( self.show.account, self.show.party, currency=self.show.currency): return 'show' return 'next_' class ReconcileStart(ModelView): "Reconcile" __name__ = 'account.reconcile.start' automatic = fields.Boolean( "Automatic", help="Automatically reconcile suggestions.") only_balanced = fields.Boolean( "Only Balanced", help="Skip suggestion with write-off.") @classmethod def default_automatic(cls): return False @classmethod def default_only_balanced(cls): return False class ReconcileShow(ModelView): 'Reconcile' __name__ = 'account.reconcile.show' company = fields.Many2One('company.company', "Company", readonly=True) accounts = fields.Many2Many('account.account', None, None, 'Account', readonly=True) account = fields.Many2One('account.account', 'Account', readonly=True) parties = fields.Many2Many( 'party.party', None, None, 'Parties', readonly=True, context={ 'company': Eval('company', -1), }, depends={'company'}) party = fields.Many2One( 'party.party', 'Party', readonly=True, context={ 'company': Eval('company', -1), }, depends={'company'}) currencies = fields.Many2Many( 'currency.currency', None, None, "Currencies", readonly=True) currency = fields.Many2One('currency.currency', "Currency", readonly=True) lines = fields.Many2Many('account.move.line', None, None, 'Lines', domain=[ ('account', '=', Eval('account')), ('party', '=', Eval('party')), ('reconciliation', '=', None), ['OR', [ ('currency', '=', Eval('currency', -1)), ('second_currency', '=', None), ], ('second_currency', '=', Eval('currency', -1)), ], ]) _write_off_states = { 'required': Bool(Eval('write_off_amount', 0)), 'invisible': ~Eval('write_off_amount', 0), } write_off_amount = Monetary( "Amount", currency='currency', digits='currency', readonly=True, states=_write_off_states) write_off = fields.Many2One( 'account.move.reconcile.write_off', "Write Off", domain=[ ('company', '=', Eval('company', -1)), ], states=_write_off_states) date = fields.Date('Date', states=_write_off_states) description = fields.Char('Description', states={ 'invisible': _write_off_states['invisible'], }) @fields.depends('lines', 'currency', 'company') def on_change_lines(self): amount = Decimal(0) if self.currency: for line in self.lines: if line.second_currency == self.currency: amount += ( line.amount_second_currency or 0) elif line.currency == self.currency: amount += ( (line.debit or 0) - (line.credit or 0)) amount = self.currency.round(amount) self.write_off_amount = amount class CancelMoves(Wizard): 'Cancel Moves' __name__ = 'account.move.cancel' start_state = 'default' default = StateView('account.move.cancel.default', 'account.move_cancel_default_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('OK', 'cancel', 'tryton-ok', default=True), ]) cancel = StateTransition() def default_cancel(self, move): default = {} if self.default.description: default['description'] = self.default.description return default def transition_cancel(self): pool = Pool() Line = pool.get('account.move.line') Move = pool.get('account.move') Warning = pool.get('res.user.warning') Unreconcile = pool.get('account.move.unreconcile_lines', type='wizard') transaction = Transaction() moves = self.records moves_w_delegation = { m: [ml for ml in m.lines if ml.reconciliation and ml.reconciliation.delegate_to] for m in moves} if any(dml for dml in moves_w_delegation.values()): names = ', '.join(m.rec_name for m in islice(moves_w_delegation.keys(), None, 5)) if len(moves_w_delegation) > 5: names += '...' key = Warning.format('cancel_delegated', moves_w_delegation) if Warning.check(key): raise CancelDelegatedWarning( key, gettext( 'account.msg_cancel_line_delegated', moves=names)) to_post = [] for move in moves: if moves_w_delegation.get(move): with transaction.set_context(_skip_warnings=True): Unreconcile.make_unreconciliation(moves_w_delegation[move]) default = self.default_cancel(move) cancel_move = move.cancel( default=default, reversal=self.default.reversal) if move.state == 'posted': to_post.append(cancel_move) to_reconcile = defaultdict(list) for line in move.lines + cancel_move.lines: if line.account.reconcile: to_reconcile[(line.account, line.party)].append(line) for lines in to_reconcile.values(): Line.reconcile(lines) if to_post: Move.post(to_post) return 'end' class CancelMovesDefault(ModelView): 'Cancel Moves' __name__ = 'account.move.cancel.default' description = fields.Char('Description') reversal = fields.Boolean( "Reversal", help="Reverse debit and credit.") @classmethod def default_reversal(cls): return True class GroupLines(Wizard): "Group Lines" __name__ = 'account.move.line.group' start = StateView('account.move.line.group.start', 'account.move_line_group_start_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Group", 'group', 'tryton-ok', default=True), ]) group = StateAction('account.act_move_form_grouping') def do_group(self, action): move, balance_line = self._group_lines(self.records) if all(l.move.state == 'posted' for l in self.records): move.post() return action, {'res_id': move.id} def _group_lines(self, lines, date=None): move, balance_line = self.group_lines(lines, self.start.journal, date) move.description = self.start.description move.save() return move, balance_line @classmethod def group_lines(cls, lines, journal, date=None): pool = Pool() Line = pool.get('account.move.line') grouping = cls.grouping(lines) move, balance_line = cls.get_move(lines, grouping, journal, date) move.save() to_reconcile = defaultdict(list) for line in chain(lines, move.lines): if line.account.reconcile: to_reconcile[line.account].append(line) if balance_line: balance_line.move = move balance_line.save() for lines in to_reconcile.values(): Line.reconcile(lines, delegate_to=balance_line) return move, balance_line @classmethod def grouping(cls, lines): if len(lines) == 1: raise GroupLineError(gettext('account.msg_group_line_single')) companies = set() parties = set() accounts = set() second_currencies = set() for line in lines: if not cls.allow_grouping(line.account): raise GroupLineError(gettext('account.msg_group_line')) companies.add(line.move.company) parties.add(line.party) accounts.add(line.account) second_currencies.add(line.second_currency) try: company, = companies except ValueError: raise GroupLineError( gettext('account.msg_group_line_same_company')) try: party, = parties except ValueError: raise GroupLineError( gettext('account.msg_group_line_many_parties')) try: second_currency, = second_currencies except ValueError: raise GroupLineError( gettext('account.msg_group_line_same_second_currency')) if len(accounts) > 2: raise GroupLineError( gettext('account.msg_group_line_maximum_account')) return { 'company': company, 'party': party, 'second_currency': second_currency, 'accounts': accounts, } @classmethod def allow_grouping(cls, account): return account.type.payable or account.type.receivable @classmethod def get_move(cls, lines, grouping, journal, date=None): pool = Pool() Date = pool.get('ir.date') Move = pool.get('account.move') Period = pool.get('account.period') Line = pool.get('account.move.line') logger.info("GETMOVEGROUPLINES:%s",lines) company = grouping['company'] if not date: with Transaction().set_context(company=company.id): date = Date.today() period = Period.find(company, date=date) move = Move() move.company = company move.date = date move.period = period move.journal = journal accounts = {a: 0 for a in grouping['accounts']} amount_second_currency = 0 maturity_dates = {a: None for a in grouping['accounts']} counterpart_lines = [] for line in lines: if maturity_dates[line.account] and line.maturity_date: maturity_dates[line.account] = min( maturity_dates[line.account], line.maturity_date) elif line.maturity_date: maturity_dates[line.account] = line.maturity_date cline = cls._counterpart_line(line) accounts[cline.account] += cline.debit - cline.credit if cline.amount_second_currency: amount_second_currency += cline.amount_second_currency counterpart_lines.append(cline) move.lines = counterpart_lines balance_line = None if len(accounts) == 1: account, = grouping['accounts'] amount = -accounts[account] if amount: balance_line = Line() balance_line.account = account else: first, second = grouping['accounts'] if accounts[first] != accounts[second]: balance_line = Line() amount = -(accounts[first] + accounts[second]) if abs(accounts[first]) > abs(accounts[second]): balance_line.account = first else: balance_line.account = second if balance_line: if balance_line.account.party_required: balance_line.party = grouping['party'] if amount > 0: balance_line.debit, balance_line.credit = amount, 0 else: balance_line.debit, balance_line.credit = 0, -amount logger.info("GETMOVEBALANCE:%s",-amount_second_currency) if grouping['second_currency']: balance_line.second_currency = grouping['second_currency'] balance_line.amount_second_currency = ( -amount_second_currency) balance_line.maturity_date = maturity_dates[balance_line.account] return move, balance_line @classmethod def _counterpart_line(cls, line): pool = Pool() Line = pool.get('account.move.line') counterpart = Line() counterpart.account = line.account counterpart.debit = line.credit counterpart.credit = line.debit counterpart.party = line.party counterpart.second_currency = line.second_currency logger.info("_COUNTERPARTY:%s",line.second_currency) if line.second_currency: counterpart.amount_second_currency = -line.amount_second_currency else: counterpart.amount_second_currency = None return counterpart class GroupLinesStart(ModelView): "Group Lines" __name__ = 'account.move.line.group.start' journal = fields.Many2One('account.journal', "Journal", required=True) description = fields.Char("Description") class RescheduleLines(Wizard): "Reschedule Lines" __name__ = 'account.move.line.reschedule' start = StateView('account.move.line.reschedule.start', 'account.move_line_reschedule_start_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Preview", 'preview', 'tryton-ok', validate=False, default=True), ]) preview = StateView('account.move.line.reschedule.preview', 'account.move_line_reschedule_preview_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Reschedule", 'reschedule', 'tryton-ok', default=True), ]) reschedule = StateAction('account.act_move_form_rescheduling') def get_origin(self): try: origin, = {r.move.origin for r in self.records} except ValueError: raise RescheduleLineError( gettext('account.msg_reschedule_line_same_origins')) return origin @classmethod def get_currency(cls, lines): try: currency, = {l.amount_currency for l in lines} except ValueError: raise RescheduleLineError( gettext('account.msg_reschedule_line_same_currency')) return currency @classmethod def get_account(cls, lines): try: account, = {l.account for l in lines} except ValueError: raise RescheduleLineError( gettext('account.msg_reschedule_line_same_account')) return account @classmethod def get_party(cls, lines): try: party, = {l.party for l in lines} except ValueError: raise RescheduleLineError( gettext('account.msg_reschedule_line_same_party')) return party @classmethod def get_total_amount(cls, lines): return sum(l.amount for l in lines) @classmethod def get_balance(cls, lines): return sum(l.debit - l.credit for l in lines) def default_start(self, fields): values = {} self.get_origin() self.get_account(self.records) currency = self.get_currency(self.records) values['currency'] = currency.id values['total_amount'] = self.get_total_amount(self.records) return values def default_preview(self, fields): values = {} currency = self.get_currency(self.records) try: journal, = {r.move.journal for r in self.records} values['journal'] = journal.id except ValueError: pass values['currency'] = currency.id if (self.start.start_date and self.start.interval and self.start.currency): remaining = self.start.total_amount date = self.start.start_date values['terms'] = terms = [] if self.start.amount: interval = self.start.interval amount = self.start.amount.copy_sign(remaining) while remaining - amount > 0: terms.append({ 'date': date, 'amount': amount, 'currency': currency.id, }) date = ( self.start.start_date + relativedelta(months=interval)) interval += self.start.interval remaining -= amount if remaining: terms.append({ 'date': date, 'amount': remaining, 'currency': currency.id, }) elif self.start.number: amount = self.start.currency.round( self.start.total_amount / self.start.number) for i in range(self.start.number): terms.append({ 'date': date + relativedelta(months=i), 'amount': amount, 'currency': currency.id, }) remaining -= amount if remaining: terms[-1]['amount'] += remaining return values def do_reschedule(self, action): move, balance_line = self.reschedule_lines( self.records, self.preview.journal, self.preview.terms) move.origin = self.get_origin() move.description = self.preview.description move.save() if all(l.move.state == 'posted' for l in self.records): move.post() action['res_id'] = [move.id] return action, {} @classmethod def _line_values(cls, lines): account = cls.get_account(lines) currency = cls.get_currency(lines) if currency == account.currency: currency = None return { 'account': account, 'second_currency': currency, 'party': cls.get_party(lines), } @classmethod def reschedule_lines(cls, lines, journal, terms): pool = Pool() Lang = pool.get('ir.lang') Line = pool.get('account.move.line') total_amount = cls.get_total_amount(lines) amount = sum(t.amount for t in terms) if amount != total_amount: lang = Lang.get() currency = cls.get_currency(lines) raise RescheduleLineError( gettext('account.msg_reschedule_line_wrong_amount', total_amount=lang.currency(total_amount, currency), amount=lang.currency(amount, currency))) balance = cls.get_balance(lines) line_values = cls._line_values(lines) account = line_values['account'] move, balance_line = cls.get_reschedule_move( amount, balance, journal, terms, **line_values) move.save() balance_line.move = move balance_line.save() if account.reconcile: Line.reconcile(lines + [balance_line]) return move, balance_line @classmethod def get_reschedule_move( cls, amount, balance, journal, terms, account, date=None, **line_values): pool = Pool() Date = pool.get('ir.date') Line = pool.get('account.move.line') Move = pool.get('account.move') Period = pool.get('account.period') company = account.company if not date: with Transaction().set_context(company=company.id): date = Date.today() period = Period.find(company, date=date) move = Move() move.company = company move.date = date move.period = period move.journal = journal balance_line = Line(account=account, **line_values) if balance >= 0: balance_line.debit, balance_line.credit = 0, balance else: balance_line.debit, balance_line.credit = -balance, 0 if balance_line.second_currency: balance_line.amount_second_currency = -amount remaining_balance = balance remaining_amount = amount lines = [] for term in terms: line = Line(account=account, **line_values) line.maturity_date = term.date factor = term.amount / amount line_amount = account.currency.round(balance * factor) if balance >= 0: line.debit, line.credit = line_amount, 0 else: line.debit, line.credit = 0, -line_amount remaining_balance -= line_amount if line.second_currency: line_amount_second_currency = line.second_currency.round( amount * factor) line.amount_second_currency = line_amount_second_currency remaining_amount -= line_amount_second_currency lines.append(line) if remaining_balance: if line.debit: line.debit += remaining_balance else: line.credit += remaining_balance if remaining_amount and line.second_currency: line.amount_second_currency += remaining_amount move.lines = lines return move, balance_line class RescheduleLinesStart(ModelView): "Reschedule Lines" __name__ = 'account.move.line.reschedule.start' start_date = fields.Date("Start Date", required=True) frequency = fields.Selection([ ('monthly', "Monthly"), ('quarterly', "Quarterly"), ('other', "Other"), ], "Frequency", sort=False, required=True) interval = fields.Integer( "Interval", required=True, states={ 'invisible': Eval('frequency') != 'other', }, help="The length of each period, in months.") amount = Monetary( "Amount", currency='currency', digits='currency', states={ 'required': ~Eval('number'), 'invisible': Bool(Eval('number')), }, domain=[If(Eval('amount'), If(Eval('total_amount', 0) > 0, [ ('amount', '<=', Eval('total_amount', 0)), ('amount', '>', 0), ], [ ('amount', '>=', Eval('total_amount', 0)), ('amount', '<', 0), ]), [])]) number = fields.Integer( "Number", domain=[ ('number', '>', 0), ], states={ 'required': ~Eval('amount'), 'invisible': Bool(Eval('amount')), }) total_amount = fields.Numeric("Total Amount", readonly=True) currency = fields.Many2One('currency.currency', "Currency", readonly=True) @classmethod def default_frequency(cls): return 'monthly' @classmethod def frequency_intervals(cls): return { 'monthly': 1, 'quarterly': 3, 'other': None, } @fields.depends('frequency', 'interval') def on_change_frequency(self): if self.frequency: self.interval = self.frequency_intervals()[self.frequency] class RescheduleLinesPreview(ModelView): "Reschedule Lines" __name__ = 'account.move.line.reschedule.preview' journal = fields.Many2One('account.journal', "Journal", required=True) description = fields.Char("Description") terms = fields.One2Many( 'account.move.line.reschedule.term', None, "Terms", domain=[ ('currency', '=', Eval('currency', -1)), ]) currency = fields.Many2One('currency.currency', "Currency", readonly=True) class RescheduleLinesTerm(ModelView): "Reschedule Lines" __name__ = 'account.move.line.reschedule.term' date = fields.Date("Date", required=True) amount = Monetary( "Amount", currency='currency', digits='currency', required=True) currency = fields.Many2One('currency.currency', "Currency", required=True) class DelegateLines(Wizard): "Delegate Lines" __name__ = 'account.move.line.delegate' start = StateView( 'account.move.line.delegate.start', 'account.move_line_delegate_start_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Delegate", 'delegate', 'tryton-ok', default=True), ]) delegate = StateAction('account.act_move_form_delegate') def default_start(self, fields): values = {} if 'journal' in fields: journals = {l.journal for l in self.records} if len(journals) == 1: journal, = journals values['journal'] = journal.id return values def do_delegate(self, action): move = self._delegate_lines(self.records, self.start.party) if all(l.move.state == 'posted' for l in self.records): move.post() return action, {'res_id': move.id} def _delegate_lines(self, lines, party, date=None): move = self.delegate_lines(lines, party, self.start.journal, date) move.description = self.start.description move.save() return move @classmethod def delegate_lines(cls, lines, party, journal, date=None): pool = Pool() Line = pool.get('account.move.line') move, counterpart, delegated = cls.get_move( lines, party, journal, date=date) move.save() Line.save(counterpart + delegated) for line, cline, dline in zip(lines, counterpart, delegated): Line.reconcile([line, cline], delegate_to=dline) return move @classmethod def get_move(cls, lines, party, journal, date=None): pool = Pool() Date = pool.get('ir.date') Move = pool.get('account.move') Period = pool.get('account.period') try: company, = {l.company for l in lines} except ValueError: raise DelegateLineError( gettext('account.msg_delegate_line_same_company')) try: origin, = {l.move.origin for l in lines} except ValueError: raise DelegateLineError( gettext('account.msg_delegate_line_same_origins')) if not date: with Transaction().set_context(company=company.id): date = Date.today() period = Period.find(company, date=date) move = Move() move.company = company move.date = date move.period = period move.journal = journal move.origin = origin counterpart = [] delegated = [] for line in lines: cline = cls.get_move_line(line) cline.move = move cline.debit, cline.credit = line.credit, line.debit if cline.amount_second_currency: cline.amount_second_currency *= -1 counterpart.append(cline) dline = cls.get_move_line(line) dline.move = move dline.party = party delegated.append(dline) return move, counterpart, delegated @classmethod def get_move_line(cls, line): pool = Pool() Line = pool.get('account.move.line') new = Line() new.debit = line.debit new.credit = line.credit new.account = line.account new.origin = line new.description = line.description logger.info("DELEGATE:%s",line) new.amount_second_currency = line.amount_second_currency new.second_currency = line.second_currency new.party = line.party new.maturity_date = line.maturity_date return new class DelegateLinesStart(ModelView): "Delegate Lines" __name__ = 'account.move.line.delegate.start' journal = fields.Many2One('account.journal', "Journal", required=True) party = fields.Many2One('party.party', "Party", required=True) description = fields.Char("Description") class GeneralJournal(Report): __name__ = 'account.move.general_journal' @classmethod def get_context(cls, records, header, data): pool = Pool() Company = pool.get('company.company') context = Transaction().context report_context = super().get_context(records, header, data) report_context['company'] = Company(context['company']) return report_context