#This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from decimal import Decimal import datetime import time try: import hashlib except ImportError: hashlib = None import md5 from trytond.model import ModelView, ModelSQL, fields from trytond.wizard import Wizard, StateTransition, StateView, StateAction, \ Button from trytond.report import Report from trytond.backend import TableHandler, FIELDS from trytond.pyson import Eval, PYSONEncoder from trytond.transaction import Transaction from trytond.pool import Pool from trytond.config import CONFIG _MOVE_STATES = { 'readonly': Eval('state') == 'posted', } _MOVE_DEPENDS = ['state'] _LINE_STATES = { 'readonly': Eval('state') == 'valid', } _LINE_DEPENDS = ['state'] class Move(ModelSQL, ModelView): 'Account Move' _name = 'account.move' _description = __doc__ name = fields.Char('Name', size=None, required=True, states=_MOVE_STATES, depends=_MOVE_DEPENDS) reference = fields.Char('Reference', size=None, readonly=True, help='Also known as Folio Number') period = fields.Many2One('account.period', 'Period', required=True, states=_MOVE_STATES, depends=_MOVE_DEPENDS, select=True) journal = fields.Many2One('account.journal', 'Journal', required=True, states=_MOVE_STATES, depends=_MOVE_DEPENDS) date = fields.Date('Effective Date', required=True, states=_MOVE_STATES, depends=_MOVE_DEPENDS, on_change_with=['period', 'journal', 'date']) post_date = fields.Date('Post Date', readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('posted', 'Posted'), ], 'State', required=True, readonly=True, select=True) lines = fields.One2Many('account.move.line', 'move', 'Lines', states=_MOVE_STATES, depends=_MOVE_DEPENDS, context={ 'journal': Eval('journal'), 'period': Eval('period'), 'date': Eval('date'), }) def __init__(self): super(Move, self).__init__() self._check_modify_exclude = ['state'] self._constraints += [ ('check_centralisation', 'period_centralized_journal'), ('check_company', 'company_in_move'), ('check_date', 'date_outside_period'), ] self._rpc.update({ 'button_post': True, 'button_draft': True, }) self._order.insert(0, ('date', 'DESC')) self._order.insert(1, ('reference', 'DESC')) self._error_messages.update({ 'del_posted_move': 'You can not delete posted moves!', 'post_empty_move': 'You can not post an empty move!', 'post_unbalanced_move': 'You can not post an unbalanced move!', 'draft_posted_move_journal': 'You can not set a posted move ' \ 'to draft in this journal!', 'modify_posted_move': 'Move "%s" is already posted!\n' 'You can not modify a posted move.', 'period_centralized_journal': 'You can not create more than ' \ 'one move per period\n' \ 'in a centralized journal!', 'company_in_move': 'You can not create lines on accounts\n' \ 'of different companies in the same move!', 'date_outside_period': 'You can not create move ' \ 'with a date outside the period!', }) def init(self, module_name): super(Move, self).init(module_name) cursor = Transaction().cursor table = TableHandler(cursor, self, module_name) table.index_action(['journal', 'period'], 'add') # Add index on create_date table.index_action('create_date', action='add') def default_period(self): period_obj = Pool().get('account.period') return period_obj.find(Transaction().context.get('company') or False, exception=False) def default_state(self): return 'draft' def default_date(self): period_obj = Pool().get('account.period') date_obj = Pool().get('ir.date') period_id = self.default_period() if period_id: period = period_obj.browse(period_id) return period.start_date return date_obj.today() def on_change_with_date(self, vals): line_obj = Pool().get('account.move.line') period_obj = Pool().get('account.period') res = vals['date'] line_ids = line_obj.search([ ('journal', '=', vals.get('journal', False)), ('period', '=', vals.get('period', False)), ], order=[('id', 'DESC')], limit=1) if line_ids: line = line_obj.browse(line_ids[0]) res = line.date elif vals.get('period'): period = period_obj.browse(vals['period']) res = period.start_date return res def check_centralisation(self, ids): for move in self.browse(ids): if move.journal.centralised: move_ids = self.search([ ('period', '=', move.period.id), ('journal', '=', move.journal.id), ('state', '!=', 'posted'), ], limit=2) if len(move_ids) > 1: return False return True def check_company(self, ids): for move in self.browse(ids): company_id = -1 for line in move.lines: if company_id < 0: company_id = line.account.company.id if line.account.company.id != company_id: return False return True def check_date(self, ids): for move in self.browse(ids): if move.date < move.period.start_date: return False if move.date > move.period.end_date: return False return True def check_modify(self, ids): 'Check posted moves for modifications.' if isinstance(ids, (int, long)): ids = [ids] for move in self.browse(ids): if move.state == 'posted': self.raise_user_error('modify_posted_move', error_args=(move.name,)) return def search_rec_name(self, name, clause): ids = self.search(['OR', ('reference',) + clause[1:], (self._rec_name,) + clause[1:], ]) return [('id', 'in', ids)] def write(self, ids, vals): keys = vals.keys() for key in self._check_modify_exclude: if key in keys: keys.remove(key) if len(keys): self.check_modify(ids) res = super(Move, self).write(ids, vals) self.validate(ids) return res def create(self, vals): pool = Pool() move_line_obj = pool.get('account.move.line') sequence_obj = pool.get('ir.sequence') journal_obj = pool.get('account.journal') if not vals.get('name'): journal_id = (vals.get('journal') or Transaction().context.get('journal')) if journal_id: vals = vals.copy() journal = journal_obj.browse(journal_id) vals['name'] = sequence_obj.get_id(journal.sequence.id) res = super(Move, self).create(vals) move = self.browse(res) if move.journal.centralised: line_id = move_line_obj.create({ 'account': move.journal.credit_account.id, 'move': move.id, 'name': 'Centralised Counterpart', }) self.write(move.id, { 'centralised_line': line_id, }) if 'lines' in vals: self.validate([res]) return res def delete(self, ids): if isinstance(ids, (int, long)): ids = [ids] move_line_obj = Pool().get('account.move.line') self.check_modify(ids) for move in self.browse(ids): if move.lines: move_line_ids = [x.id for x in move.lines] move_line_obj.delete(move_line_ids) return super(Move, self).delete(ids) def copy(self, ids, default=None): line_obj = Pool().get('account.move.line') int_id = False if isinstance(ids, (int, long)): int_id = True ids = [ids] if default is None: default = {} default = default.copy() default['reference'] = False default['state'] = self.default_state() default['post_date'] = False default['lines'] = False new_ids = [] for move in self.browse(ids): new_id = super(Move, self).copy(move.id, default=default) line_obj.copy([x.id for x in move.lines], default={ 'move': new_id, }) new_ids.append(new_id) if int_id: return new_ids[0] return new_ids def validate(self, ids): ''' Validate balanced move and centralise it if in centralised journal ''' currency_obj = Pool().get('currency.currency') move_line_obj = Pool().get('account.move.line') if isinstance(ids, (int, long)): ids = [ids] for move in self.browse(ids): if not move.lines: continue amount = Decimal('0.0') company = None draft_lines = [] for line in move.lines: amount += line.debit - line.credit if not company: company = line.account.company if line.state == 'draft': draft_lines.append(line) if not currency_obj.is_zero(company.currency, amount): if not move.journal.centralised: move_line_obj.write( [x.id for x in move.lines if x.state != 'draft'], { 'state': 'draft', }) else: if not move.centralised_line: centralised_amount = - amount else: centralised_amount = move.centralised_line.debit \ - move.centralised_line.credit \ - amount if centralised_amount >= Decimal('0.0'): debit = centralised_amount credit = Decimal('0.0') account_id = move.journal.debit_account.id else: debit = Decimal('0.0') credit = - centralised_amount account_id = move.journal.credit_account.id if not move.centralised_line: centralised_line_id = move_line_obj.create({ 'debit': debit, 'credit': credit, 'account': account_id, 'move': move.id, 'name': 'Centralised Counterpart', }) self.write(move.id, { 'centralised_line': centralised_line_id, }) else: move_line_obj.write( move.centralised_line.id, { 'debit': debit, 'credit': credit, 'account': account_id, }) continue if not draft_lines: continue move_line_obj.write( [x.id for x in draft_lines], { 'state': 'valid', }) return def post(self, ids): pool = Pool() currency_obj = pool.get('currency.currency') sequence_obj = pool.get('ir.sequence') date_obj = pool.get('ir.date') if isinstance(ids, (int, long)): ids = [ids] moves = self.browse(ids) for move in moves: amount = Decimal('0.0') if not move.lines: self.raise_user_error('post_empty_move') company = None for line in move.lines: amount += line.debit - line.credit if not company: company = line.account.company if not currency_obj.is_zero(company.currency, amount): self.raise_user_error('post_unbalanced_move') for move in moves: reference = sequence_obj.get_id(move.period.post_move_sequence.id) self.write(move.id, { 'reference': reference, 'state': 'posted', 'post_date': date_obj.today(), }) return def draft(self, ids): for move in self.browse(ids): if not move.journal.update_posted: self.raise_user_error('draft_posted_move_journal') return self.write(ids, { 'state': 'draft', }) def button_post(self, ids): return self.post(ids) def button_draft(self, ids): return self.draft(ids) Move() class Reconciliation(ModelSQL, ModelView): 'Account Move Reconciliation Lines' _name = 'account.move.reconciliation' _description = __doc__ name = fields.Char('Name', size=None, required=True) lines = fields.One2Many('account.move.line', 'reconciliation', 'Lines') def __init__(self): super(Reconciliation, self).__init__() self._constraints += [ ('check_lines', 'invalid_reconciliation'), ] self._error_messages.update({ 'modify': 'You can not modify a reconciliation!', 'invalid_reconciliation': 'You can not create reconciliation ' \ 'where lines are not balanced, nor valid, ' \ 'nor in the same account, nor in account to reconcile, ' \ 'nor from the same party!', }) def create(self, vals): move_line_obj = Pool().get('account.move.line') sequence_obj = Pool().get('ir.sequence') if 'name' not in vals: vals = vals.copy() vals['name'] = sequence_obj.get('account.move.reconciliation') res = super(Reconciliation, self).create(vals) reconciliation = self.browse(res) move_line_obj.workflow_trigger_trigger( [x.id for x in reconciliation.lines]) return res def write(self, ids, vals): self.raise_user_error('modify') def check_lines(self, ids): currency_obj = Pool().get('currency.currency') for reconciliation in self.browse(ids): amount = Decimal('0.0') account = None party = None for line in reconciliation.lines: if line.state != 'valid': return False amount += line.debit - line.credit if not account: account = line.account elif account.id != line.account.id: return False if not account.reconcile: return False if not party: party = line.party elif line.party and party.id != line.party.id: return False if not currency_obj.is_zero(account.company.currency, amount): return False return True Reconciliation() class Line(ModelSQL, ModelView): 'Account Move Line' _name = 'account.move.line' _description = __doc__ name = fields.Char('Name', size=None, required=True) debit = fields.Numeric('Debit', digits=(16, Eval('currency_digits', 2)), required=True, on_change=['account', 'debit', 'credit', 'tax_lines', 'journal', 'move'], depends=['currency_digits', 'credit', 'tax_lines', 'journal']) credit = fields.Numeric('Credit', digits=(16, Eval('currency_digits', 2)), required=True, on_change=['account', 'debit', 'credit', 'tax_lines', 'journal', 'move'], depends=['currency_digits', 'debit', 'tax_lines', 'journal']) account = fields.Many2One('account.account', 'Account', required=True, domain=[('kind', '!=', 'view')], select=True, on_change=['account', 'debit', 'credit', 'tax_lines', 'journal', 'move']) move = fields.Many2One('account.move', 'Move', select=True, required=True, states={ 'required': False, 'readonly': Eval('state') == 'valid', }, depends=['state']) journal = fields.Function(fields.Many2One('account.journal', 'Journal'), 'get_move_field', setter='set_move_field', searcher='search_move_field') period = fields.Function(fields.Many2One('account.period', 'Period'), 'get_move_field', setter='set_move_field', searcher='search_move_field') date = fields.Function(fields.Date('Effective Date', required=True), 'get_move_field', setter='set_move_field', searcher='search_move_field') reference = fields.Char('Reference', size=None) amount_second_currency = fields.Numeric('Amount Second Currency', digits=(16, Eval('second_currency_digits', 2)), help='The amount expressed in a second currency', depends=['second_currency_digits']) second_currency = fields.Many2One('currency.currency', 'Second Currency', help='The second currency') party = fields.Many2One('party.party', 'Party', on_change=['move', 'party', 'account', 'debit', 'credit', 'journal'], select=True, depends=['debit', 'credit', 'account', 'journal']) maturity_date = fields.Date('Maturity Date', help='This field is used for payable and receivable lines. \n' \ 'You can put the limit date for the payment.') state = fields.Selection([ ('draft', 'Draft'), ('valid', 'Valid'), ], 'State', readonly=True, required=True, select=True) active = fields.Boolean('Active', select=True) reconciliation = fields.Many2One('account.move.reconciliation', 'Reconciliation', readonly=True, ondelete='SET NULL', select=True) tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines') move_state = fields.Function(fields.Selection([ ('draft', 'Draft'), ('posted', 'Posted'), ], 'Move State'), 'get_move_field', searcher='search_move_field') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') second_currency_digits = fields.Function(fields.Integer( 'Second Currency Digits'), 'get_currency_digits') def __init__(self): super(Line, self).__init__() self._sql_constraints += [ ('credit_debit', 'CHECK(credit * debit = 0.0)', 'Wrong credit/debit values!'), ] self._constraints += [ ('check_account', 'move_view_inactive_account'), ] self._rpc.update({ 'on_write': False, }) self._order[0] = ('id', 'DESC') self._error_messages.update({ 'add_modify_closed_journal_period': 'You can not ' \ 'add/modify lines in a closed journal period!', 'modify_posted_move': 'You can not modify lines of a posted move!', 'modify_reconciled': 'You can not modify reconciled lines!', 'no_journal': 'No journal defined!', 'move_view_inactive_account': 'You can not create move lines\n' \ 'on view/inactive accounts!', 'already_reconciled': 'Line "%s" (%d) already reconciled!', }) def init(self, module_name): super(Line, self).init(module_name) cursor = Transaction().cursor table = TableHandler(cursor, self, module_name) # Index for General Ledger table.index_action(['move', 'account'], 'add') # Migration from 1.2 table.not_null_action('blocked', action='remove') def default_date(self): ''' Return the date of the last line for journal, period or the starting date of the period or today ''' period_obj = Pool().get('account.period') date_obj = Pool().get('ir.date') res = date_obj.today() line_ids = self.search([ ('journal', '=', Transaction().context.get('journal', False)), ('period', '=', Transaction().context.get('period', False)), ], order=[('id', 'DESC')], limit=1) if line_ids: line = self.browse(line_ids[0]) res = line.date elif Transaction().context.get('period'): period = period_obj.browse(Transaction().context['period']) res = period.start_date if Transaction().context.get('date'): res = Transaction().context['date'] return res def default_state(self): return 'draft' def default_active(self): return True def default_currency_digits(self): return 2 def default_debit(self): return Decimal(0) def default_credit(self): return Decimal(0) def default_get(self, fields, with_rec_name=True): pool = Pool() move_obj = pool.get('account.move') tax_obj = pool.get('account.tax') account_obj = pool.get('account.account') tax_code_obj = pool.get('account.tax.code') currency_obj = pool.get('currency.currency') values = super(Line, self).default_get(fields, with_rec_name=with_rec_name) if 'move' not in fields: #Not manual entry if 'date' in values: values = values.copy() del values['date'] return values if (Transaction().context.get('journal') and Transaction().context.get('period')): line_ids = self.search([ ('move.journal', '=', Transaction().context['journal']), ('move.period', '=', Transaction().context['period']), ('create_uid', '=', Transaction().user), ('state', '=', 'draft'), ], order=[('id', 'DESC')], limit=1) if not line_ids: return values line = self.browse(line_ids[0]) values['move'] = line.move.id if 'move' not in values: return values move = move_obj.browse(values['move']) total = Decimal('0.0') taxes = {} no_code_taxes = [] for line in move.lines: total += line.debit - line.credit if line.party and 'party' in fields: values.setdefault('party', line.party.id) if 'reference' in fields: values.setdefault('reference', line.reference) if 'name' in fields: values.setdefault('name', line.name) if move.journal.type in ('expense', 'revenue'): line_code_taxes = [x.code.id for x in line.tax_lines] for tax in line.account.taxes: if move.journal.type == 'revenue': if line.debit: base_id = tax.credit_note_base_code.id code_id = tax.credit_note_tax_code.id account_id = tax.credit_note_account.id else: base_id = tax.invoice_base_code.id code_id = tax.invoice_tax_code.id account_id = tax.invoice_account.id else: if line.debit: base_id = tax.invoice_base_code.id code_id = tax.invoice_tax_code.id account_id = tax.invoice_account.id else: base_id = tax.credit_note_base_code.id code_id = tax.credit_note_tax_code.id account_id = tax.credit_note_account.id if base_id in line_code_taxes or not base_id: taxes.setdefault((account_id, code_id, tax.id), False) for tax_line in line.tax_lines: taxes[(line.account.id, tax_line.code.id, tax_line.tax.id)] = True if not line.tax_lines: no_code_taxes.append(line.account.id) for no_code_account_id in no_code_taxes: for (account_id, code_id, tax_id), test in \ taxes.iteritems(): if (not test and not code_id and no_code_account_id == account_id): taxes[(account_id, code_id, tax_id)] = True if 'account' in fields: if total >= Decimal('0.0'): values.setdefault('account', move.journal.credit_account \ and move.journal.credit_account.id or False) else: values.setdefault('account', move.journal.debit_account \ and move.journal.debit_account.id or False) if ('debit' in fields) or ('credit' in fields): values.setdefault('debit', total < 0 and - total or False) values.setdefault('credit', total > 0 and total or False) if move.journal.type in ('expense', 'revenue'): for account_id, code_id, tax_id in taxes: if taxes[(account_id, code_id, tax_id)]: continue for line in move.lines: if move.journal.type == 'revenue': if line.debit: key = 'credit_note' else: key = 'invoice' else: if line.debit: key = 'invoice' else: key = 'credit_note' line_amount = Decimal('0.0') tax_amount = Decimal('0.0') for tax_line in tax_obj.compute( [x.id for x in line.account.taxes], line.debit or line.credit, 1): if (tax_line['tax'][key + '_account'].id \ or line.account.id) == account_id \ and tax_line['tax'][key + '_tax_code'].id \ == code_id \ and tax_line['tax'].id == tax_id: if line.debit: line_amount += tax_line['amount'] else: line_amount -= tax_line['amount'] tax_amount += tax_line['amount'] * \ tax_line['tax'][key + '_tax_sign'] line_amount = currency_obj.round( line.account.company.currency, line_amount) tax_amount = currency_obj.round( line.account.company.currency, tax_amount) if ('debit' in fields): values['debit'] = line_amount > Decimal('0.0') \ and line_amount or Decimal('0.0') if ('credit' in fields): values['credit'] = line_amount < Decimal('0.0') \ and - line_amount or Decimal('0.0') if 'account' in fields: values['account'] = account_id values['account.rec_name'] = account_obj.browse( account_id).rec_name if 'tax_lines' in fields and code_id: values['tax_lines'] = [ { 'amount': tax_amount, 'currency_digits': line.currency_digits, 'code': code_id, 'code.rec_name': tax_code_obj.browse( code_id).rec_name, 'tax': tax_id, 'tax.rec_name': tax_obj.browse(tax_id).rec_name }, ] return values def get_currency_digits(self, ids, names): res = {} for line in self.browse(ids): for name in names: res.setdefault(name, {}) res[name].setdefault(line.id, 2) if name == 'currency_digits': res[name][line.id] = line.account.currency_digits elif name == 'second_currency_digits': if line.account.second_currency: res[name][line.id] = line.account.second_currency.digits return res def on_change_debit(self, vals): res = {} journal_obj = Pool().get('account.journal') if vals.get('journal') or Transaction().context.get('journal'): journal = journal_obj.browse(vals.get('journal') or Transaction().context.get('journal')) if journal.type in ('expense', 'revenue'): res['tax_lines'] = self._compute_tax_lines(vals, journal.type) if not res['tax_lines']: del res['tax_lines'] if vals.get('debit'): res['credit'] = Decimal('0.0') return res def on_change_credit(self, vals): res = {} journal_obj = Pool().get('account.journal') if vals.get('journal') or Transaction().context.get('journal'): journal = journal_obj.browse(vals.get('journal') or Transaction().context.get('journal')) if journal.type in ('expense', 'revenue'): res['tax_lines'] = self._compute_tax_lines( vals, journal.type) if not res['tax_lines']: del res['tax_lines'] if vals.get('credit'): res['debit'] = Decimal('0.0') return res def on_change_account(self, vals): account_obj = Pool().get('account.account') journal_obj = Pool().get('account.journal') res = {} if Transaction().context.get('journal'): journal = journal_obj.browse(Transaction().context['journal']) if journal.type in ('expense', 'revenue'): res['tax_lines'] = self._compute_tax_lines(vals, journal.type) if not res['tax_lines']: del res['tax_lines'] if vals.get('account'): account = account_obj.browse(vals['account']) res['currency_digits'] = account.currency_digits if account.second_currency: res['second_currency_digits'] = account.second_currency.digits return res def _compute_tax_lines(self, vals, journal_type): res = {} pool = Pool() account_obj = pool.get('account.account') tax_code_obj = pool.get('account.tax.code') tax_obj = pool.get('account.tax') if vals.get('move'): #Only for first line return res if vals.get('tax_lines'): res['remove'] = [x['id'] for x in vals['tax_lines']] if vals.get('account'): account = account_obj.browse(vals['account']) debit = vals.get('debit', Decimal('0.0')) credit = vals.get('credit', Decimal('0.0')) for tax in account.taxes: if journal_type == 'revenue': if debit: key = 'credit_note' else: key = 'invoice' else: if debit: key = 'invoice' else: key = 'credit_note' base_amounts = {} for tax_line in tax_obj.compute( [x.id for x in account.taxes], debit or credit, 1): code_id = tax_line['tax'][key + '_base_code'].id if not code_id: continue tax_id = tax_line['tax'].id base_amounts.setdefault((code_id, tax_id), Decimal('0.0')) base_amounts[code_id, tax_id] += tax_line['base'] * \ tax_line['tax'][key + '_tax_sign'] for code_id, tax_id in base_amounts: if not base_amounts[code_id, tax_id]: continue res.setdefault('add', []).append({ 'amount': base_amounts[code_id, tax_id], 'currency_digits': account.currency_digits, 'code': code_id, 'code.rec_name': tax_code_obj.browse(code_id).rec_name, 'tax': tax_id, 'tax.rec_name': tax_obj.browse(tax_id).rec_name, }) return res def on_change_party(self, vals): pool = Pool() party_obj = pool.get('party.party') journal_obj = pool.get('account.journal') currency_obj = pool.get('currency.currency') cursor = Transaction().cursor res = {} if (not vals.get('party')) or vals.get('account'): return res party = party_obj.browse(vals.get('party')) if not party.account_receivable \ or not party.account_payable: return res if party and (not vals.get('debit')) and (not vals.get('credit')): type_name = FIELDS[self.debit._type].sql_type(self.debit)[0] query = 'SELECT ' \ 'CAST(COALESCE(SUM(' \ '(COALESCE(debit, 0) - COALESCE(credit, 0))' \ '), 0) AS ' + type_name + ') ' \ 'FROM account_move_line ' \ 'WHERE reconciliation IS NULL ' \ 'AND party = %s ' \ 'AND account = %s' cursor.execute(query, (party.id, party.account_receivable.id)) amount = cursor.fetchone()[0] # SQLite uses float for SUM if not isinstance(amount, Decimal): amount = Decimal(str(amount)) if not currency_obj.is_zero( party.account_receivable.currency, amount): if amount > Decimal('0.0'): res['credit'] = currency_obj.round( party.account_receivable.currency, amount) res['debit'] = Decimal('0.0') else: res['credit'] = Decimal('0.0') res['debit'] = - currency_obj.round( party.account_receivable.currency, amount) res['account'] = party.account_receivable.id res['account.rec_name'] = party.account_receivable.rec_name else: cursor.execute(query, (party.id, party.account_payable.id)) amount = cursor.fetchone()[0] if not currency_obj.is_zero( party.account_payable.currency, amount): if amount > Decimal('0.0'): res['credit'] = currency_obj.round( party.account_payable.currency, amount) res['debit'] = Decimal('0.0') else: res['credit'] = Decimal('0.0') res['debit'] = - currency_obj.round( party.account_payable.currency, amount) res['account'] = party.account_payable.id res['account.rec_name'] = party.account_payable.rec_name if party and vals.get('debit'): if vals['debit'] > Decimal('0.0'): if 'account' not in res: res['account'] = party.account_receivable.id res['account.rec_name'] = party.account_receivable.rec_name else: if 'account' not in res: res['account'] = party.account_payable.id res['account.rec_name'] = party.account_payable.id if party and vals.get('credit'): if vals['credit'] > Decimal('0.0'): if 'account' not in res: res['account'] = party.account_payable.id res['account.rec_name'] = party.account_payable.rec_name else: if 'account' not in res: res['account'] = party.account_receivable.id res['account.rec_name'] = party.account_receivable.rec_name journal_id = (vals.get('journal') or Transaction().context.get('journal')) if journal_id and party: journal = journal_obj.browse(journal_id) if journal.type == 'revenue': if 'account' not in res: res['account'] = party.account_receivable.id res['account.rec_name'] = party.account_receivable.rec_name elif journal.type == 'expense': if 'account' not in res: res['account'] = party.account_payable.id res['account.rec_name'] = party.account_payable.rec_name return res def get_move_field(self, ids, name): if name == 'move_state': name = 'state' if name not in ('period', 'journal', 'date', 'state'): raise Exception('Invalid name') res = {} for line in self.browse(ids): if name in ('date', 'state'): res[line.id] = line.move[name] else: res[line.id] = line.move[name].id return res def set_move_field(self, ids, name, value): if name == 'move_state': name = 'state' if name not in ('period', 'journal', 'date', 'state'): raise Exception('Invalid name') if not value: return move_obj = Pool().get('account.move') lines = self.browse(ids) move_obj.write([line.move.id for line in lines], { name: value, }) def search_move_field(self, name, clause): if name == 'move_state': name = 'state' return [('move.' + name,) + tuple(clause[1:])] def query_get(self, obj='l'): ''' Return SQL clause and fiscal years for account move line depending of the context. :param obj: the SQL alias of account_move_line in the query :return: a tuple with the SQL clause and the fiscalyear ids ''' fiscalyear_obj = Pool().get('account.fiscalyear') if Transaction().context.get('date'): time.strptime(str(Transaction().context['date']), '%Y-%m-%d') fiscalyear_ids = fiscalyear_obj.search([ ('start_date', '<=', Transaction().context['date']), ('end_date', '>=', Transaction().context['date']), ], limit=1) fiscalyear_id = fiscalyear_ids and fiscalyear_ids[0] or 0 if Transaction().context.get('posted'): return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT m.id FROM account_move AS m, ' \ 'account_period AS p ' \ 'WHERE m.period = p.id ' \ 'AND p.fiscalyear = ' + \ str(fiscalyear_id) + ' ' \ 'AND m.date <= date(\'' + \ str(Transaction().context['date']) + \ '\') ' \ 'AND m.state = \'posted\' ' \ ')', fiscalyear_ids) else: return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT m.id FROM account_move AS m, ' \ 'account_period AS p ' \ 'WHERE m.period = p.id ' \ 'AND p.fiscalyear = ' + \ str(fiscalyear_id) + ' ' \ 'AND m.date <= date(\'' + \ str(Transaction().context['date']) + \ '\')' \ ')', fiscalyear_ids) if not Transaction().context.get('fiscalyear'): fiscalyear_ids = fiscalyear_obj.search([ ('state', '=', 'open'), ]) fiscalyear_clause = (','.join(map(str, fiscalyear_ids))) or '0' else: fiscalyear_ids = [int(Transaction().context.get('fiscalyear'))] fiscalyear_clause = '%s' % int( Transaction().context.get('fiscalyear')) if Transaction().context.get('periods'): ids = ','.join( str(int(x)) for x in Transaction().context['periods']) if Transaction().context.get('posted'): return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT id FROM account_move ' \ 'WHERE period IN (' + ids + ') ' \ 'AND state = \'posted\' ' \ ')', []) else: return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT id FROM account_move ' \ 'WHERE period IN (' + ids + ')' \ ')', []) else: if Transaction().context.get('posted'): return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT id FROM account_move ' \ 'WHERE period IN (' \ 'SELECT id FROM account_period ' \ 'WHERE fiscalyear IN (' + fiscalyear_clause + ')' \ ') ' \ 'AND state = \'posted\' ' \ ')', fiscalyear_ids) else: return (obj + '.active ' \ 'AND ' + obj + '.state != \'draft\' ' \ 'AND ' + obj + '.move IN (' \ 'SELECT id FROM account_move ' \ 'WHERE period IN (' \ 'SELECT id FROM account_period ' \ 'WHERE fiscalyear IN (' + fiscalyear_clause + ')' \ ')' \ ')', fiscalyear_ids) def on_write(self, ids): lines = self.browse(ids) res = [] for line in lines: res.extend([x.id for x in line.move.lines]) return list({}.fromkeys(res)) def check_account(self, ids): for line in self.browse(ids): if line.account.kind in ('view',): return False if not line.account.active: return False return True def check_journal_period_modify(self, period_id, journal_id): ''' Check if the lines can be modified or created for the journal - period and if there is no journal - period, create it ''' pool = Pool() journal_period_obj = pool.get('account.journal.period') journal_obj = pool.get('account.journal') period_obj = pool.get('account.period') journal_period_ids = journal_period_obj.search([ ('journal', '=', journal_id), ('period', '=', period_id), ], limit=1) if journal_period_ids: journal_period = journal_period_obj.browse(journal_period_ids[0]) if journal_period.state == 'close': self.raise_user_error('add_modify_closed_journal_period') else: journal = journal_obj.browse(journal_id) period = period_obj.browse(period_id) journal_period_obj.create({ 'name': journal.name + ' - ' + period.name, 'journal': journal.id, 'period': period.id, }) return def check_modify(self, ids): ''' Check if the lines can be modified ''' journal_period_done = [] for line in self.browse(ids): if line.move.state == 'posted': self.raise_user_error('modify_posted_move') if line.reconciliation: self.raise_user_error('modify_reconciled') journal_period = (line.journal.id, line.period.id) if journal_period not in journal_period_done: self.check_journal_period_modify(line.period.id, line.journal.id) journal_period_done.append(journal_period) return def delete(self, ids): move_obj = Pool().get('account.move') if isinstance(ids, (int, long)): ids = [ids] self.check_modify(ids) lines = self.browse(ids) move_ids = [x.move.id for x in lines] res = super(Line, self).delete(ids) move_obj.validate(move_ids) return res def write(self, ids, vals): move_obj = Pool().get('account.move') if isinstance(ids, (int, long)): ids = [ids] if len(vals) > 1 or 'reconciliation' not in vals: self.check_modify(ids) lines = self.browse(ids) move_ids = [x.move.id for x in lines] res = super(Line, self).write(ids, vals) Transaction().timestamp = {} lines = self.browse(ids) for line in lines: if line.move.id not in move_ids: move_ids.append(line.move.id) move_obj.validate(move_ids) return res def create(self, vals): journal_obj = Pool().get('account.journal') move_obj = Pool().get('account.move') vals = vals.copy() if not vals.get('move'): journal_id = (vals.get('journal') or Transaction().context.get('journal')) if not journal_id: self.raise_user_error('no_journal') journal = journal_obj.browse(journal_id) if journal.centralised: move_ids = move_obj.search([ ('period', '=', vals.get('period') or Transaction().context.get('period')), ('journal', '=', journal_id), ('state', '!=', 'posted'), ], limit=1) if move_ids: vals['move'] = move_ids[0] if not vals.get('move'): vals['move'] = move_obj.create({ 'period': (vals.get('period') or Transaction().context.get('period')), 'journal': journal_id, 'date': vals.get('date', False), }) res = super(Line, self).create(vals) line = self.browse(res) self.check_journal_period_modify(line.period.id, line.journal.id) move_obj.validate([vals['move']]) return res def copy(self, ids, default=None): if default is None: default = {} if 'move' not in default: default['move'] = False if 'reconciliation' not in default: default['reconciliation'] = False return super(Line, self).copy(ids, default=default) def view_header_get(self, value, view_type='form'): journal_period_obj = Pool().get('account.journal.period') if (not Transaction().context.get('journal') or not Transaction().context.get('period')): return value journal_period_ids = journal_period_obj.search([ ('journal', '=', Transaction().context['journal']), ('period', '=', Transaction().context['period']), ], limit=1) if not journal_period_ids: return value journal_period = journal_period_obj.browse(journal_period_ids[0]) return value + ': ' + journal_period.rec_name def fields_view_get(self, view_id=None, view_type='form', hexmd5=None): journal_obj = Pool().get('account.journal') result = super(Line, self).fields_view_get(view_id=view_id, view_type=view_type, hexmd5=hexmd5) if view_type == 'tree' and 'journal' in Transaction().context: title = self.view_header_get('', view_type=view_type) journal = journal_obj.browse(Transaction().context['journal']) if not journal.view: return result xml = '\n' \ '\n' % title fields = set() for column in journal.view.columns: fields.add(column.field.name) attrs = [] if column.field.name == 'debit': attrs.append('sum="Debit"') elif column.field.name == 'credit': attrs.append('sum="Credit"') if column.readonly: attrs.append('readonly="1"') if column.required: attrs.append('required="1"') else: attrs.append('required="0"') xml += '\n' % (column.field.name, ' '.join(attrs)) for depend in getattr(self, column.field.name).depends: fields.add(depend) fields.add('state') xml += '' result['arch'] = xml result['fields'] = self.fields_get(fields_names=list(fields)) del result['md5'] if hashlib: result['md5'] = hashlib.md5(str(result)).hexdigest() else: result['md5'] = md5.new(str(result)).hexdigest() if hexmd5 == result['md5']: return True return result def reconcile(self, ids, journal_id=False, date=False, account_id=False): pool = Pool() move_obj = pool.get('account.move') currency_obj = pool.get('currency.currency') reconciliation_obj = pool.get('account.move.reconciliation') period_obj = pool.get('account.period') date_obj = pool.get('ir.date') translation_obj = pool.get('ir.translation') for line in self.browse(ids): if line.reconciliation: self.raise_user_error('already_reconciled', error_args=(line.name, line.id,)) ids = ids[:] if journal_id and account_id: if not date: date = date_obj.today() account = None amount = Decimal('0.0') for line in self.browse(ids): amount += line.debit - line.credit if not account: account = line.account amount = currency_obj.round(account.currency, amount) period_id = period_obj.find(account.company.id, date=date) lang_code = CONFIG['language'] if account.company.lang: lang_code = account.company.lang.code writeoff = translation_obj.get_source( 'account.move.reconcile_lines.writeoff', 'view', lang_code, 'Write-Off') or 'Write-Off' move_id = move_obj.create({ 'journal': journal_id, 'period': period_id, 'date': date, 'lines': [ ('create', { 'name': writeoff, 'account': account.id, 'debit': amount < Decimal('0.0') and - amount \ or Decimal('0.0'), 'credit': amount > Decimal('0.0') and amount \ or Decimal('0.0'), }), ('create', { 'name': writeoff, 'account': account_id, 'debit': amount > Decimal('0.0') and amount \ or Decimal('0.0'), 'credit': amount < Decimal('0.0') and - amount \ or Decimal('0.0'), }), ], }) ids += self.search([ ('move', '=', move_id), ('account', '=', account.id), ('debit', '=', amount < Decimal('0.0') and - amount \ or Decimal('0.0')), ('credit', '=', amount > Decimal('0.0') and amount \ or Decimal('0.0')), ], limit=1) return reconciliation_obj.create({ 'lines': [('add', x) for x in ids], }) Line() class Move2(ModelSQL, ModelView): _name = 'account.move' centralised_line = fields.Many2One('account.move.line', 'Centralised Line', readonly=True) Move2() class OpenJournalAsk(ModelView): 'Open Journal Ask' _name = 'account.move.open_journal.ask' _description = __doc__ journal = fields.Many2One('account.journal', 'Journal', required=True) period = fields.Many2One('account.period', 'Period', required=True, domain=[ ('state', '!=', 'close'), ('fiscalyear.company.id', '=', Eval('context', {}).get('company', 0)), ]) def default_period(self): period_obj = Pool().get('account.period') return period_obj.find(Transaction().context.get('company') or False, exception=False) OpenJournalAsk() 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, session): if (Transaction().context.get('active_model', '') == 'account.journal.period' and Transaction().context.get('active_id')): return 'open_' return 'ask' def default_ask(self, session, fields): journal_period_obj = Pool().get('account.journal.period') if (Transaction().context.get('active_model', '') == 'account.journal.period' and Transaction().context.get('active_id')): journal_period = \ journal_period_obj.browse(Transaction().context['active_id']) return { 'journal': journal_period.journal.id, 'period': journal_period.period.id, } return {} def do_open_(self, session, action): pool = Pool() journal_period_obj = pool.get('account.journal.period') journal_obj = pool.get('account.journal') period_obj = pool.get('account.period') if (Transaction().context.get('active_model', '') == 'account.journal.period' and Transaction().context.get('active_id')): journal_period = \ journal_period_obj.browse(Transaction().context['active_id']) journal_id = journal_period.journal.id period_id = journal_period.period.id else: journal_id = session.ask.journal.id period_id = session.ask.period.id journal_period_ids = journal_period_obj.search([ ('journal', '=', journal_id), ('period', '=', period_id), ]) if not journal_period_ids: journal = journal_obj.browse(journal_id) period = period_obj.browse(period_id) journal_period_id = journal_period_obj.create({ 'name': journal.name + ' - ' + period.name, 'journal': journal.id, 'period': period.id, }) else: journal_period_id = journal_period_ids[0] journal_period = journal_period_obj.browse(journal_period_id) action['name'] += ' - %s' % journal_period.rec_name action['pyson_domain'] = PYSONEncoder().encode([ ('journal', '=', journal_id), ('period', '=', period_id), ]) action['pyson_context'] = PYSONEncoder().encode({ 'journal': journal_id, 'period': period_id, }) return action, {} def transition_open_(self, session): return 'end' OpenJournal() class OpenAccount(Wizard): 'Open Account' _name = 'account.move.open_account' start_state = 'open_' open_ = StateAction('account.act_move_line_form') def do_open_(self, session, action): pool = Pool() fiscalyear_obj = pool.get('account.fiscalyear') if not Transaction().context.get('fiscalyear'): fiscalyear_ids = fiscalyear_obj.search([ ('state', '=', 'open'), ]) else: fiscalyear_ids = [Transaction().context['fiscalyear']] period_ids = [] for fiscalyear in fiscalyear_obj.browse(fiscalyear_ids): for period in fiscalyear.periods: period_ids.append(period.id) action['pyson_domain'] = [ ('period', 'in', period_ids), ('account', '=', Transaction().context['active_id']), ] if Transaction().context.get('posted'): action['pyson_domain'].append(('move.state', '=', 'posted')) action['pyson_domain'] = PYSONEncoder().encode(action['pyson_domain']) action['pyson_context'] = PYSONEncoder().encode({ 'fiscalyear': Transaction().context.get('fiscalyear'), }) return action, {} OpenAccount() class ReconcileLinesWriteOff(ModelView): 'Reconcile Lines Write-Off' _name = 'account.move.reconcile_lines.writeoff' _description = __doc__ journal = fields.Many2One('account.journal', 'Journal', required=True) date = fields.Date('Date', required=True) account = fields.Many2One('account.account', 'Account', required=True, domain=[('kind', '!=', 'view')]) def default_date(self): date_obj = Pool().get('ir.date') return date_obj.today() ReconcileLinesWriteOff() 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() def transition_start(self, session): line_obj = Pool().get('account.move.line') currency_obj = Pool().get('currency.currency') company = None amount = Decimal('0.0') for line in line_obj.browse(Transaction().context['active_ids']): amount += line.debit - line.credit if not company: company = line.account.company if not company: return 'end' if currency_obj.is_zero(company.currency, amount): return 'reconcile' return 'writeoff' def transition_reconcile(self, session): line_obj = Pool().get('account.move.line') journal_id = False date = False account_id = False if session.writeoff.journal: journal_id = session.writeoff.journal.id if session.writeoff.date: date = session.writeoff.date if session.writeoff.account: account_id = session.writeoff.account.id line_obj.reconcile(Transaction().context['active_ids'], journal_id, date, account_id) return 'end' ReconcileLines() class UnreconcileLinesStart(ModelView): 'Unreconcile Lines' _name = 'account.move.unreconcile_lines.start' _description = __doc__ UnreconcileLinesStart() class UnreconcileLines(Wizard): 'Unreconcile Lines' _name = 'account.move.unreconcile_lines' start = StateView('account.move.unreconcile_lines.start', 'account.unreconcile_lines_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Unreconcile', 'unreconcile', 'tryton-ok', default=True), ]) unreconcile = StateTransition() def transition_unreconcile(self, session): line_obj = Pool().get('account.move.line') reconciliation_obj = Pool().get('account.move.reconciliation') lines = line_obj.browse(Transaction().context['active_ids']) reconciliation_ids = [x.reconciliation.id for x in lines \ if x.reconciliation] if reconciliation_ids: reconciliation_obj.delete(reconciliation_ids) return 'end' UnreconcileLines() class OpenReconcileLinesStart(ModelView): 'Open Reconcile Lines' _name = 'account.move.open_reconcile_lines.start' _description = __doc__ account = fields.Many2One('account.account', 'Account', required=True, domain=[('kind', '!=', 'view'), ('reconcile', '=', True)]) OpenReconcileLinesStart() class OpenReconcileLines(Wizard): 'Open Reconcile Lines' _name = 'account.move.open_reconcile_lines' start = StateView('account.move.open_reconcile_lines.start', 'account.open_reconcile_lines_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Open', 'open_', 'tryton-ok', default=True), ]) open_ = StateAction('account.act_move_line_form') def do_open_(self, session, action): action['pyson_domain'] = PYSONEncoder().encode([ ('account', '=', session.start.account.id), ('reconciliation', '=', False), ]) return action, {} OpenReconcileLines() class FiscalYearLine(ModelSQL): 'Fiscal Year - Move Line' _name = 'account.fiscalyear-account.move.line' _table = 'account_fiscalyear_line_rel' _description = __doc__ fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', ondelete='CASCADE', select=True) line = fields.Many2One('account.move.line', 'Line', ondelete='RESTRICT', select=True, required=True) FiscalYearLine() class FiscalYear(ModelSQL, ModelView): _name = 'account.fiscalyear' close_lines = fields.Many2Many('account.fiscalyear-account.move.line', 'fiscalyear', 'line', 'Close Lines') FiscalYear() class PrintGeneralJournalStart(ModelView): 'Print General Journal' _name = 'account.move.print_general_journal.start' _description = __doc__ from_date = fields.Date('From Date', required=True) to_date = fields.Date('To Date', required=True) company = fields.Many2One('company.company', 'Company', required=True) posted = fields.Boolean('Posted Move', help='Show only posted move') def default_from_date(self): date_obj = Pool().get('ir.date') return datetime.date(date_obj.today().year, 1, 1) def default_to_date(self): date_obj = Pool().get('ir.date') return date_obj.today() def default_company(self): return Transaction().context.get('company') or False def default_posted(self): return False PrintGeneralJournalStart() class PrintGeneralJournal(Wizard): 'Print General Journal' _name = 'account.move.print_general_journal' start = StateView('account.move.print_general_journal.start', 'account.print_general_journal_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Print', 'print_', 'tryton-print', default=True), ]) print_ = StateAction('account.report_general_journal') def do_print_(self, session, action): data = { 'company': session.start.company.id, 'from_date': session.start.from_date, 'to_date': session.start.to_date, 'posted': session.start.posted, } return action, data PrintGeneralJournal() class GeneralJournal(Report): _name = 'account.move.general_journal' def _get_objects(self, ids, model, data): move_obj = Pool().get('account.move') clause = [ ('date', '>=', data['from_date']), ('date', '<=', data['to_date']), ] if data['posted']: clause.append(('state', '=', 'posted')) move_ids = move_obj.search(clause, order=[('date', 'ASC'), ('reference', 'ASC'), ('id', 'ASC')]) return move_obj.browse(move_ids) def parse(self, report, objects, data, localcontext): company_obj = Pool().get('company.company') company = company_obj.browse(data['company']) localcontext['company'] = company localcontext['digits'] = company.currency.digits localcontext['from_date'] = data['from_date'] localcontext['to_date'] = data['to_date'] return super(GeneralJournal, self).parse(report, objects, data, localcontext) GeneralJournal()