typo
[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 from lxml import etree
24
25 from osv import fields, osv
26 from tools.translate import _
27 import decimal_precision as dp
28
29 class account_analytic_account(osv.osv):
30     _name = 'account.analytic.account'
31     _inherit = ['mail.thread']
32     _description = 'Analytic Account'
33
34     def _compute_level_tree(self, cr, uid, ids, child_ids, res, field_names, context=None):
35         currency_obj = self.pool.get('res.currency')
36         recres = {}
37         def recursive_computation(account):
38             result2 = res[account.id].copy()
39             for son in account.child_ids:
40                 result = recursive_computation(son)
41                 for field in field_names:
42                     if (account.currency_id.id != son.currency_id.id) and (field!='quantity'):
43                         result[field] = currency_obj.compute(cr, uid, son.currency_id.id, account.currency_id.id, result[field], context=context)
44                     result2[field] += result[field]
45             return result2
46         for account in self.browse(cr, uid, ids, context=context):
47             if account.id not in child_ids:
48                 continue
49             recres[account.id] = recursive_computation(account)
50         return recres
51
52     def _debit_credit_bal_qtty(self, cr, uid, ids, fields, arg, context=None):
53         res = {}
54         if context is None:
55             context = {}
56         child_ids = tuple(self.search(cr, uid, [('parent_id', 'child_of', ids)]))
57         for i in child_ids:
58             res[i] =  {}
59             for n in fields:
60                 res[i][n] = 0.0
61
62         if not child_ids:
63             return res
64
65         where_date = ''
66         where_clause_args = [tuple(child_ids)]
67         if context.get('from_date', False):
68             where_date += " AND l.date >= %s"
69             where_clause_args  += [context['from_date']]
70         if context.get('to_date', False):
71             where_date += " AND l.date <= %s"
72             where_clause_args += [context['to_date']]
73         cr.execute("""
74               SELECT a.id,
75                      sum(
76                          CASE WHEN l.amount > 0
77                          THEN l.amount
78                          ELSE 0.0
79                          END
80                           ) as debit,
81                      sum(
82                          CASE WHEN l.amount < 0
83                          THEN -l.amount
84                          ELSE 0.0
85                          END
86                           ) as credit,
87                      COALESCE(SUM(l.amount),0) AS balance,
88                      COALESCE(SUM(l.unit_amount),0) AS quantity
89               FROM account_analytic_account a
90                   LEFT JOIN account_analytic_line l ON (a.id = l.account_id)
91               WHERE a.id IN %s
92               """ + where_date + """
93               GROUP BY a.id""", where_clause_args)
94         for row in cr.dictfetchall():
95             res[row['id']] = {}
96             for field in fields:
97                 res[row['id']][field] = row[field]
98         return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
99
100     def name_get(self, cr, uid, ids, context=None):
101         if isinstance(ids, (int, long)):
102             ids=[ids]
103         if not ids:
104             return []
105         res = []
106         for account in self.browse(cr, uid, ids, context=context):
107             data = []
108             acc = account
109             while acc:
110                 data.insert(0, acc.name)
111                 acc = acc.parent_id
112             data = ' / '.join(data)
113             res.append((account.id, data))
114         return res
115
116     def _complete_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
117         res = self.name_get(cr, uid, ids)
118         return dict(res)
119
120     def _child_compute(self, cr, uid, ids, name, arg, context=None):
121         result = {}
122         if context is None:
123             context = {}
124
125         for account in self.browse(cr, uid, ids, context=context):
126             result[account.id] = map(lambda x: x.id, [child for child in account.child_ids if child.state != 'template'])
127
128         return result
129
130     def _get_analytic_account(self, cr, uid, ids, context=None):
131         company_obj = self.pool.get('res.company')
132         analytic_obj = self.pool.get('account.analytic.account')
133         accounts = []
134         for company in company_obj.browse(cr, uid, ids, context=context):
135             accounts += analytic_obj.search(cr, uid, [('company_id', '=', company.id)])
136         return accounts
137
138     def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None):
139         if isinstance(ids, (int, long)):
140             ids=[ids]
141         for account in self.browse(cr, uid, ids, context=context):
142             if account.company_id:
143                 if account.company_id.currency_id.id != value:
144                     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."))
145         return cr.execute("""update account_analytic_account set currency_id=%s where id=%s""", (value, account.id, ))
146
147     def _currency(self, cr, uid, ids, field_name, arg, context=None):
148         result = {}
149         for rec in self.browse(cr, uid, ids, context=context):
150             if rec.company_id:
151                 result[rec.id] = rec.company_id.currency_id.id
152             else:
153                 result[rec.id] = rec.currency_id.id
154         return result
155
156     _columns = {
157         'name': fields.char('Account Name', size=128, required=True),
158         'complete_name': fields.function(_complete_name_calc, type='char', string='Full Account Name'),
159         'code': fields.char('Code/Reference', size=24, select=True),
160         'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Project')], 'Type of Account', help='If you select the View Type, it means you won\'t allow to create journal entries using that account.'),
161         'description': fields.text('Description'),
162         'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
163         'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
164         'child_complete_ids': fields.function(_child_compute, relation='account.analytic.account', string="Account Hierarchy", type='many2many'),
165         'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
166         'balance': fields.function(_debit_credit_bal_qtty, type='float', string='Balance', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
167         'debit': fields.function(_debit_credit_bal_qtty, type='float', string='Debit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
168         'credit': fields.function(_debit_credit_bal_qtty, type='float', string='Credit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
169         'quantity': fields.function(_debit_credit_bal_qtty, type='float', string='Quantity', multi='debit_credit_bal_qtty'),
170         'quantity_max': fields.float('Maximum Time', help='Sets the higher limit of time to work on the contract.'),
171         'partner_id': fields.many2one('res.partner', 'Customer'),
172         'user_id': fields.many2one('res.users', 'Project Manager'),
173         'manager_id': fields.many2one('res.users', 'Account Manager'),
174         'date_start': fields.date('Date Start'),
175         'date': fields.date('Date End', select=True),
176         '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.
177         'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','To Renew'),('close','Closed')], 'Status', required=True,),
178         'currency_id': fields.function(_currency, fnct_inv=_set_company_currency,
179             store = {
180                 'res.company': (_get_analytic_account, ['currency_id'], 10),
181             }, string='Currency', type='many2one', relation='res.currency'),
182     }
183     
184     def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}):
185         res={}
186         if partner_id:
187             part = self.pool.get('res.partner').browse(cr, uid, partner_id,context=context)
188             if part.user_id:res['manager_id'] = part.user_id.id
189         return {'value': res}
190
191     def _default_company(self, cr, uid, context=None):
192         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
193         if user.company_id:
194             return user.company_id.id
195         return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
196
197     def _get_default_currency(self, cr, uid, context=None):
198         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
199         return user.company_id.currency_id.id
200
201     _defaults = {
202         'type': 'normal',
203         'company_id': _default_company,
204         'code' : lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account'),
205         'state': 'open',
206         'user_id': lambda self, cr, uid, ctx: uid,
207         'partner_id': lambda self, cr, uid, ctx: ctx.get('partner_id', False),
208         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
209         'currency_id': _get_default_currency,
210     }
211
212     def check_recursion(self, cr, uid, ids, context=None, parent=None):
213         return super(account_analytic_account, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
214
215     _order = 'name asc'
216     _constraints = [
217         (check_recursion, 'Error! You can not create recursive analytic accounts.', ['parent_id']),
218     ]
219
220     def copy(self, cr, uid, id, default=None, context=None):
221         if not default:
222             default = {}
223         default['code'] = False
224         default['line_ids'] = []
225         return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
226
227     def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
228         if context is None:context = {}
229         
230         res = super(account_analytic_account, self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
231         
232         doc = etree.XML(res['arch'])
233         node = []
234         if view_type == 'form':node = doc.xpath("//field[@name='name']")
235         if view_type == 'tree':node = doc.xpath("//field[@name='complete_name']")
236         if node:
237             curr_node = node[0]
238             if context.get('default_type') == 'contract':
239                 curr_node.set('string', 'Contract/Project Name')
240                 curr_node.set('placeholder', 'Contract or Project Name')
241             elif context.get('default_type') == 'template':
242                 curr_node.set('string', 'Template Name')
243                 curr_node.set('placeholder', 'Template Name')
244             else:
245                 curr_node.set('string', 'Account Name')
246                 curr_node.set('placeholder', 'Account Name')
247         res['arch'] = etree.tostring(doc)
248         return res
249
250     def on_change_company(self, cr, uid, id, company_id):
251         if not company_id:
252             return {}
253         currency = self.pool.get('res.company').read(cr, uid, [company_id], ['currency_id'])[0]['currency_id']
254         return {'value': {'currency_id': currency}}
255
256     def on_change_parent(self, cr, uid, id, parent_id):
257         if not parent_id:
258             return {}
259         parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
260         if parent['partner_id']:
261             partner = parent['partner_id'][0]
262         else:
263             partner = False
264         res = {'value': {}}
265         if partner:
266             res['value']['partner_id'] = partner
267         return res
268
269     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
270         if not args:
271             args=[]
272         if context is None:
273             context={}
274         if context.get('current_model') == 'project.project':
275             cr.execute("select analytic_account_id from project_project")
276             project_ids = [x[0] for x in cr.fetchall()]
277             return self.name_get(cr, uid, project_ids, context=context)
278         if name:
279             account = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
280             if not account:
281                 names=map(lambda i : i.strip(),name.split('/'))
282                 for i in range(len(names)):
283                     dom=[('name', operator, names[i])]
284                     if i>0:
285                         dom+=[('id','child_of',account)]
286                     account = self.search(cr, uid, dom, limit=limit, context=context)
287                 newacc = account
288                 while newacc:
289                     newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
290                     account += newacc
291                 if args:
292                     account = self.search(cr, uid, [('id', 'in', account)] + args, limit=limit, context=context)
293         else:
294             account = self.search(cr, uid, args, limit=limit, context=context)
295         return self.name_get(cr, uid, account, context=context)
296
297 account_analytic_account()
298
299
300 class account_analytic_line(osv.osv):
301     _name = 'account.analytic.line'
302     _description = 'Analytic Line'
303
304     _columns = {
305         'name': fields.char('Description', size=256, required=True),
306         'date': fields.date('Date', required=True, select=True),
307         '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')),
308         'unit_amount': fields.float('Quantity', help='Specifies the amount of quantity to count.'),
309         'account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True, domain=[('type','<>','view')]),
310         'user_id': fields.many2one('res.users', 'User'),
311         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
312
313     }
314     _defaults = {
315         'date': lambda *a: time.strftime('%Y-%m-%d'),
316         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
317         'amount': 0.00
318     }
319
320     _order = 'date desc'
321     
322     def _check_no_view(self, cr, uid, ids, context=None):
323         analytic_lines = self.browse(cr, uid, ids, context=context)
324         for line in analytic_lines:
325             if line.account_id.type == 'view':
326                 return False
327         return True
328     
329     _constraints = [
330         (_check_no_view, 'You can not create analytic line on view account.', ['account_id']),
331     ]    
332
333 account_analytic_line()
334
335 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: