'Move' from trytond.osv import fields, OSV, ExceptORM from trytond.wizard import Wizard, WizardOSV from decimal import Decimal import datetime _MOVE_STATES = { 'readonly': "state == 'posted'", } _LINE_STATES = { 'readonly': "state == 'valid'", } class Move(OSV): 'Account Move' _name = 'account.move' _description = __doc__ name = fields.Char('Name', size=None, required=True) reference = fields.Char('Reference', size=None, readonly=True) period = fields.Many2One('account.period', 'Period', required=True, states=_MOVE_STATES) journal = fields.Many2One('account.journal', 'Journal', required=True, states=_MOVE_STATES) date = fields.Date('Effective Date', required=True, states=_MOVE_STATES) state = fields.Selection([ ('draft', 'Draft'), ('posted', 'Posted'), ], 'State', required=True, readonly=True) lines = fields.One2Many('account.move.line', 'move', 'Lines', states=_MOVE_STATES) def __init__(self): super(Move, self).__init__() self._constraints += [ ('check_centralisation', 'Error! You can not create more than one move per period \n' \ 'in centralized journal', ['journal']), ('check_company', 'Error! You can not create lines on account \n' \ 'from different company in the same move!', ['lines']), ] self._rpc_allowed += [ 'button_post', 'button_draft', ] def _auto_init(self, cursor, module_name): super(Move, self)._auto_init(cursor, module_name) cursor.execute('SELECT indexname FROM pg_indexes ' \ 'WHERE indexname = \'account_move_journal_period_index\'') if not cursor.rowcount: cursor.execute('CREATE INDEX account_move_journal_period_index ' \ 'ON account_move (period, journal)') def default_name(self, cursor, user, context=None): sequence_obj = self.pool.get('ir.sequence') return sequence_obj.get(cursor, user, 'account.move') def default_period(self, cursor, user, context=None): period_obj = self.pool.get('account.period') return period_obj.find(cursor, user, exception=False, context=context) def default_state(self, cursor, user, context=None): return 'draft' def default_date(self, cursor, user, context=None): return datetime.date.today() def check_centralisation(self, cursor, user, ids): for move in self.browse(cursor, user, ids): if move.journal.centralised: move_ids = self.search(cursor, user, [ ('period', '=', move.period.id), ('journal', '=', move.journal.id), ], limit=2) if len(move_ids) > 1: return False return True def check_company(self, cursor, user, ids): for move in self.browse(cursor, user, 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 write(self, cursor, user, ids, vals, context=None): res = super(Move, self).write(cursor, user, ids, vals, context=context) self.validate(cursor, user, ids, context=context) return res def create(self, cursor, user, vals, context=None): res = super(Move, self).create(cursor, user, vals, context=context) if 'lines' in vals: self.validate(cursor, user, [res], context=context) return res def unlink(self, cursor, user, ids, context=None): move_line_obj = self.pool.get('account.move.line') for move in self.browse(cursor, user, ids, context=context): if move.state == 'posted': raise ExceptORM('UserError', 'You can not delete posted move!') if move.lines: move_line_ids = [x.id for x in move.lines] move_line_obj.unlink(cursor, user, move_line_ids, context=context) return super(Move, self).unlink(cursor, user, ids, context=context) def validate(self, cursor, user, ids, context=None): ''' Validate balanced move and centralise it if in centralised journal ''' currency_obj = self.pool.get('account.currency') move_line_obj = self.pool.get('account.move.line') if isinstance(ids, (int, long)): ids = [ids] for move in self.browse(cursor, user, ids, context=context): 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(cursor, user, company.currency, amount): if move.journal.centralised: #TODO centralised move raise Exception('Not implemented') else: move_line_obj.write(cursor, user, [x.id for x in move.lines if x.state != 'draft'], { 'state': 'draft', }, context=context) continue if not draft_lines: continue move_line_obj.write(cursor, user, [x.id for x in draft_lines], { 'state': 'valid', }, context=context) if move.journal.type not in ('expense', 'revenue'): continue #TODO compute tax return def post(self, cursor, user, ids, context=None): currency_obj = self.pool.get('account.currency') sequence_obj = self.pool.get('ir.sequence') moves = self.browse(cursor, user, ids, context=context) for move in moves: amount = Decimal('0.0') 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(cursor, user, company.currency, amount): raise ExceptORM('UserError', 'You can not post a unbalanced move!') for move in moves: reference = sequence_obj.get_id(cursor, user, move.journal.sequence.id) self.write(cursor, user, move.id, { 'reference': reference, 'state': 'posted', }, context=context) return def draft(self, cursor, user, ids, context=None): for move in self.browse(cursor, user, ids, context=context): if not move.journal.update_posted: raise ExceptORM('UserError', 'You can not modify a posted move in this journal!') return self.write(cursor, user, ids, { 'state': 'draft', }, context=context) def button_post(self, cursor, user, ids, context=None): return self.post(cursor, user, ids, context=None) def button_draft(self, cursor, user, ids, context=None): return self.draft(cursor, user, ids, context=context) Move() class Line(OSV): 'Account Move Line' _name = 'account.move.line' _description = __doc__ _order = 'id DESC' name = fields.Char('Name', size=None, required=True) debit = fields.Numeric('Debit', digits=(16, 2)) credit = fields.Numeric('Credit', digits=(16, 2)) account = fields.Many2One('account.account', 'Account', required=True, domain=[('type', '!=', 'view'), ('type', '!=', 'closed')], select=1) move = fields.Many2One('account.move', 'Move', states=_LINE_STATES, select=1, required=True) journal = fields.Function('get_move_field', fnct_inv='set_move_field', type='many2one', relation='account.journal', string='Journal') period = fields.Function('get_move_field', fnct_inv='set_move_field', type='many2one', relation='account.period', string='Period') date = fields.Function('get_move_field', fnct_inv='set_move_field', type='date', string='Effective Date', required=True) reference = fields.char('Reference', size=None) amount_second_currency = fields.Numeric('Amount Second Currency', digits=(16, 2), help='The amount expressed in a second currency') second_currency = fields.Many2One('account.currency', 'Second Currency', help='The second currency') partner = fields.Many2One('partner.partner', 'Partner', on_change=['move', 'partner', 'account', 'debit', 'credit', 'journal']) blocked = fields.Boolean('Litigation', help='Mark the line as litigation with the partner.') maturity_date = fields.Date('Muturity Date', help='This field is used for payable and receivable linees. \n' \ 'You can put the limit date for the payment.') state = fields.Selection([ ('draft', 'Draft'), ('valid', 'Valid'), ], 'State', readonly=True, required=True) active = fields.Boolean('Active', select=2) #TODO add reconcile def __init__(self): super(Line, self).__init__() self._sql_constraints += [ ('credit_debit', 'CHECK((credit * debit = 0) AND (credit + debit >= 0))', 'Wrong credit/debit values!'), ] self._constraints += [ ('check_account', 'You can not create move line \n' \ 'on view/closed/inactive account!', ['account']), ] self._rpc_allowed += [ 'on_write', ] def default_date(self, cursor, user, context=None): ''' Return the date of the last line for journal, period or the starting date of the period or today ''' if context is None: context = {} period_obj = self.pool.get('account.period') res = datetime.date.today() if context.get('journal') and context.get('period'): line_ids = self.search(cursor, user, [ ('journal', '=', context['journal']), ('period', '=', context['period']), ], order='id DESC', limit=1, context=context) if line_ids: line = self.browse(cursor, user, line_ids[0], context=context) res = line.date else: period = period_obj.browse(cursor, user, context['period'], context=context) res = period.start_date return res def default_state(self, cursor, user, context=None): return 'draft' def default_blocked(self, cursor, user, context=None): return False def default_active(self, cursor, user, context=None): return True def default_get(self, cursor, user, fields, context=None): if context is None: context = {} move_obj = self.pool.get('account.move') values = super(Line, self).default_get(cursor, user, fields, context=context) if 'move' not in fields: #Not manual entry return values if context.get('journal') and context.get('period'): line_ids = self.search(cursor, user, [ ('move.journal', '=', context['journal']), ('move.period', '=', context['period']), ('create_uid', '=', user), ('state', '=', 'draft'), ], order='id DESC', limit=1, context=context) if not line_ids: return values line = self.browse(cursor, user, line_ids[0], context=context) values['move'] = line.move.id if 'move' not in values: return values move = move_obj.browse(cursor, user, values['move'], context=context) total = Decimal('0.0') for line in move.lines: total += line.debit - line.credit if line.partner and 'partner' in fields: values.setdefault('partner', line.partner.id) if 'reference' in fields: values.setdefault('reference', line.reference) if 'name' in fields: values.setdefault('name', line.name) #TODO taxes if 'account' in fields: if total >= 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 values['account']: #TODO add taxes code pass if move.journal.type in ('expense', 'revenue'): #TODO taxes pass #Compute last line 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) return values def on_change_partner(self, cursor, user, ids, vals, context=None): partner_obj = self.pool.get('partner.partner') if (not vals.get('partner')) or vals.get('account'): return {} partner = partner_obj.browse(cursor, user, vals.get('partner'), context=context) #TODO add property on partner return {} def get_move_field(self, cursor, user, ids, name, arg, context=None): if name not in ('period', 'journal', 'date'): raise Exception('Invalid name') obj = self.pool.get('account.' + name) res = {} for line in self.browse(cursor, user, ids, context=context): if name in ('date',): res[line.id] = line.move[name] else: res[line.id] = line.move[name].id if name in ('date',): return res obj_names = {} for obj_id, obj_name in obj.name_get(cursor, user, [x for x in res.values() if x], context=context): obj_names[obj_id] = obj_name for i in res.keys(): if res[i] and res[i] in obj_names: res[i] = (res[i], obj_names[res[i]]) else: res[i] = False return res def set_move_field(self, cursor, user, id, name, value, arg, context=None): if name not in ('period', 'journal', 'date'): raise Exception('Invalid name') move_obj = self.pool.get('account.move') line = self.browse(cursor, user, id, context=context) move_obj.write(cursor, user, line.move.id, { name: value, }, context=context) def query_get(self, cursor, user, obj='l', context=None): ''' Return SQL clause for account move line. obj is the SQL alias of the account_move_line in the query. ''' fiscalyear_obj = self.pool.get('account.fiscalyear') if context is None: context = {} if not context.get('fiscalyear', False): fiscalyear_ids = fiscalyear_obj.search(cursor, user, [ ('state', '=', 'open'), ], context=context) fiscalyear_clause = (','.join([str(x) for x in fiscalyear_ids])) or 'False' else: fiscalyear_clause = '%s' % int(context.get('fiscalyear')) if context.get('periods', False): ids = ','.join([str(int(x)) for x in context['periods']]) 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 id IN (' + 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 + ')' \ ')' \ ')' def on_write(self, cursor, user, id, context=None): line = self.browse(cursor, user, id, context) return [x.id for x in line.move.lines] def check_account(self, cursor, user, ids): for line in self.browse(cursor, user, ids): if line.account.type in ('view', 'closed'): return False if not line.account.active: return False return True def check_journal_period_modify(self, cursor, user, period_id, journal_id, context=None): ''' Check if the lines can be modified or created for the journal - period and if there is no journal - period, create it ''' journal_period_obj = self.pool.get('account.journal.period') journal_obj = self.pool.get('account.journal') period_obj = self.pool.get('account.period') journal_period_ids = journal_period_obj.search(cursor, user, [ ('journal', '=', journal_id), ('period', '=', period_id), ], limit=1, context=context) if journal_period_ids: journal_period = journal_period_obj.browse(cursor, user, journal_period_ids[0], context=context) if journal_period.state == 'close': raise ExceptORM('UserError', 'You can not add/modify lines \n' \ 'in a closed journal period!') else: journal = journal_obj.browse(cursor, user, journal_id, context=context) period = period_obj.browse(cursor, user, period_id, context=context) journal_period_obj.create(cursor, user, { 'name': journal.name + ' - ' + period.name, 'journal': journal.id, 'period': period.id, }, context=context) return def check_modify(self, cursor, user, ids, context=None): ''' Check if the lines can be modified ''' journal_period_done = [] for line in self.browse(cursor, user, ids, context=context): if line.move.state == 'posted': raise ExceptORM('UserError', 'You can not modify line from a posted move!') #TODO add reconcile journal_period = (line.journal.id, line.period.id) if journal_period not in journal_period_done: self.check_journal_period_modify(cursor, user, line.period.id, line.journal.id, context=context) journal_period_done.append(journal_period) return def unlink(self, cursor, user, ids, context=None): move_obj = self.pool.get('account.move') self.check_modify(cursor, user, ids, context=context) lines = self.browse(cursor, user, ids, context=context) move_ids = [x.move.id for x in lines] res = super(Line, self).unlink(cursor, user, ids, context=context) move_obj.validate(cursor, user, move_ids, context=context) return res def write(self, cursor, user, ids, vals, context=None): move_obj = self.pool.get('account.move') self.check_modify(cursor, user, ids, context=context) lines = self.browse(cursor, user, ids, context=context) move_ids = [x.move.id for x in lines] res = super(Line, self).write(cursor, user, ids, vals, context=context) lines = self.browse(cursor, user, ids, context=context) for line in lines: if line.move.id not in move_ids: move_ids.append(line.move.id) move_obj.validate(cursor, user, move_ids, context=context) return res def create(self, cursor, user, vals, context=None): if context is None: context = {} journal_obj = self.pool.get('account.journal') move_obj = self.pool.get('account.move') vals = vals.copy() if not vals.get('move'): journal_id = vals.get('journal', context.get('journal')) if not journal_id: raise ExceptORM('Error', 'No journal defined!') journal = journal_obj.browse(cursor, user, journal_id, context=context) if journal.centralised: #TODO journalised raise Exception('Not implemented') else: vals['move'] = move_obj.create(cursor, user, { 'period': vals.get('period', context.get('period')), 'journal': vals.get('journal', context.get('journal')), }, context=context) res = super(Line, self).create(cursor, user, vals, context=context) line = self.browse(cursor, user, res, context=context) self.check_journal_period_modify(cursor, user, line.period.id, line.journal.id, context=context) move_obj.validate(cursor, user, [vals['move']], context=context) return res def view_header_get(self, cursor, user, view_id=None, view_type='form', context=None): if context is None: context = {} journal_obj = self.pool.get('account.journal') period_obj = self.pool.get('account.period') if not context.get('journal') or not context.get('period'): return False journal = journal_obj.browse(cursor, user, context['journal'], context=context) period = period_obj.browse(cursor, user, context['period'], context=context) if journal and period: return journal.name + ': ' + period.name return False def fields_view_get(self, cursor, user, view_id=None, view_type='form', context=None, toolbar=False, hexmd5=None): if context is None: context = {} journal_obj = self.pool.get('account.journal') result = super(Line, self).fields_view_get(cursor, user, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, hexmd5=hexmd5) if view_type == 'tree' and 'journal' in context: title = self.view_header_get(cursor, user, view_id=view_id, view_type=view_type, context=context) journal = journal_obj.browse(cursor, user, context['journal'], context=context) if not journal.view: return result xml = '\n' \ '\n' fields = [] for column in journal.view.columns: fields.append(column.field.name) attrs = [] if column.field.name == 'debit': attrs.append('sum="Total debit"') elif column.field.name == 'credit': attrs.append('sum="Total 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)) xml += '' result['arch'] = xml result['fields'] = self.fields_get(cursor, user, fields_names=fields, context=context) #TODO add hexmd5 return result Line() class OpenJournalInit(WizardOSV): _name = 'account.move.open_journal.init' journal = fields.Many2One('account.journal', 'Journal', required=True) period = fields.Many2One('account.period', 'Period', required=True) def default_period(self, cursor, user, context=None): period_obj = self.pool.get('account.period') return period_obj.find(cursor, user, exception=False, context=context) OpenJournalInit() class OpenJournal(Wizard): 'Open Journal' _name = 'account.move.open_journal' states = { 'init': { 'result': { 'type': 'form', 'object': 'account.move.open_journal.init', 'state': [ ('end', 'Cancel', 'gtk-cancel'), ('open', 'Open', 'gtk-ok', True), ], }, }, 'open': { 'result': { 'type': 'action', 'action': '_action_open_journal', 'state': 'end', }, }, } def _action_open_journal(self, cursor, user, data, context=None): journal_period_obj = self.pool.get('account.journal.period') journal_obj = self.pool.get('account.journal') period_obj = self.pool.get('account.period') if not journal_period_obj.search(cursor, user, [ ('journal', '=', data['form']['journal']), ('period', '=', data['form']['period']), ], context=context): journal = journal_obj.browse(cursor, user, data['form']['journal'], context=context) period = period_obj.browse(cursor, user, data['form']['period'], context=context) journal_period_obj.create(cursor, user, { 'name': journal.name + ' - ' + period.name, 'journal': journal.id, 'period': period.id, }, context=context) return { 'domain': str([ ('journal', '=', data['form']['journal']), ('period', '=', data['form']['period']), ]), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move.line', 'type': 'ir.action.act_window', 'context': str({ 'journal': data['form']['journal'], 'period': data['form']['period'], }), } OpenJournal()