[MERGE] useability account
[odoo/odoo.git] / addons / analytic / analytic.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23
24 from osv import fields, osv
25 from tools.translate import _
26 import decimal_precision as dp
27
28 class account_analytic_account(osv.osv):
29     _name = 'account.analytic.account'
30     _inherit = ['mail.thread']
31     _description = 'Analytic Account'
32
33     def _compute_level_tree(self, cr, uid, ids, child_ids, res, field_names, context=None):
34         currency_obj = self.pool.get('res.currency')
35         recres = {}
36         def recursive_computation(account):
37             result2 = res[account.id].copy()
38             for son in account.child_ids:
39                 result = recursive_computation(son)
40                 for field in field_names:
41                     if (account.currency_id.id != son.currency_id.id) and (field!='quantity'):
42                         result[field] = currency_obj.compute(cr, uid, son.currency_id.id, account.currency_id.id, result[field], context=context)
43                     result2[field] += result[field]
44             return result2
45         for account in self.browse(cr, uid, ids, context=context):
46             if account.id not in child_ids:
47                 continue
48             recres[account.id] = recursive_computation(account)
49         return recres
50
51     def _debit_credit_bal_qtty(self, cr, uid, ids, fields, arg, context=None):
52         res = {}
53         if context is None:
54             context = {}
55         child_ids = tuple(self.search(cr, uid, [('parent_id', 'child_of', ids)]))
56         for i in child_ids:
57             res[i] =  {}
58             for n in fields:
59                 res[i][n] = 0.0
60
61         if not child_ids:
62             return res
63
64         where_date = ''
65         where_clause_args = [tuple(child_ids)]
66         if context.get('from_date', False):
67             where_date += " AND l.date >= %s"
68             where_clause_args  += [context['from_date']]
69         if context.get('to_date', False):
70             where_date += " AND l.date <= %s"
71             where_clause_args += [context['to_date']]
72         cr.execute("""
73               SELECT a.id,
74                      sum(
75                          CASE WHEN l.amount > 0
76                          THEN l.amount
77                          ELSE 0.0
78                          END
79                           ) as debit,
80                      sum(
81                          CASE WHEN l.amount < 0
82                          THEN -l.amount
83                          ELSE 0.0
84                          END
85                           ) as credit,
86                      COALESCE(SUM(l.amount),0) AS balance,
87                      COALESCE(SUM(l.unit_amount),0) AS quantity
88               FROM account_analytic_account a
89                   LEFT JOIN account_analytic_line l ON (a.id = l.account_id)
90               WHERE a.id IN %s
91               """ + where_date + """
92               GROUP BY a.id""", where_clause_args)
93         for row in cr.dictfetchall():
94             res[row['id']] = {}
95             for field in fields:
96                 res[row['id']][field] = row[field]
97         return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
98
99     def name_get(self, cr, uid, ids, context=None):
100         res = []
101         for id in ids:
102             elmt = self.browse(cr, uid, id, context=context)
103             res.append((id, self._get_one_full_name(elmt)))
104         return res
105
106     def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
107         if context == None:
108             context = {}
109         res = {}
110         for elmt in self.browse(cr, uid, ids, context=context):
111             res[elmt.id] = self._get_one_full_name(elmt)
112         return res
113
114     def _get_one_full_name(self, elmt, level=6):
115         if level<=0:
116             return '...'
117         if elmt.parent_id:
118             parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/"
119         else:
120             parent_path = ''
121         return parent_path + elmt.name
122
123     def _child_compute(self, cr, uid, ids, name, arg, context=None):
124         result = {}
125         if context is None:
126             context = {}
127
128         for account in self.browse(cr, uid, ids, context=context):
129             result[account.id] = map(lambda x: x.id, [child for child in account.child_ids if child.state != 'template'])
130
131         return result
132
133     def _get_analytic_account(self, cr, uid, ids, context=None):
134         company_obj = self.pool.get('res.company')
135         analytic_obj = self.pool.get('account.analytic.account')
136         accounts = []
137         for company in company_obj.browse(cr, uid, ids, context=context):
138             accounts += analytic_obj.search(cr, uid, [('company_id', '=', company.id)])
139         return accounts
140
141     def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None):
142         if isinstance(ids, (int, long)):
143             ids=[ids]
144         for account in self.browse(cr, uid, ids, context=context):
145             if account.company_id:
146                 if account.company_id.currency_id.id != value:
147                     raise osv.except_osv(_('Error!'), _("If you set a company, the currency selected has to be the same as it's currency. \nYou can remove the company belonging, and thus change the currency, only on analytic account of type 'view'. This can be really usefull for consolidation purposes of several companies charts with different currencies, for example."))
148         if value:
149             return cr.execute("""update account_analytic_account set currency_id=%s where id=%s""", (value, account.id, ))
150
151     def _currency(self, cr, uid, ids, field_name, arg, context=None):
152         result = {}
153         for rec in self.browse(cr, uid, ids, context=context):
154             if rec.company_id:
155                 result[rec.id] = rec.company_id.currency_id.id
156             else:
157                 result[rec.id] = rec.currency_id.id
158         return result
159
160     _columns = {
161         'name': fields.char('Account/Contract Name', size=128, required=True),
162         'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
163         'code': fields.char('Reference', size=24, select=True),
164         'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
165                                  help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\
166                                   "The type 'Analytic account' stands for usual accounts that you only want to use in accounting.\n"\
167                                   "If you select Contract or Project, it offers you the possibility to manage the validity and the invoicing options for this account.\n"\
168                                   "The special type 'Template of Contract' allows you to define a template with default data that you can reuse easily."),
169         'template_id': fields.many2one('account.analytic.account', 'Template of Contract'),
170         'description': fields.text('Description'),
171         'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
172         'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
173         'child_complete_ids': fields.function(_child_compute, relation='account.analytic.account', string="Account Hierarchy", type='many2many'),
174         'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
175         'balance': fields.function(_debit_credit_bal_qtty, type='float', string='Balance', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
176         'debit': fields.function(_debit_credit_bal_qtty, type='float', string='Debit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
177         'credit': fields.function(_debit_credit_bal_qtty, type='float', string='Credit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
178         'quantity': fields.function(_debit_credit_bal_qtty, type='float', string='Quantity', multi='debit_credit_bal_qtty'),
179         'quantity_max': fields.float('Prepaid Service Units', help='Sets the higher limit of time to work on the contract, based on the timesheet. (for instance, number of hours in a limited support contract.)'),
180         'partner_id': fields.many2one('res.partner', 'Customer'),
181         'user_id': fields.many2one('res.users', 'Project Manager'),
182         'manager_id': fields.many2one('res.users', 'Account Manager'),
183         'date_start': fields.date('Start Date'),
184         'date': fields.date('Date End', select=True),
185         'company_id': fields.many2one('res.company', 'Company', required=False), #not required because we want to allow different companies to use the same chart of account, except for leaf accounts.
186         'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','To Renew'),('close','Closed')], 'Status', required=True,),
187         'currency_id': fields.function(_currency, fnct_inv=_set_company_currency, #the currency_id field is readonly except if it's a view account and if there is no company
188             store = {
189                 'res.company': (_get_analytic_account, ['currency_id'], 10),
190             }, string='Currency', type='many2one', relation='res.currency'),
191     }
192
193     def on_change_template(self, cr, uid, ids, template_id, context=None):
194         if not template_id:
195             return {}
196         res = {'value':{}}
197         template = self.browse(cr, uid, template_id, context=context)
198         res['value']['date_start'] = template.date_start
199         res['value']['date'] = template.date
200         res['value']['quantity_max'] = template.quantity_max
201         res['value']['description'] = template.description
202         return res
203
204     def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}):
205         res={}
206         if partner_id:
207             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
208             if partner.user_id:
209                 res['manager_id'] = partner.user_id.id
210             if not name:
211                 res['name'] = _('Contract: ') + partner.name
212         return {'value': res}
213
214     def _default_company(self, cr, uid, context=None):
215         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
216         if user.company_id:
217             return user.company_id.id
218         return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
219
220     def _get_default_currency(self, cr, uid, context=None):
221         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
222         return user.company_id.currency_id.id
223
224     _defaults = {
225         'type': 'normal',
226         'company_id': _default_company,
227         'code' : lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account'),
228         'state': 'open',
229         'user_id': lambda self, cr, uid, ctx: uid,
230         'partner_id': lambda self, cr, uid, ctx: ctx.get('partner_id', False),
231         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
232         'currency_id': _get_default_currency,
233     }
234
235     def check_recursion(self, cr, uid, ids, context=None, parent=None):
236         return super(account_analytic_account, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
237
238     _order = 'name asc'
239     _constraints = [
240         (check_recursion, 'Error! You cannot create recursive analytic accounts.', ['parent_id']),
241     ]
242
243     def copy(self, cr, uid, id, default=None, context=None):
244         if not default:
245             default = {}
246         analytic = self.browse(cr, uid, id, context=context)
247         default.update(
248             code=False,
249             line_ids=[],
250             name=_("%s (copy)") % (analytic['name']))
251         return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
252
253     def on_change_company(self, cr, uid, id, company_id):
254         if not company_id:
255             return {}
256         currency = self.pool.get('res.company').read(cr, uid, [company_id], ['currency_id'])[0]['currency_id']
257         return {'value': {'currency_id': currency}}
258
259     def on_change_parent(self, cr, uid, id, parent_id):
260         if not parent_id:
261             return {}
262         parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
263         if parent['partner_id']:
264             partner = parent['partner_id'][0]
265         else:
266             partner = False
267         res = {'value': {}}
268         if partner:
269             res['value']['partner_id'] = partner
270         return res
271
272     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
273         if not args:
274             args=[]
275         if context is None:
276             context={}
277         if context.get('current_model') == 'project.project':
278             project_obj = self.pool.get("account.analytic.account")
279             project_ids = project_obj.search(cr, uid, args)
280             return self.name_get(cr, uid, project_ids, context=context)
281         if name:
282             account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
283             if not account_ids:
284                 names=map(lambda i : i.strip(),name.split('/'))
285                 for i in range(len(names)):
286                     dom=[('name', operator, names[i])]
287                     if i>0:
288                         dom+=[('id','child_of',account_ids)]
289                     account_ids = self.search(cr, uid, dom, limit=limit, context=context)
290                 newacc = account_ids
291                 while newacc:
292                     newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
293                     account_ids += newacc
294                 if args:
295                     account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
296         else:
297             account_ids = self.search(cr, uid, args, limit=limit, context=context)
298         return self.name_get(cr, uid, account_ids, context=context)
299
300     def create(self, cr, uid, vals, context=None):
301         contract =  super(account_analytic_account, self).create(cr, uid, vals, context=context)
302         if contract:
303             self.create_send_note(cr, uid, [contract], context=context)
304         return contract
305
306     def create_send_note(self, cr, uid, ids, context=None):
307         for obj in self.browse(cr, uid, ids, context=context):
308             self.message_post(cr, uid, [obj.id], body=_("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name),
309                 subtype="analytic.mt_account_status", context=context)
310
311 account_analytic_account()
312
313
314 class account_analytic_line(osv.osv):
315     _name = 'account.analytic.line'
316     _description = 'Analytic Line'
317
318     _columns = {
319         'name': fields.char('Description', size=256, required=True),
320         'date': fields.date('Date', required=True, select=True),
321         'amount': fields.float('Amount', required=True, help='Calculated by multiplying the quantity and the price given in the Product\'s cost price. Always expressed in the company main currency.', digits_compute=dp.get_precision('Account')),
322         'unit_amount': fields.float('Quantity', help='Specifies the amount of quantity to count.'),
323         'account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True, domain=[('type','<>','view')]),
324         'user_id': fields.many2one('res.users', 'User'),
325         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
326
327     }
328
329     def _get_default_date(self, cr, uid, context=None):
330         return fields.date.context_today(self, cr, uid, context=context)
331
332     def __get_default_date(self, cr, uid, context=None):
333         return self._get_default_date(cr, uid, context=context)
334
335     _defaults = {
336         'date': __get_default_date,
337         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
338         'amount': 0.00
339     }
340
341     _order = 'date desc'
342
343     def _check_no_view(self, cr, uid, ids, context=None):
344         analytic_lines = self.browse(cr, uid, ids, context=context)
345         for line in analytic_lines:
346             if line.account_id.type == 'view':
347                 return False
348         return True
349
350     _constraints = [
351         (_check_no_view, 'You cannot create analytic line on view account.', ['account_id']),
352     ]
353
354 account_analytic_line()
355
356 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: