diff --git a/CHANGELOG b/CHANGELOG index 68df9a59d6..5a4ebc30fc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* New reports design: General Ledger, Trial Balance and Aged Balance * Add Python3 support * Merge Third Party Balance into Aged Balance * Add date to Aged Balance diff --git a/__init__.py b/__init__.py index 2a77933b59..4a3f377dd6 100644 --- a/__init__.py +++ b/__init__.py @@ -24,16 +24,19 @@ def register(): Account, AccountDeferral, OpenChartAccountStart, - PrintGeneralLedgerStart, - PrintTrialBalanceStart, - OpenBalanceSheetStart, - OpenIncomeStatementStart, + GeneralLedgerAccount, + GeneralLedgerAccountContext, + GeneralLedgerLine, + GeneralLedgerLineContext, + BalanceSheetContext, + IncomeStatementContext, CreateChartStart, CreateChartAccount, CreateChartProperties, UpdateChartStart, UpdateChartSucceed, - OpenAgedBalanceStart, + AgedBalanceContext, + AgedBalance, Configuration, Period, JournalType, @@ -80,13 +83,8 @@ def register(): BalanceNonDeferral, CloseFiscalYear, OpenChartAccount, - PrintGeneralLedger, - PrintTrialBalance, - OpenBalanceSheet, - OpenIncomeStatement, CreateChart, UpdateChart, - OpenAgedBalance, ClosePeriod, ReOpenPeriod, OpenJournalCash, @@ -106,6 +104,6 @@ def register(): Pool.register( GeneralLedger, TrialBalance, - AgedBalance, + AgedBalanceReport, GeneralJournal, module='account', type_='report') diff --git a/account.py b/account.py index e9d0bc40de..347762fc0f 100644 --- a/account.py +++ b/account.py @@ -2,33 +2,31 @@ # this repository contains the full copyright notices and license terms. from decimal import Decimal import datetime -import operator -from itertools import izip, groupby from functools import wraps -from sql import Column, Null -from sql.aggregate import Sum +from sql import Column, Null, Window, Literal +from sql.aggregate import Sum, Max from sql.conditionals import Coalesce, Case from trytond.model import ModelView, ModelSQL, fields, Unique from trytond.wizard import Wizard, StateView, StateAction, StateTransition, \ - StateReport, Button + Button from trytond.report import Report from trytond.tools import reduce_ids, grouped_slice -from trytond.pyson import Eval, PYSONEncoder, Date +from trytond.pyson import Eval, PYSONEncoder from trytond.transaction import Transaction from trytond.pool import Pool from trytond import backend __all__ = ['TypeTemplate', 'Type', 'OpenType', 'AccountTemplate', 'Account', 'AccountDeferral', 'OpenChartAccountStart', 'OpenChartAccount', - 'PrintGeneralLedgerStart', 'PrintGeneralLedger', 'GeneralLedger', - 'PrintTrialBalanceStart', 'PrintTrialBalance', 'TrialBalance', - 'OpenBalanceSheetStart', 'OpenBalanceSheet', - 'OpenIncomeStatementStart', 'OpenIncomeStatement', + 'GeneralLedgerAccount', 'GeneralLedgerAccountContext', + 'GeneralLedgerLine', 'GeneralLedgerLineContext', + 'GeneralLedger', 'TrialBalance', + 'BalanceSheetContext', 'IncomeStatementContext', + 'AgedBalanceContext', 'AgedBalance', 'AgedBalanceReport', 'CreateChartStart', 'CreateChartAccount', 'CreateChartProperties', - 'CreateChart', 'UpdateChartStart', 'UpdateChartSucceed', 'UpdateChart', - 'OpenAgedBalanceStart', 'OpenAgedBalance', 'AgedBalance'] + 'CreateChart', 'UpdateChartStart', 'UpdateChartSucceed', 'UpdateChart'] def inactive_records(func): @@ -1000,9 +998,129 @@ class OpenChartAccount(Wizard): return 'end' -class PrintGeneralLedgerStart(ModelView): - 'Print General Ledger' - __name__ = 'account.print_general_ledger.start' +class GeneralLedgerAccount(ModelSQL, ModelView): + 'General Ledger Account' + __name__ = 'account.general_ledger.account' + + # TODO reuse rec_name of Account + name = fields.Char('Name') + code = fields.Char('Code') + active = fields.Boolean('Active') + company = fields.Many2One('company.company', 'Company') + start_debit = fields.Function(fields.Numeric('Start Debit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + debit = fields.Function(fields.Numeric('Debit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_debit_credit') + end_debit = fields.Function(fields.Numeric('End Debit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + start_credit = fields.Function(fields.Numeric('Start Credit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + credit = fields.Function(fields.Numeric('Credit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_debit_credit') + end_credit = fields.Function(fields.Numeric('End Credit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + start_balance = fields.Function(fields.Numeric('Start Balance', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + end_balance = fields.Function(fields.Numeric('End Balance', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_account') + lines = fields.One2Many('account.general_ledger.line', 'account', 'Lines', + readonly=True) + general_ledger_balance = fields.Boolean('General Ledger Balance') + currency_digits = fields.Function(fields.Integer('Currency Digits'), + 'get_currency_digits') + + @classmethod + def __setup__(cls): + super(GeneralLedgerAccount, cls).__setup__() + cls._order.insert(0, ('code', 'ASC')) + cls._order.insert(1, ('name', 'ASC')) + + @classmethod + def table_query(cls): + pool = Pool() + context = Transaction().context + Account = pool.get('account.account') + account = Account.__table__() + columns = [] + for fname, field in cls._fields.iteritems(): + if not hasattr(field, 'set'): + columns.append(Column(account, fname).as_(fname)) + return account.select(*columns, + where=(account.company == context.get('company')) + & (account.kind != 'view')) + + @classmethod + def get_period_ids(cls, name): + pool = Pool() + Period = pool.get('account.period') + context = Transaction().context + + period = None + if name.startswith('start_'): + period_ids = [0] + if context.get('start_period'): + period = Period(context['start_period']) + elif name.startswith('end_'): + period_ids = [] + if context.get('end_period'): + period = Period(context['end_period']) + + if period: + periods = Period.search([ + ('fiscalyear', '=', context.get('fiscalyear')), + ('end_date', '<=', period.start_date), + ]) + if periods: + period_ids = [p.id for p in periods] + return period_ids + + @classmethod + def get_account(cls, records, name): + pool = Pool() + Account = pool.get('account.account') + + period_ids = cls.get_period_ids(name) + with Transaction().set_context(periods=period_ids): + accounts = Account.browse(records) + fname = name + for test in ['start_', 'end_']: + if name.startswith(test): + fname = name[len(test):] + break + return {a.id: getattr(a, fname) for a in accounts} + + @classmethod + def get_debit_credit(cls, records, name): + pool = Pool() + Account = pool.get('account.account') + + start_period_ids = cls.get_period_ids('start_%s' % name) + end_period_ids = cls.get_period_ids('end_%s' % name) + periods_ids = list( + set(end_period_ids).difference(set(start_period_ids))) + with Transaction().set_context(periods=periods_ids): + accounts = Account.browse(records) + return {a.id: getattr(a, name) for a in accounts} + + def get_currency_digits(self, name): + return self.company.currency.digits + + def get_rec_name(self, name): + # TODO add start/end balance + return self.name + + +class GeneralLedgerAccountContext(ModelView): + 'General Ledger Account Context' + __name__ = 'account.general_ledger.account.context' fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', required=True) start_period = fields.Many2One('account.period', 'Start Period', @@ -1018,64 +1136,145 @@ class PrintGeneralLedgerStart(ModelView): depends=['fiscalyear', 'start_period']) company = fields.Many2One('company.company', 'Company', required=True) posted = fields.Boolean('Posted Move', help='Show only posted move') - empty_account = fields.Boolean('Empty Account', - help='With account without move') - @staticmethod - def default_fiscalyear(): - FiscalYear = Pool().get('account.fiscalyear') - return FiscalYear.find( - Transaction().context.get('company'), exception=False) + @classmethod + def default_fiscalyear(cls): + pool = Pool() + FiscalYear = pool.get('account.fiscalyear') + context = Transaction().context + return context.get( + 'fiscalyear', + FiscalYear.find(context.get('company'), exception=False)) - @staticmethod - def default_company(): + @classmethod + def default_start_period(cls): + return Transaction().context.get('start_period') + + @classmethod + def default_end_period(cls): + return Transaction().context.get('end_period') + + @classmethod + def default_company(cls): return Transaction().context.get('company') - @staticmethod - def default_posted(): - return False + @classmethod + def default_posted(cls): + return Transaction().context.get('posted', False) - @staticmethod - def default_empty_account(): - return False - - @fields.depends('fiscalyear') + @fields.depends('fiscalyear', 'start_period', 'end_period') def on_change_fiscalyear(self): - self.start_period = None - self.end_period = None + if (self.start_period + and self.start_period.fiscalyear != self.fiscalyear): + self.start_period = None + if (self.end_period + and self.end_period.fiscalyear != self.fiscalyear): + self.end_period = None -class PrintGeneralLedger(Wizard): - 'Print General Ledger' - __name__ = 'account.print_general_ledger' - start = StateView('account.print_general_ledger.start', - 'account.print_general_ledger_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Print', 'print_', 'tryton-print', default=True), - ]) - print_ = StateReport('account.general_ledger') +class GeneralLedgerLine(ModelSQL, ModelView): + 'General Ledger Line' + __name__ = 'account.general_ledger.line' - def do_print_(self, action): - if self.start.start_period: - start_period = self.start.start_period.id - else: - start_period = None - if self.start.end_period: - end_period = self.start.end_period.id - else: - end_period = None - data = { - 'company': self.start.company.id, - 'fiscalyear': self.start.fiscalyear.id, - 'start_period': start_period, - 'end_period': end_period, - 'posted': self.start.posted, - 'empty_account': self.start.empty_account, - } - return action, data + move = fields.Many2One('account.move', 'Move') + date = fields.Date('Date') + account = fields.Many2One('account.general_ledger.account', 'Account') + party = fields.Many2One('party.party', 'Party', + states={ + 'invisible': ~Eval('party_required', False), + }, + depends=['party_required']) + party_required = fields.Function(fields.Boolean('Party Required'), + 'get_party_required') + company = fields.Many2One('company.company', 'Company') + debit = fields.Numeric('Debit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + credit = fields.Numeric('Credit', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + balance = fields.Numeric('Balance', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + origin = fields.Reference('Origin', selection='get_origin') + description = fields.Char('Description') + move_description = fields.Char('Move Description') + state = fields.Selection([ + ('draft', 'Draft'), + ('posted', 'Posted'), + ], 'State') + state_string = state.translated('state') + currency_digits = fields.Function(fields.Integer('Currency Digits'), + 'get_currency_digits') - def transition_print_(self): - return 'end' + @classmethod + def __setup__(cls): + super(GeneralLedgerLine, cls).__setup__() + cls._order.insert(0, ('date', 'ASC')) + + @classmethod + def table_query(cls): + pool = Pool() + Line = pool.get('account.move.line') + Move = pool.get('account.move') + Account = pool.get('account.general_ledger.account') + context = Transaction().context + line = Line.__table__() + move = Move.__table__() + columns = [] + for fname, field in cls._fields.iteritems(): + if hasattr(field, 'set'): + continue + field_line = getattr(Line, fname, None) + if fname == 'balance': + # TODO replace the test by a generic one on backend + if backend.name() == 'postgresql': + w_columns = [line.account] + if context.get('party_cumulate', False): + w_columns.append(line.party) + column = Sum(line.debit - line.credit, + window=Window(w_columns, + order_by=[move.date.asc, line.id])).as_('balance') + else: + column = (line.debit - line.credit).as_('balance') + elif fname == 'move_description': + column = Column(move, 'description').as_(fname) + elif (not field_line + or fname == 'state' + or isinstance(field_line, fields.Function)): + column = Column(move, fname).as_(fname) + else: + column = Column(line, fname).as_(fname) + columns.append(column) + start_period_ids = set(Account.get_period_ids('start_balance')) + end_period_ids = set(Account.get_period_ids('end_balance')) + period_ids = list(end_period_ids.difference(start_period_ids)) + with Transaction().set_context(periods=period_ids): + line_query, fiscalyear_ids = Line.query_get(line) + return line.join(move, condition=line.move == move.id + ).select(*columns, where=line_query) + + def get_party_required(self, name): + return self.account.party_required + + def get_currency_digits(self, name): + return self.company.currency.digits + + @classmethod + def get_origin(cls): + Line = Pool().get('account.move.line') + return Line.get_origin() + + +class GeneralLedgerLineContext(GeneralLedgerAccountContext): + 'General Ledger Line Context' + __name__ = 'account.general_ledger.line.context' + + party_cumulate = fields.Boolean('Cumulate per Party') + + @classmethod + def default_party_cumulate(cls): + return False class GeneralLedger(Report): @@ -1083,326 +1282,61 @@ class GeneralLedger(Report): @classmethod def get_context(cls, records, data): + pool = Pool() + Company = pool.get('company.company') + Fiscalyear = pool.get('account.fiscalyear') + Period = pool.get('account.period') + context = Transaction().context + report_context = super(GeneralLedger, cls).get_context(records, data) - pool = Pool() - Account = pool.get('account.account') - Period = pool.get('account.period') - Company = pool.get('company.company') + report_context['company'] = Company(context['company']) + report_context['fiscalyear'] = Fiscalyear(context['fiscalyear']) - company = Company(data['company']) - - accounts = Account.search([ - ('company', '=', data['company']), - ('kind', '!=', 'view'), - ], order=[('code', 'ASC'), ('id', 'ASC')]) - - start_period_ids = [0] - start_periods = [] - if data['start_period']: - start_period = Period(data['start_period']) - start_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ('end_date', '<=', start_period.start_date), - ]) - start_period_ids += [p.id for p in start_periods] - else: - start_period = None - - with Transaction().set_context( - fiscalyear=data['fiscalyear'], - periods=start_period_ids, - posted=data['posted']): - start_accounts = Account.browse(accounts) - id2start_account = {} - for account in start_accounts: - id2start_account[account.id] = account - - end_period_ids = [] - if data['end_period']: - end_period = Period(data['end_period']) - end_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ('end_date', '<=', end_period.start_date), - ]) - if end_period not in end_periods: - end_periods.append(end_period) - else: - end_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ]) - end_period = None - end_period_ids = [p.id for p in end_periods] - - with Transaction().set_context( - fiscalyear=data['fiscalyear'], - periods=end_period_ids, - posted=data['posted']): - end_accounts = Account.browse(accounts) - id2end_account = {} - for account in end_accounts: - id2end_account[account.id] = account - - if not data['empty_account']: - account2lines = dict(cls.get_lines(accounts, - end_periods, data['posted'])) - accounts = Account.browse([a.id for a in accounts - if a in account2lines]) - - account_id2lines = cls.lines( - [a for a in accounts if not a.general_ledger_balance], - list(set(end_periods).difference(set(start_periods))), - data['posted']) - - report_context['accounts'] = accounts - report_context['id2start_account'] = id2start_account - report_context['id2end_account'] = id2end_account - report_context['digits'] = company.currency.digits - report_context['lines'] = \ - lambda account_id: account_id2lines[account_id] - report_context['company'] = company - report_context['start_period'] = start_period - report_context['end_period'] = end_period + for period in ['start_period', 'end_period']: + if context.get(period): + report_context[period] = Period(context[period]) + else: + report_context[period] = None + report_context['accounts'] = records return report_context - @classmethod - def get_lines(cls, accounts, periods, posted): - MoveLine = Pool().get('account.move.line') - clause = [ - ('account', 'in', [a.id for a in accounts]), - ('period', 'in', [p.id for p in periods]), - ('state', '!=', 'draft'), - ] - if posted: - clause.append(('move.state', '=', 'posted')) - lines = MoveLine.search(clause, - order=[ - ('account.id', 'ASC'), - ('date', 'ASC'), - ]) - return groupby(lines, operator.attrgetter('account')) - - @classmethod - def lines(cls, accounts, periods, posted): - Move = Pool().get('account.move') - res = dict((a.id, []) for a in accounts) - account2lines = cls.get_lines(accounts, periods, posted) - - state_selections = dict(Move.fields_get( - fields_names=['state'])['state']['selection']) - - for account, lines in account2lines: - balance = Decimal('0.0') - for line in lines: - balance += line.debit - line.credit - res[account.id].append({ - 'date': line.date, - 'move': line.move.rec_name, - 'debit': line.debit, - 'credit': line.credit, - 'balance': balance, - 'description': '\n'.join( - (line.move.description or '', - line.description or '')).strip(), - 'origin': (line.move.origin.rec_name - if line.move.origin else ''), - 'state': state_selections.get(line.move.state, - line.move.state), - }) - return res - - -class PrintTrialBalanceStart(ModelView): - 'Print Trial Balance' - __name__ = 'account.print_trial_balance.start' - fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', - required=True) - start_period = fields.Many2One('account.period', 'Start Period', - domain=[ - ('fiscalyear', '=', Eval('fiscalyear')), - ('start_date', '<=', (Eval('end_period'), 'start_date')) - ], - depends=['end_period', 'fiscalyear']) - end_period = fields.Many2One('account.period', 'End Period', - domain=[ - ('fiscalyear', '=', Eval('fiscalyear')), - ('start_date', '>=', (Eval('start_period'), 'start_date')) - ], - depends=['start_period', 'fiscalyear']) - company = fields.Many2One('company.company', 'Company', required=True) - posted = fields.Boolean('Posted Move', help='Show only posted move') - empty_account = fields.Boolean('Empty Account', - help='With account without move') - - @staticmethod - def default_fiscalyear(): - FiscalYear = Pool().get('account.fiscalyear') - return FiscalYear.find( - Transaction().context.get('company'), exception=False) - - @staticmethod - def default_company(): - return Transaction().context.get('company') - - @staticmethod - def default_posted(): - return False - - @staticmethod - def default_empty_account(): - return False - - @fields.depends('fiscalyear') - def on_change_fiscalyear(self): - self.start_period = None - self.end_period = None - - -class PrintTrialBalance(Wizard): - 'Print Trial Balance' - __name__ = 'account.print_trial_balance' - start = StateView('account.print_trial_balance.start', - 'account.print_trial_balance_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Print', 'print_', 'tryton-print', default=True), - ]) - print_ = StateReport('account.trial_balance') - - def do_print_(self, action): - if self.start.start_period: - start_period = self.start.start_period.id - else: - start_period = None - if self.start.end_period: - end_period = self.start.end_period.id - else: - end_period = None - data = { - 'company': self.start.company.id, - 'fiscalyear': self.start.fiscalyear.id, - 'start_period': start_period, - 'end_period': end_period, - 'posted': self.start.posted, - 'empty_account': self.start.empty_account, - } - return action, data - - def transition_print_(self): - return 'end' - class TrialBalance(Report): __name__ = 'account.trial_balance' @classmethod def get_context(cls, records, data): + pool = Pool() + Company = pool.get('company.company') + Fiscalyear = pool.get('account.fiscalyear') + Period = pool.get('account.period') + context = Transaction().context + report_context = super(TrialBalance, cls).get_context(records, data) - pool = Pool() - Account = pool.get('account.account') - Period = pool.get('account.period') - Company = pool.get('company.company') + report_context['company'] = Company(context['company']) + report_context['fiscalyear'] = Fiscalyear(context['fiscalyear']) - company = Company(data['company']) - - accounts = Account.search([ - ('company', '=', data['company']), - ('kind', '!=', 'view'), - ]) - - start_periods = [] - if data['start_period']: - start_period = Period(data['start_period']) - start_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ('end_date', '<=', start_period.start_date), - ]) - - if data['end_period']: - end_period = Period(data['end_period']) - end_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ('end_date', '<=', end_period.start_date), - ]) - end_periods = list(set(end_periods).difference( - set(start_periods))) - if end_period not in end_periods: - end_periods.append(end_period) - else: - end_periods = Period.search([ - ('fiscalyear', '=', data['fiscalyear']), - ]) - end_periods = list(set(end_periods).difference( - set(start_periods))) - - start_period_ids = [p.id for p in start_periods] or [0] - end_period_ids = [p.id for p in end_periods] - - with Transaction().set_context( - fiscalyear=data['fiscalyear'], - periods=start_period_ids, - posted=data['posted']): - start_accounts = Account.browse(accounts) - - with Transaction().set_context( - fiscalyear=None, - periods=end_period_ids, - posted=data['posted']): - in_accounts = Account.browse(accounts) - - with Transaction().set_context( - fiscalyear=data['fiscalyear'], - periods=start_period_ids + end_period_ids, - posted=data['posted']): - end_accounts = Account.browse(accounts) - - to_remove = set() - if not data['empty_account']: - for account in in_accounts: - if account.debit == Decimal('0.0') \ - and account.credit == Decimal('0.0'): - to_remove.add(account) - - accounts = [] - for start_account, in_account, end_account in izip( - start_accounts, in_accounts, end_accounts): - if in_account in to_remove: - continue - accounts.append({ - 'code': start_account.code, - 'name': start_account.name, - 'start_balance': start_account.balance, - 'debit': in_account.debit, - 'credit': in_account.credit, - 'end_balance': end_account.balance, - }) - - periods = end_periods - - report_context['accounts'] = accounts - periods.sort(key=operator.attrgetter('start_date')) - report_context['start_period'] = periods[0] - periods.sort(key=operator.attrgetter('end_date')) - report_context['end_period'] = periods[-1] - report_context['company'] = company - report_context['digits'] = company.currency.digits - report_context['sum'] = \ - lambda accounts, field: cls.sum(accounts, field) + for period in ['start_period', 'end_period']: + if context.get(period): + report_context[period] = Period(context[period]) + else: + report_context[period] = None + report_context['accounts'] = records + report_context['sum'] = cls.sum return report_context @classmethod def sum(cls, accounts, field): - amount = Decimal('0.0') - for account in accounts: - amount += account[field] - return amount + return sum((getattr(a, field) for a in accounts), Decimal('0')) -class OpenBalanceSheetStart(ModelView): - 'Open Balance Sheet' - __name__ = 'account.open_balance_sheet.start' +class BalanceSheetContext(ModelView): + 'Balance Sheet Context' + __name__ = 'account.balance_sheet.context' date = fields.Date('Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) posted = fields.Boolean('Posted Move', help='Show only posted move') @@ -1421,45 +1355,9 @@ class OpenBalanceSheetStart(ModelView): return False -class OpenBalanceSheet(Wizard): - 'Open Balance Sheet' - __name__ = 'account.open_balance_sheet' - start = StateView('account.open_balance_sheet.start', - 'account.open_balance_sheet_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Open', 'open_', 'tryton-ok', default=True), - ]) - open_ = StateAction('account.act_account_balance_sheet_tree') - - def do_open_(self, action): - pool = Pool() - Lang = pool.get('ir.lang') - - company = self.start.company - lang, = Lang.search([ - ('code', '=', Transaction().language), - ]) - - date = Lang.strftime(self.start.date, lang.code, lang.date) - - action['pyson_context'] = PYSONEncoder().encode({ - 'date': Date(self.start.date.year, - self.start.date.month, - self.start.date.day), - 'posted': self.start.posted, - 'company': company.id, - 'cumulate': True, - }) - action['name'] += ' %s - %s' % (date, company.rec_name) - return action, {} - - def transition_open_(self): - return 'end' - - -class OpenIncomeStatementStart(ModelView): - 'Open Income Statement' - __name__ = 'account.open_income_statement.start' +class IncomeStatementContext(ModelView): + 'Income Statement Context' + __name__ = 'account.income_statement.context' fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', required=True) start_period = fields.Many2One('account.period', 'Start Period', @@ -1497,50 +1395,224 @@ class OpenIncomeStatementStart(ModelView): self.end_period = None -class OpenIncomeStatement(Wizard): - 'Open Income Statement' - __name__ = 'account.open_income_statement' - start = StateView('account.open_income_statement.start', - 'account.open_income_statement_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Open', 'open_', 'tryton-ok', default=True), - ]) - open_ = StateAction('account.act_account_income_statement_tree') +class AgedBalanceContext(ModelView): + 'Aged Balance Context' + __name__ = 'account.aged_balance.context' + type = fields.Selection([ + ('customer', 'Customers'), + ('supplier', 'Suppliers'), + ('customer_supplier', 'Customers and Suppliers'), + ], + "Type", required=True) + date = fields.Date('Date', required=True) + term1 = fields.Integer("First Term", required=True) + term2 = fields.Integer("Second Term", required=True, + domain=[ + ('term2', '>', Eval('term1', 0)), + ], + depends=['term1']) + term3 = fields.Integer("Third Term", required=True, + domain=[ + ('term3', '>', Eval('term2', 0)), + ], + depends=['term2']) + unit = fields.Selection([ + ('day', 'Days'), + ('month', 'Months'), + ], "Unit", required=True) + company = fields.Many2One('company.company', 'Company', required=True) + posted = fields.Boolean('Posted Move', help='Show only posted move') - def do_open_(self, action): + @classmethod + def default_type(cls): + return 'customer' + + @classmethod + def default_posted(cls): + return False + + @classmethod + def default_date(cls): + return Pool().get('ir.date').today() + + @staticmethod + def default_term1(): + return 30 + + @staticmethod + def default_term2(): + return 60 + + @staticmethod + def default_term3(): + return 90 + + @staticmethod + def default_unit(): + return 'day' + + @staticmethod + def default_company(): + return Transaction().context.get('company') + + +class AgedBalance(ModelSQL, ModelView): + 'Aged Balance' + __name__ = 'account.aged_balance' + + party = fields.Many2One('party.party', 'Party') + company = fields.Many2One('company.company', 'Company') + term0 = fields.Numeric('Now', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + term1 = fields.Numeric('First Term', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + term2 = fields.Numeric('Second Term', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + term3 = fields.Numeric('Third Term', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + balance = fields.Numeric('Balance', + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + currency_digits = fields.Function(fields.Integer('Currency Digits'), + 'get_currency_digits') + + @classmethod + def __setup__(cls): + super(AgedBalance, cls).__setup__() + cls._order.insert(0, ('party', 'ASC')) + + @classmethod + def table_query(cls): pool = Pool() - Period = pool.get('account.period') + context = Transaction().context + MoveLine = pool.get('account.move.line') + Move = pool.get('account.move') + Reconciliation = pool.get('account.move.reconciliation') + Account = pool.get('account.account') - start_periods = [] - if self.start.start_period: - start_periods = Period.search([ - ('fiscalyear', '=', self.start.fiscalyear.id), - ('end_date', '<=', self.start.start_period.start_date), - ]) + line = MoveLine.__table__() + move = Move.__table__() + reconciliation = Reconciliation.__table__() + account = Account.__table__() - end_periods = [] - if self.start.end_period: - end_periods = Period.search([ - ('fiscalyear', '=', self.start.fiscalyear.id), - ('end_date', '<=', self.start.end_period.start_date), - ]) - end_periods = list(set(end_periods).difference( - set(start_periods))) - if self.start.end_period not in end_periods: - end_periods.append(self.start.end_period) + company_id = context.get('company') + date = context.get('date') + with Transaction().set_context(date=None): + line_query, _ = MoveLine.query_get(line) + kind = cls.get_kind() + columns = [ + line.party.as_('id'), + Literal(0).as_('create_uid'), + Max(line.create_date).as_('create_date'), + Literal(0).as_('write_uid'), + Max(line.write_date).as_('write_date'), + line.party.as_('party'), + move.company.as_('company'), + (Sum(line.debit) - Sum(line.credit)).as_('balance'), + ] + + terms = cls.get_terms() + factor = cls.get_unit_factor() + term_values = sorted(terms.values()) + + for name, value in terms.iteritems(): + if value is None or factor is None or date is None: + columns.append(Literal(None).as_(name)) + continue + cond = line.maturity_date <= (date - value * factor) + idx = term_values.index(value) + if idx + 1 < len(terms): + cond &= line.maturity_date > ( + date - term_values[idx + 1] * factor) + else: + cond |= line.maturity_date == Null + columns.append( + Sum(Case((cond, line.debit - line.credit), else_=0)).as_(name)) + + return line.join(move, condition=line.move == move.id + ).join(account, condition=line.account == account.id + ).join(reconciliation, 'LEFT', + condition=reconciliation.id == line.reconciliation + ).select(*columns, + where=(line.party != Null) + & (account.active == True) + & account.kind.in_(kind) + & ((line.reconciliation == Null) + | (reconciliation.date > date)) + & (move.date <= date) + & (account.company == company_id) + & line_query, + group_by=(line.party, move.company)) + + @classmethod + def get_terms(cls): + context = Transaction().context + return { + 'term0': 0, + 'term1': context.get('term1'), + 'term2': context.get('term2'), + 'term3': context.get('term3'), + } + + @classmethod + def get_unit_factor(cls): + context = Transaction().context + unit = context.get('unit', 'day') + if unit == 'month': + return datetime.timedelta(days=30) + elif unit == 'day': + return datetime.timedelta(days=1) + + @classmethod + def get_kind(cls): + context = Transaction().context + type_ = context.get('type', 'customer') + if type_ == 'customer_supplier': + return ['payable', 'receivable'] + elif type_ == 'supplier': + return ['payable'] + elif type_ == 'customer': + return ['receivable'] else: - end_periods = Period.search([ - ('fiscalyear', '=', self.start.fiscalyear.id), - ]) - end_periods = list(set(end_periods).difference( - set(start_periods))) + return [] - action['pyson_context'] = PYSONEncoder().encode({ - 'periods': [p.id for p in end_periods], - 'posted': self.start.posted, - 'company': self.start.company.id, - }) - return action, {} + def get_currency_digits(self, name): + return self.company.currency.digits + + +class AgedBalanceReport(Report): + __name__ = 'account.aged_balance' + + @classmethod + def get_context(cls, records, data): + pool = Pool() + Company = pool.get('company.company') + Context = pool.get('account.aged_balance.context') + AgedBalance = pool.get('account.aged_balance') + context = Transaction().context + + report_context = super(AgedBalanceReport, cls).get_context( + records, data) + + context_fields = Context.fields_get(['type', 'unit']) + + report_context['company'] = Company(context['company']) + report_context['date'] = context['date'] + report_context['type'] = dict( + context_fields['type']['selection'])[context['type']] + report_context['unit'] = dict( + context_fields['unit']['selection'])[context['unit']] + report_context.update(AgedBalance.get_terms()) + report_context['sum'] = cls.sum + return report_context + + @classmethod + def sum(cls, records, field): + return sum((getattr(r, field) for r in records), Decimal('0')) class CreateChartStart(ModelView): @@ -1851,194 +1923,3 @@ class UpdateChart(Wizard): tax_rule_line_template.create_rule_line(template2tax, template2rule, template2rule_line=template2rule_line) return 'succeed' - - -class OpenAgedBalanceStart(ModelView): - 'Open Aged Balance' - __name__ = 'account.open_aged_balance.start' - company = fields.Many2One('company.company', 'Company', required=True) - balance_type = fields.Selection( - [('customer', 'Customer'), ('supplier', 'Supplier'), ('both', 'Both')], - "Type", required=True) - date = fields.Date('Date', required=True) - term1 = fields.Integer("First Term", required=True) - term2 = fields.Integer("Second Term", required=True, - domain=[ - ('term2', '>', Eval('term1', 0)), - ], - depends=['term1']) - term3 = fields.Integer("Third Term", required=True, - domain=[ - ('term3', '>', Eval('term2', 0)), - ], - depends=['term2']) - unit = fields.Selection( - [('day', 'Day'), ('month', 'Month')], "Unit", required=True) - posted = fields.Boolean('Posted Move', help='Show only posted move') - - @staticmethod - def default_balance_type(): - return "customer" - - @staticmethod - def default_posted(): - return False - - @classmethod - def default_date(cls): - return Pool().get('ir.date').today() - - @staticmethod - def default_term1(): - return 30 - - @staticmethod - def default_term2(): - return 60 - - @staticmethod - def default_term3(): - return 90 - - @staticmethod - def default_unit(): - return 'day' - - @staticmethod - def default_company(): - return Transaction().context.get('company') - - -class OpenAgedBalance(Wizard): - 'Open Aged Party Balance' - __name__ = 'account.open_aged_balance' - start = StateView('account.open_aged_balance.start', - 'account.open_aged_balance_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Print', 'print_', 'tryton-print', default=True), - ]) - print_ = StateReport('account.aged_balance') - - @classmethod - def __setup__(cls): - super(OpenAgedBalance, cls).__setup__() - cls._error_messages.update({ - 'warning': 'Warning', - 'term_overlap_desc': 'You cannot define overlapping terms', - }) - - def do_print_(self, action): - if not (self.start.term1 < self.start.term2 < self.start.term3): - self.raise_user_error(error="warning", - error_description="term_overlap_desc") - data = { - 'company': self.start.company.id, - 'date': self.start.date, - 'term1': self.start.term1, - 'term2': self.start.term2, - 'term3': self.start.term3, - 'unit': self.start.unit, - 'posted': self.start.posted, - 'balance_type': self.start.balance_type, - } - return action, data - - def transition_print_(self): - return 'end' - - -class AgedBalance(Report): - __name__ = 'account.aged_balance' - - @classmethod - def get_context(cls, records, data): - report_context = super(AgedBalance, cls).get_context(records, data) - - pool = Pool() - Party = pool.get('party.party') - MoveLine = pool.get('account.move.line') - Move = pool.get('account.move') - Reconciliation = pool.get('account.move.reconciliation') - Account = pool.get('account.account') - Company = pool.get('company.company') - cursor = Transaction().connection.cursor() - - line = MoveLine.__table__() - move = Move.__table__() - reconciliation = Reconciliation.__table__() - account = Account.__table__() - - company = Company(data['company']) - report_context['digits'] = company.currency.digits - report_context['posted'] = data['posted'] - with Transaction().set_context(context=report_context): - line_query, _ = MoveLine.query_get(line) - - terms = (0, data['term1'], data['term2'], data['term3']) - if data['unit'] == 'month': - coef = datetime.timedelta(days=30) - else: - coef = datetime.timedelta(days=1) - - kind = { - 'both': ('payable', 'receivable'), - 'supplier': ('payable',), - 'customer': ('receivable',), - }[data['balance_type']] - - report_context['date'] = date = data['date'] - res = {} - for position, term in enumerate(terms): - term_query = line.maturity_date <= (date - term * coef) - if position + 1 < len(terms): - term_query &= line.maturity_date > ( - date - terms[position + 1] * coef) - else: - term_query |= line.maturity_date == Null - - cursor.execute(*line.join(move, condition=line.move == move.id - ).join(account, condition=line.account == account.id - ).join(reconciliation, 'LEFT', - condition=reconciliation.id == line.reconciliation - ).select(line.party, Sum(line.debit) - Sum(line.credit), - where=(line.party != Null) - & (account.active == True) - & account.kind.in_(kind) - & ((line.reconciliation == Null) - | (reconciliation.date > date)) - & (move.date <= date) - & (account.company == data['company']) - & term_query & line_query, - group_by=line.party, - having=(Sum(line.debit) - Sum(line.credit)) != 0)) - for party, solde in cursor.fetchall(): - if party in res: - res[party][position] = solde - else: - res[party] = [(i[0] == position) and solde - or Decimal("0.0") for i in enumerate(terms)] - parties = Party.search([ - ('id', 'in', [k for k in res.iterkeys()]), - ]) - - report_context['main_title'] = data['balance_type'] - report_context['unit'] = data['unit'] - total = 0 - for i in range(len(terms)): - report_context['total' + str(i)] = sum( - (v[i] for v in res.itervalues())) - total += report_context['total' + str(i)] - report_context['term' + str(i)] = terms[i] - report_context['total'] = total - - report_context['company'] = company - report_context['parties'] = [{ - 'name': p.rec_name, - 'balance': sum(res[p.id]), - 'amount0': res[p.id][0], - 'amount1': res[p.id][1], - 'amount2': res[p.id][2], - 'amount3': res[p.id][3], - } for p in parties] - - return report_context diff --git a/account.xml b/account.xml index dff06932d4..d838e22d35 100644 --- a/account.xml +++ b/account.xml @@ -417,50 +417,118 @@ this repository contains the full copyright notices and license terms. --> open_chart_start_form - - account.print_general_ledger.start - form - print_general_legder_start_form - - - - Print General Legder - account.print_general_ledger - - - General Ledger - + account.general_ledger.account account.general_ledger account/general_ledger.odt - - - account.print_trial_balance.start - form - print_trial_balance_start_form + + form_print + account.general_ledger.account,-1 + - - Print Trial Balance - account.print_trial_balance - - - Trial Balance - + account.general_ledger.account account.trial_balance account/trial_balance.odt + + form_print + account.general_ledger.account,-1 + + - - account.open_balance_sheet.start + + account.general_ledger.account + tree + general_ledger_account_list + + + + General Ledger - Accounts + account.general_ledger.account + account.general_ledger.account.context + + + + + + + + + + + + + + + + + + + + + + + + + + account.general_ledger.line + tree + general_ledger_line_list + + + + General Ledger - Lines + account.general_ledger.line + account.general_ledger.line.context + + + + + + + + + tree_open + account.general_ledger.account,-1 + + + + + + + + + + + + + + + + + + + + + + account.general_ledger.account.context form - open_balance_sheet_start_form + general_ledger_account_context_form + + + + account.general_ledger.line.context + + general_ledger_line_context_form @@ -472,9 +540,11 @@ this repository contains the full copyright notices and license terms. --> Balance Sheet account.account.type + account.balance_sheet.context + @@ -482,18 +552,13 @@ this repository contains the full copyright notices and license terms. --> - - Open Balance Sheet - account.open_balance_sheet - - + - - account.open_income_statement.start + + account.balance_sheet.context form - open_income_statement_start_form + balance_sheet_context_form @@ -505,6 +570,7 @@ this repository contains the full copyright notices and license terms. --> Income Statement account.account.type + account.income_statement.context @@ -515,31 +581,49 @@ this repository contains the full copyright notices and license terms. --> - - Open Income Statement - account.open_income_statement - - + - - account.open_aged_balance.start + + account.income_statement.context form - open_aged_balance_start_form + income_statement_context_form + + + + account.aged_balance + tree + aged_balance_list + + + Aged Balance + account.aged_balance + account.aged_balance.context + + + + + + + + + + + account.aged_balance.context + form + aged_balance_context_form Aged Balance - + account.aged_balance account.aged_balance account/aged_balance.odt - - Open Aged Balance - account.open_aged_balance + + form_print + account.aged_balance,-1 + - diff --git a/aged_balance.odt b/aged_balance.odt index c0ca768e1d..82b91286a6 100644 Binary files a/aged_balance.odt and b/aged_balance.odt differ diff --git a/general_ledger.odt b/general_ledger.odt index de55c2f403..8b85b84d6d 100644 Binary files a/general_ledger.odt and b/general_ledger.odt differ diff --git a/move.xml b/move.xml index 86283cafb9..8110b94f20 100644 --- a/move.xml +++ b/move.xml @@ -300,6 +300,7 @@ this repository contains the full copyright notices and license terms. --> account.move.line,-1 + Unreconcile Lines diff --git a/tests/scenario_reports.rst b/tests/scenario_reports.rst index 01ee57ec9f..5deff0f5c6 100644 --- a/tests/scenario_reports.rst +++ b/tests/scenario_reports.rst @@ -79,18 +79,36 @@ Create a moves:: Print some reports:: - >>> print_general_ledger = Wizard('account.print_general_ledger') - >>> print_general_ledger.form.start_period = None - >>> print_general_ledger.form.end_period = None - >>> print_general_ledger.execute('print_') + >>> GeneralLedgerAccount = Model.get('account.general_ledger.account') + >>> gl_accounts = GeneralLedgerAccount.find([]) - >>> print_trial_balance = Wizard('account.print_trial_balance') - >>> print_trial_balance.form.start_period = None - >>> print_trial_balance.form.end_period = None - >>> print_trial_balance.execute('print_') + >>> general_ledger = Report('account.general_ledger', context={ + ... 'company': company.id, + ... 'fiscalyear': fiscalyear.id, + ... }) + >>> _ = general_ledger.execute(gl_accounts) - >>> aged_balance = Wizard('account.open_aged_balance') - >>> aged_balance.execute('print_') + >>> trial_balance = Report('account.trial_balance', context={ + ... 'company': company.id, + ... 'fiscalyear': fiscalyear.id, + ... }) + >>> _ = trial_balance.execute(gl_accounts) + + >>> AgedBalance = Model.get('account.aged_balance') + >>> context = { + ... 'company': company.id, + ... 'type': 'customer', + ... 'date': today, + ... 'term1': 30, + ... 'term2': 60, + ... 'term3': 90, + ... 'unit': 'day', + ... } + >>> with config.set_context(context): + ... aged_balances = AgedBalance.find([]) + + >>> aged_balance = Report('account.aged_balance', context=context) + >>> _ = aged_balance.execute(aged_balances) >>> print_general_journal = Wizard('account.move.print_general_journal') >>> print_general_journal.execute('print_') diff --git a/trial_balance.odt b/trial_balance.odt index dfc0bc7993..7c7b38f4f4 100644 Binary files a/trial_balance.odt and b/trial_balance.odt differ diff --git a/view/open_aged_balance_start_form.xml b/view/aged_balance_context_form.xml similarity index 72% rename from view/open_aged_balance_start_form.xml rename to view/aged_balance_context_form.xml index 2fd86af7e5..e60b91fa67 100644 --- a/view/open_aged_balance_start_form.xml +++ b/view/aged_balance_context_form.xml @@ -1,25 +1,22 @@ -
-