commit fe267639822cfd280fee7600c543a5630207c303 Author: Cédric Krier Date: Fri May 23 15:04:38 2008 +0200 Initial commit for analytic_account module diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..e348ce69f2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2004-2006 Tiny SPRL. (http://tiny.be) +# Copyright (c) 2007-2008 Cedric Krier. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contact a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from account import * +from line import * diff --git a/__tryton__.py b/__tryton__.py new file mode 100644 index 0000000000..a7e511d41a --- /dev/null +++ b/__tryton__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Analytic Account', + 'version': '0.1', + 'author': 'B2CK', + 'email': 'info@b2ck.com', + 'website': 'http://www.tryton.org/', + 'category': 'Generic Modules', + 'description': 'Financial and Accounting Module', + 'depends': [ + 'ir', + 'company', + 'currency', + 'account', + 'relationship', + 'res', + ], + 'xml': [ + 'analytic_account.xml', + 'account.xml', + 'line.xml', + ], +} diff --git a/account.py b/account.py new file mode 100644 index 0000000000..67ff5250ae --- /dev/null +++ b/account.py @@ -0,0 +1,270 @@ +"Account" + +from trytond.osv import fields, OSV +from trytond.wizard import Wizard, WizardOSV +from decimal import Decimal + + +class Account(OSV): + 'Analytic Account' + _name = 'analytic_account.account' + _description = __doc__ + + name = fields.Char('Name', required=True, translate=True, select=1) + complete_name = fields.Function('get_complete_name', type='char', + string='Name') + code = fields.Char('Code', select=1) + active = fields.Boolean('Active', select=2) + company = fields.Many2One('company.company', 'Company') + currency = fields.Many2One('currency.currency', 'Currency', required=True) + type = fields.Selection([ + ('view', 'View'), + ('normal', 'Normal'), + ], 'Type', required=True) + parent = fields.Many2One('analytic_account.account', 'Parent', select=2) + childs = fields.One2Many('analytic_account.account', 'parent', 'Childs') + #TODO fix digits depend of the currency + balance = fields.Function('get_balance', digits=(16, 2), string='Balance') + credit = fields.Function('get_credit_debit', digits=(16, 2), string='Credit') + debit = fields.Function('get_credit_debit', digits=(16, 2), string='Debit') + state = fields.Selection([ + ('draft', 'Draft'), + ('opened', 'Opened'), + ('closed', 'Closed'), + ], 'State', required=True) + note = fields.Text('Note') + display_balance = fields.Selection([ + ('debit-credit', 'Debit - Credit'), + ('credit-debit', 'Credit - Debit'), + ], 'Display Balance', required=True) + + def __init__(self): + super(Account, self).__init__() + self._constraints += [ + ('check_recursion', + 'Error! You can not create recursive account.', ['parent']), + ] + self._order.insert(0, ('code', 'ASC')) + + def default_active(self, cursor, user, context=None): + return True + + def default_company(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + if context is None: + context = {} + if context.get('company'): + return company_obj.name_get(cursor, user, context['company'], + context=context)[0] + return False + + def default_currency(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + currency_obj = self.pool.get('currency.currency') + if context is None: + context = {} + if context.get('company'): + company = company_obj.browse(cursor, user, context['company'], + context=context) + return currency_obj.name_get(cursor, user, company.currency.id, + context=context)[0] + return False + + def default_type(self, cursor, user, context=None): + return 'normal' + + def default_state(self, cursor, user, context=None): + return 'draft' + + def default_display_balance(self, cursor, user, context=None): + return 'credit-debit' + + def get_complete_name(self, cursor, user, ids, name, arg, context=None): + res = self.name_get(cursor, user, ids, context=context) + return dict(res) + + def get_balance(self, cursor, user, ids, name, arg, context=None): + res = {} + line_obj = self.pool.get('analytic_account.line') + currency_obj = self.pool.get('currency.currency') + + child_ids = self.search(cursor, user, [('parent', 'child_of', ids)], + context=context) + all_ids = {}.fromkeys(ids + child_ids).keys() + + id2account = {} + accounts = self.browse(cursor, user, all_ids, context=context) + for account in accounts: + id2account[account.id] = account + + line_query = line_obj.query_get(cursor, user, context=context) + cursor.execute('SELECT a.id, ' \ + 'SUM((COALESCE(l.debit, 0) - COALESCE(l.credit, 0))), ' \ + 'l.currency ' \ + 'FROM analytic_account_account a ' \ + 'LEFT JOIN analytic_account_line l ' \ + 'ON (a.id = l.account) ' \ + 'WHERE a.type != \'view\' ' \ + 'AND a.id IN (' + \ + ','.join(['%s' for x in all_ids]) + ') ' \ + 'AND ' + line_query + ' ' \ + 'AND a.active ' \ + 'GROUP BY a.id, l.currency', all_ids) + account_sum = {} + id2currency = {} + for account_id, sum, currency_id in cursor.fetchall(): + account_sum.setdefault(account_id, Decimal('0.0')) + if currency_id != id2account[account_id].currency.id: + currency = None + if currency_id in id2currency: + currency = id2currency[currency_id] + else: + currency = currency_obj.browse(cursor, user, currency_id, + context=context) + id2currency[currency.id] = currency + account_sum[account_id] += currency_obj.compute(cursor, user, + currency, sum, id2account[account_id].currency, + round=True, context=context) + else: + account_sum[account_id] += currency_obj.round(cursor, user, + id2account[account_id].currency, sum) + + for account_id in ids: + res.setdefault(account_id, Decimal('0.0')) + child_ids = self.search(cursor, user, [ + ('parent', 'child_of', [account_id]), + ], context=context) + to_currency = id2account[account_id].currency + for child_id in child_ids: + from_currency = id2account[child_id].currency + res[account_id] += currency_obj.compute(cursor, user, + from_currency, account_sum.get(child_id, Decimal('0.0')), + to_currency, round=True, context=context) + res[account_id] = currency_obj.round(cursor, user, to_currency, + res[account_id]) + if id2account[account_id].display_balance == 'credit-debit': + res[account_id] = - res[account_id] + return res + + def get_credit_debit(self, cursor, user, ids, name, arg, context=None): + res = {} + line_obj = self.pool.get('analytic_account.line') + currency_obj = self.pool.get('currency.currency') + + if name not in ('credit', 'debit'): + raise Exception('Bad argument') + + id2account = {} + accounts = self.browse(cursor, user, ids, context=context) + for account in accounts: + res[account.id] = Decimal('0.0') + id2account[account.id] = account + + line_query = line_obj.query_get(cursor, user, context=context) + cursor.execute('SELECT a.id, ' \ + 'SUM(COALESCE(l.' + name + ', 0)), ' \ + 'l.currency ' \ + 'FROM analytic_account_account a ' \ + 'LEFT JOIN analytic_account_line l ' \ + 'ON (a.id = l.account) ' \ + 'WHERE a.type != \'view\' ' \ + 'AND a.id IN (' + \ + ','.join(['%s' for x in ids]) + ') ' \ + 'AND ' + line_query + ' ' \ + 'AND a.active ' \ + 'GROUP BY a.id, l.currency', ids) + + id2currency = {} + for account_id, sum, currency_id in cursor.fetchall(): + if currency_id != id2account[account_id].currency.id: + currency = None + if currency_id in id2currency: + currency = id2currency[currency_id] + else: + currency = currency_obj.browse(cursor, user, currency_id, + context=context) + id2currency[currency.id] = currency + res[account_id] += currency_obj.compute(cursor, user, + currency, sum, id2account[account_id].currency, + round=True, context=context) + else: + res[account_id] += currency_obj.round(cursor, user, + id2account[account_id].currency, sum) + return res + + def name_search(self, cursor, user, name='', args=None, operator='ilike', + context=None, limit=None): + if name: + ids = self.search(cursor, user, + [('code', 'like', name + '%')] + args, + limit=limit, context=context) + if not ids: + ids = self.search(cursor, user, + [(self._rec_name, operator, name)] + args, + limit=limit, context=context) + else: + ids = self.search(cursor, user, args, limit=limit, context=context) + res = self.name_get(cursor, user, ids, context=context) + return res + + def name_get(self, cursor, user, ids, context=None): + if not ids: + return [] + if isinstance(ids, (int, long)): + ids = [ids] + return [(r['id'], r['code'] and r['code'] + ' - ' + str(r[self._rec_name]) \ + or str(r[self._rec_name])) for r in self.read(cursor, user, ids, + [self._rec_name, 'code'], context=context, load='_classic_write')] + +Account() + + +class OpenChartAccountInit(WizardOSV): + _name = 'analytic_account.account.open_chart_account.init' + start_date = fields.Date('Start Date') + end_date = fields.Date('End Date') + +OpenChartAccountInit() + + +class OpenChartAccount(Wizard): + 'Open Chart Of Account' + _name = 'analytic_account.account.open_chart_account' + states = { + 'init': { + 'result': { + 'type': 'form', + 'object': 'analytic_account.account.open_chart_account.init', + 'state': [ + ('end', 'Cancel', 'gtk-cancel'), + ('open', 'Open', 'gtk-ok', True), + ], + }, + }, + 'open': { + 'result': { + 'type': 'action', + 'action': '_action_open_chart', + 'state': 'end', + }, + }, + } + + def _action_open_chart(self, cursor, user, data, context=None): + model_data_obj = self.pool.get('ir.model.data') + act_window_obj = self.pool.get('ir.action.act_window') + + model_data_ids = model_data_obj.search(cursor, user, [ + ('fs_id', '=', 'act_account_tree2'), + ('module', '=', 'analytic_account'), + ], limit=1, context=context) + model_data = model_data_obj.browse(cursor, user, model_data_ids[0], + context=context) + res = act_window_obj.read(cursor, user, model_data.db_id, context=context) + res['context'] = str({ + 'start_date': data['form']['start_date'], + 'end_date': data['form']['end_date'] + }) + return res + +OpenChartAccount() diff --git a/account.xml b/account.xml new file mode 100644 index 0000000000..235b3f7824 --- /dev/null +++ b/account.xml @@ -0,0 +1,157 @@ + + + + + + + analytic_account.account + form + + + + + analytic_account.account + tree + childs + + + + + + + + + + ]]> + + + + Analytic Accounts + analytic_account.account + form + + + + + + + + + + + + + + Analytic Accounts + analytic_account.account + tree + [('parent', '=', False)] + + + + + + + + + analytic_account.account + tree + childs + + + + + + + + + + + ]]> + + + + Analytic Accounts + analytic_account.account + tree + [('parent', '=', False)] + + + + + + + + Open Chart of Analytic Account + analytic_account.account.open_chart_account + + + + + + + + + + + + + + + + + + + + analytic_account.account.open_chart_account.init + form + + + + + diff --git a/analytic_account.xml b/analytic_account.xml new file mode 100644 index 0000000000..fde953b97d --- /dev/null +++ b/analytic_account.xml @@ -0,0 +1,14 @@ + + + + + Analytic + + + Analytic Admin + + + + + + diff --git a/line.py b/line.py new file mode 100644 index 0000000000..7f9a8a873d --- /dev/null +++ b/line.py @@ -0,0 +1,128 @@ +"Line" + +from trytond.osv import fields, OSV +from trytond.wizard import Wizard +import datetime + + +class Line(OSV): + 'Analytic Line' + _name = 'analytic_account.line' + _description = __doc__ + + name = fields.Char('Name', required=True) + debit = fields.Numeric('Debit', digits=(16, 2)) + credit = fields.Numeric('Credit', digits=(16, 2)) + currency = fields.Many2One('currency.currency', 'Currency', required=True) + account = fields.Many2One('analytic_account.account', 'Account', + required=True, select=1, domain=[('type', '!=', 'view')]) + move_line = fields.Many2One('account.move.line', 'Account Move Line', + ondelete='CASCADE', required=True) + journal = fields.Many2One('account.journal', 'Journal', required=True, + select=1) + date = fields.Date('Date', required=True) + reference = fields.Char('Reference') + party = fields.Many2One('relationship.party', 'Party') + active = fields.Boolean('Active', select=2) + + def __init__(self): + super(Line, self).__init__() + self._sql_constraints += [ + ('credit_debit', + 'CHECK((credit * debit = 0.0) AND (credit + debit >= 0.0))', + 'Wrong credit/debit values!'), + ] + self._constraints += [ + ('check_account', 'You can not create move line \n' \ + 'on view/inactive account!', ['account']), + ] + self._order.insert(0, ('date', 'ASC')) + + def default_currency(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + currency_obj = self.pool.get('currency.currency') + if context is None: + context = {} + if context.get('company'): + company = company_obj.browse(cursor, user, context['company'], + context=context) + return currency_obj.name_get(cursor, user, company.currency.id, + context=context)[0] + return False + + def default_date(self, cursor, user, context=None): + return datetime.date.today() + + def default_active(self, cursor, user, context=None): + return True + + def query_get(self, cursor, user, obj='l', context=None): + ''' + Return SQL clause for analytic line depending of the context. + obj is the SQL alias of the analytic_account_line in the query. + ''' + if context is None: + context = {} + + res = obj + '.active ' + if context.get('start_date'): + res += 'AND ' + obj + '.date >= ' + str(context['start_date']) + if context.get('end_date'): + res += 'AND ' + obj + '.date <= ' + str(context['end_date']) + return res + + def check_account(self, cursor, user, ids): + for line in self.browse(cursor, user, ids): + if line.account.type == 'view': + return False + if not line.account.active: + return False + return True + +Line() + + +class MoveLine(OSV): + _name = 'account.move.line' + analytic_lines = fields.One2Many('analytic_account.line', 'move_line', + 'Analytic Lines') + +MoveLine() + + +class OpenAccount(Wizard): + 'Open Account' + _name = 'analytic_account.line.open_account' + states = { + 'init': { + 'result': { + 'type': 'action', + 'action': '_action_open_account', + 'state': 'end', + }, + }, + } + + def _action_open_account(self, cursor, user, data, context=None): + model_data_obj = self.pool.get('ir.model.data') + act_window_obj = self.pool.get('ir.action.act_window') + + if context is None: + context = {} + + model_data_ids = model_data_obj.search(cursor, user, [ + ('fs_id', '=', 'act_line_form'), + ('module', '=', 'analytic_account'), + ], limit=1, context=context) + model_data = model_data_obj.browse(cursor, user, model_data_ids[0], + context=context) + res = act_window_obj.read(cursor, user, model_data.db_id, context=context) + res['domain'] = [] + + if context.get('start_date'): + res['domain'].append(('date', '>=', context['start_date'])) + if context.get('end_date'): + res['domain'].append(('date', '<=', context['end_date'])) + return res + +OpenAccount() diff --git a/line.xml b/line.xml new file mode 100644 index 0000000000..f0ad17f424 --- /dev/null +++ b/line.xml @@ -0,0 +1,103 @@ + + + + + analytic_account.line + form + + + + + analytic_account.line + tree + + + + + + + + + + + + + + ]]> + + + + Analytic Lines + analytic_account.line + form + + + + + + + + + + + + + Open Account + analytic_account.line.open_account + + + tree_open + analytic_account.account,0 + + + + + account.move.line + + + + + + + + + + + ]]> + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..5aba20bcbf --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +from distutils.core import setup +from distutils.command import sdist + + +class mysdist(sdist.sdist): + + def add_defaults(self): + sdist.sdist.add_defaults(self) + if self.distribution.has_pure_modules(): + build_py = self.get_finalized_command('build_py') + data = [] + for package in build_py.packages: + src_dir = build_py.get_package_dir(package) + data.extend(build_py.find_data_files(package, src_dir)) + self.filelist.extend(data) + +info = eval(file('__tryton__.py').read()) + +requires = ['trytond'] + ['trytond-' + x for x in info.get('depends', [])] + +setup(name='trytond-' + info['name'].lower(), + version=info.get('version', '0'), + description=info.get('description', ''), + author=info.get('author', ''), + author_email=info.get('email', ''), + url=info.get('website', ''), + package_dir={'trytond.modules.' + info['name'].lower(): '.'}, + packages=[ + 'trytond.modules.' + info['name'].lower(), + ], + package_data={ + 'trytond.modules.' + info['name'].lower(): info.get('xml', []), + }, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Plugins', + 'Intended Audience :: Developers', + 'Intended Audience :: Financial and Insurance Industry', + 'Intended Audience :: Legal Industry', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Office/Business', + 'Topic :: Office/Business :: Financial :: Accounting', + ], + license=info.get('license', 'GPL-2'), + #requires=requires, + cmdclass={ + 'sdist': mysdist, + }, +)