diff --git a/account_move_template/__init__.py b/account_move_template/__init__.py new file mode 100644 index 00000000..c60d936a --- /dev/null +++ b/account_move_template/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import account_document_template +import account_move_template +import wizard diff --git a/account_move_template/__openerp__.py b/account_move_template/__openerp__.py new file mode 100644 index 00000000..2400975f --- /dev/null +++ b/account_move_template/__openerp__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name': "Account Move Template", + 'version': '0.1', + 'category': 'Generic Modules/Accounting', + 'summary': "Templates for recurring Journal Entries", + 'description': """ +Templates for Journal Entries + +User can configure journal entries templates, useful for recurring entries. +The amount of each template line can be computed (through python code) or kept as user input. +If user input, when using the template, user has to fill the amount of every input lines. +The journal entry form allows lo load, through a wizard, the template to use and the amounts to fill. + +""", + 'author': 'Agile Business Group', + 'website': 'http://www.agilebg.com', + 'license': 'AGPL-3', + 'depends': ['account_accountant', 'analytic'], + 'data': [ + 'move_template.xml', + 'wizard/select_template.xml', + 'security/ir.model.access.csv', + ], + 'test': [ + 'test/generate_move.yml', + ], + 'active': False, + 'installable': True, +} diff --git a/account_move_template/account_document_template.py b/account_move_template/account_document_template.py new file mode 100644 index 00000000..18f03df5 --- /dev/null +++ b/account_move_template/account_document_template.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import fields, orm +from openerp.tools.translate import _ +import re + + +class account_document_template(orm.Model): + + _computed_lines = {} + _current_template_id = 0 + _cr = None + _uid = None + _name = 'account.document.template' + + _columns = { + 'name': fields.char('Name', size=64, required=True), + } + + def _input_lines(self, cr, uid, template): + count = 0 + for line in template.template_line_ids: + if line.type == 'input': + count += 1 + return count + + def _get_template_line(self, cr, uid, template_id, line_number): + for line in self.browse(cr, uid, template_id).template_line_ids: + if line.sequence == line_number: + return line + return False + + def _generate_empty_lines(self, cr, uid, template_id): + lines = {} + for template_line in self.browse(cr, uid, template_id).template_line_ids: + lines[template_line.sequence] = None + return lines + + def lines(self, line_number): + if self._computed_lines[line_number] is not None: + return self._computed_lines[line_number] + line = self._get_template_line(self._cr, self._uid, self._current_template_id, line_number) + if re.match('L\( *' + str(line_number) + ' *\)', line.python_code): + raise orm.except_orm( + _('Error'), + _('Line %s can\'t refer to itself') % str(line_number) + ) + try: + self._computed_lines[line_number] = eval(line.python_code.replace('L', 'self.lines')) + except KeyError: + raise orm.except_orm( + _('Error'), + _('Code "%s" refers to non existing line') % line.python_code) + return self._computed_lines[line_number] + + def compute_lines(self, cr, uid, template_id, input_lines): + # input_lines: dictionary in the form {line_number: line_amount} + # returns all the lines (included input lines) in the form {line_number: line_amount} + template = self.browse(cr, uid, template_id) + if len(input_lines) != self._input_lines(cr, uid, template): + raise orm.except_orm( + _('Error'), + _('Inconsistency between input lines and filled lines for template %s') % template.name + ) + self._current_template_id = template.id + self._cr = cr + self._uid = uid + self._computed_lines = self._generate_empty_lines(cr, uid, template_id) + self._computed_lines.update(input_lines) + for line_number in self._computed_lines: + self.lines(line_number) + return self._computed_lines + + def check_zero_lines(self, cr, uid, wizard): + if not wizard.line_ids: + return True + for template_line in wizard.line_ids: + if template_line.amount: + return True + return False + + +class account_document_template_line(orm.Model): + + _name = 'account.document.template.line' + + _columns = { + 'name': fields.char('Name', size=64, required=True), + 'sequence': fields.integer('Sequence', required=True), + 'type': fields.selection([('computed', 'Computed'), ('input', 'User input')], 'Type', required=True), + 'python_code': fields.text('Python Code'), + } diff --git a/account_move_template/account_move_template.py b/account_move_template/account_move_template.py new file mode 100644 index 00000000..49546031 --- /dev/null +++ b/account_move_template/account_move_template.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import fields, orm + + +class account_move_template(orm.Model): + + _inherit = 'account.document.template' + _name = 'account.move.template' + + _columns = { + 'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True), + 'template_line_ids': fields.one2many('account.move.template.line', 'template_id', 'Template Lines'), + 'cross_journals': fields.boolean('Cross-Journals'), + 'transitory_acc_id': fields.many2one('account.account', 'Transitory account', required=False), + } + + _defaults = { + 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get( + cr, uid, 'account.move.template', context=c + ), + } + + def _check_different_journal(self, cr, uid, ids, context=None): + #Check that the journal on these lines are different/same in the case of cross journals/single journal + journal_ids = [] + all_journal_ids = [] + move_template = self.pool.get('account.move.template').browse(cr, uid, ids)[0] + if not move_template.template_line_ids: + return True + for template_line in move_template.template_line_ids: + all_journal_ids.append(template_line.journal_id.id) + if template_line.journal_id.id not in journal_ids: + journal_ids.append(template_line.journal_id.id) + if move_template.cross_journals: + return len(all_journal_ids) == len(journal_ids) + else: + return len(journal_ids) == 1 + + _constraints = [ + (_check_different_journal, + 'If the template is "cross-journals", the Journals must be different,' + 'if the template does not "cross-journals" the Journals must be the same!', + ['journal_id']) + ] + + +class account_move_template_line(orm.Model): + _name = 'account.move.template.line' + _inherit = 'account.document.template.line' + + _columns = { + 'journal_id': fields.many2one('account.journal', 'Journal', required=True), + 'account_id': fields.many2one('account.account', 'Account', + required=True, ondelete="cascade"), + 'move_line_type': fields.selection( + [('cr', 'Credit'), + ('dr', 'Debit')], + 'Move Line Type', + required=True + ), + 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete="cascade"), + 'template_id': fields.many2one('account.move.template', 'Template'), + 'account_tax_id': fields.many2one('account.tax', 'Tax'), + } + + _sql_constraints = [ + ('sequence_template_uniq', 'unique (template_id,sequence)', + 'The sequence of the line must be unique per template !') + ] diff --git a/account_move_template/wizard/__init__.py b/account_move_template/wizard/__init__.py new file mode 100644 index 00000000..3a53db9e --- /dev/null +++ b/account_move_template/wizard/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import select_template diff --git a/account_move_template/wizard/select_template.py b/account_move_template/wizard/select_template.py new file mode 100644 index 00000000..53906793 --- /dev/null +++ b/account_move_template/wizard/select_template.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 Agile Business Group sagl () +# Copyright (C) 2011 Domsense srl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import fields, orm +import time +from openerp.tools.translate import _ + + +class wizard_select_template(orm.TransientModel): + + _name = "wizard.select.move.template" + _columns = { + 'template_id': fields.many2one( + 'account.move.template', + 'Move Template', + required=True + ), + 'partner_id': fields.many2one('res.partner', 'Partner'), + + 'line_ids': fields.one2many( + 'wizard.select.move.template.line', + 'template_id', + 'Lines' + ), + + 'state': fields.selection( + [ + ('template_selected', 'Template selected'), + ], + 'State' + ), + } + + def on_change_template_id(self, cr, uid, ids, template_id): + res = {} + if template_id: + res['value'] = {'line_ids': []} + template_pool = self.pool.get('account.move.template') + template = template_pool.browse(cr, uid, template_id) + for line in template.template_line_ids: + if line.type == 'input': + res['value']['line_ids'].append({ + 'sequence': line.sequence, + 'name': line.name, + 'account_id': line.account_id.id, + 'move_line_type': line.move_line_type, + }) + return res + + def load_lines(self, cr, uid, ids, context=None): + wizard = self.browse(cr, uid, ids, context=context)[0] + template_pool = self.pool.get('account.move.template') + wizard_line_pool = self.pool.get('wizard.select.move.template.line') + model_data_obj = self.pool.get('ir.model.data') + + template = template_pool.browse(cr, uid, wizard.template_id.id) + for line in template.template_line_ids: + if line.type == 'input': + wizard_line_pool.create(cr, uid, { + 'template_id': wizard.id, + 'sequence': line.sequence, + 'name': line.name, + 'amount': 0.0, + 'account_id': line.account_id.id, + 'move_line_type': line.move_line_type, + }) + if not wizard.line_ids: + return self.load_template(cr, uid, ids) + wizard.write({'state': 'template_selected'}) + + view_rec = model_data_obj.get_object_reference(cr, uid, 'account_move_template', 'wizard_select_template') + view_id = view_rec and view_rec[1] or False + + return { + 'view_type': 'form', + 'view_id': [view_id], + 'view_mode': 'form', + 'res_model': 'wizard.select.move.template', + 'res_id': wizard.id, + 'type': 'ir.actions.act_window', + 'target': 'new', + 'context': context, + } + + def load_template(self, cr, uid, ids, context=None): + template_obj = self.pool.get('account.move.template') + account_period_obj = self.pool.get('account.period') + + wizard = self.browse(cr, uid, ids, context=context)[0] + if not template_obj.check_zero_lines(cr, uid, wizard): + raise orm.except_orm(_('Error !'), _('At least one amount has to be non-zero!')) + input_lines = {} + + for template_line in wizard.line_ids: + input_lines[template_line.sequence] = template_line.amount + + period_id = account_period_obj.find(cr, uid, context=context) + if not period_id: + raise orm.except_orm(_('No period found !'), _('Unable to find a valid period !')) + period_id = period_id[0] + + computed_lines = template_obj.compute_lines(cr, uid, wizard.template_id.id, input_lines) + + moves = {} + for line in wizard.template_id.template_line_ids: + if line.journal_id.id not in moves: + moves[line.journal_id.id] = self._make_move( + cr, uid, + wizard.template_id.name, + period_id, + line.journal_id.id, + wizard.partner_id.id + ) + + self._make_move_line( + cr, uid, + line, + computed_lines, + moves[line.journal_id.id], + period_id, + wizard.partner_id.id + ) + if wizard.template_id.cross_journals: + trans_account_id = wizard.template_id.transitory_acc_id.id + self._make_transitory_move_line( + cr, + uid, + line, + computed_lines, + moves[line.journal_id.id], + period_id, + trans_account_id, + wizard.partner_id.id + ) + + return { + 'domain': "[('id','in', " + str(moves.values()) + ")]", + 'name': 'Entries', + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'target': 'current', + } + + def _make_move(self, cr, uid, ref, period_id, journal_id, partner_id): + account_move_obj = self.pool.get('account.move') + move_id = account_move_obj.create(cr, uid, { + 'ref': ref, + 'period_id': period_id, + 'journal_id': journal_id, + 'partner_id': partner_id, + }) + return move_id + + def _make_move_line(self, cr, uid, line, computed_lines, move_id, period_id, partner_id): + account_move_line_obj = self.pool.get('account.move.line') + analytic_account_id = False + if line.analytic_account_id: + if not line.journal_id.analytic_journal_id: + raise orm.except_orm( + _('No Analytic Journal !'), + _("You have to dfine an analytic journal on the '%s' journal!") + % (line.journal_id.name,) + ) + + analytic_account_id = line.analytic_account_id.id + val = { + 'name': line.name, + 'move_id': move_id, + 'journal_id': line.journal_id.id, + 'period_id': period_id, + 'analytic_account_id': analytic_account_id, + 'account_id': line.account_id.id, + 'date': time.strftime('%Y-%m-%d'), + 'account_tax_id': line.account_tax_id.id, + 'credit': 0.0, + 'debit': 0.0, + 'partner_id': partner_id, + } + if line.move_line_type == 'cr': + val['credit'] = computed_lines[line.sequence] + if line.move_line_type == 'dr': + val['debit'] = computed_lines[line.sequence] + id_line = account_move_line_obj.create(cr, uid, val) + return id_line + + def _make_transitory_move_line(self, cr, uid, line, + computed_lines, move_id, period_id, + trans_account_id, partner_id): + account_move_line_obj = self.pool.get('account.move.line') + analytic_account_id = False + if line.analytic_account_id: + if not line.journal_id.analytic_journal_id: + raise orm.except_orm( + _('No Analytic Journal !'), + _("You have to define an analytic journal on the '%s' journal!") + % (line.template_id.journal_id.name,) + ) + analytic_account_id = line.analytic_account_id.id + val = { + 'name': 'transitory', + 'move_id': move_id, + 'journal_id': line.journal_id.id, + 'period_id': period_id, + 'analytic_account_id': analytic_account_id, + 'account_id': trans_account_id, + 'date': time.strftime('%Y-%m-%d'), + 'partner_id': partner_id, + } + if line.move_line_type != 'cr': + val['credit'] = computed_lines[line.sequence] + if line.move_line_type != 'dr': + val['debit'] = computed_lines[line.sequence] + id_line = account_move_line_obj.create(cr, uid, val) + return id_line + + +class wizard_select_template_line(orm.TransientModel): + _description = 'Template Lines' + _name = "wizard.select.move.template.line" + _columns = { + 'template_id': fields.many2one('wizard.select.move.template', 'Template'), + 'sequence': fields.integer('Number', required=True), + 'name': fields.char('Name', size=64, required=True, readonly=True), + 'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True), + 'move_line_type': fields.selection( + [('cr', 'Credit'), + ('dr', 'Debit')], + 'Move Line Type', + required=True, + readonly=True + ), + 'amount': fields.float('Amount', required=True), + }