[FIX] analytic account & project
[odoo/odoo.git] / addons / analytic / project.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 import operator
24
25 from osv import fields, osv
26 import decimal_precision as dp
27
28 #
29 # Object definition
30 #
31
32 class account_analytic_account(osv.osv):
33     _name = 'account.analytic.account'
34     _description = 'Analytic Accounts'
35
36     def _compute_currency_for_level_tree(self, cr, uid, ids, ids2, res, acc_set, context={}):
37         # Handle multi-currency on each level of analytic account
38         # This is a refactoring of _balance_calc computation
39         cr.execute("SELECT a.id, r.currency_id FROM account_analytic_account a INNER JOIN res_company r ON (a.company_id = r.id) where a.id in (%s)" % acc_set)
40         currency= dict(cr.fetchall())
41         res_currency= self.pool.get('res.currency')
42         for id in ids:
43             if id not in ids2:
44                 continue
45             for child in self.search(cr, uid, [('parent_id', 'child_of', [id])]):
46                 if child != id:
47                     res.setdefault(id, 0.0)
48                     if  currency[child]!=currency[id]:
49                         res[id] += res_currency.compute(cr, uid, currency[child], currency[id], res.get(child, 0.0), context=context)
50                     else:
51                         res[id] += res.get(child, 0.0)
52
53         cur_obj = res_currency.browse(cr,uid,currency.values(),context)
54         cur_obj = dict([(o.id, o) for o in cur_obj])
55         for id in ids:
56             if id in ids2:
57                 res[id] = res_currency.round(cr,uid,cur_obj[currency[id]],res.get(id,0.0))
58
59         return dict([(i, res[i]) for i in ids ])
60
61
62     def _credit_calc(self, cr, uid, ids, name, arg, context={}):
63         res = {}
64         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
65         acc_set = ",".join(map(str, ids2))
66
67         for i in ids:
68             res.setdefault(i,0.0)
69
70         if not ids2:
71             return res
72
73         where_date = ''
74         if context.get('from_date',False):
75             where_date += " AND l.date >= '" + context['from_date'] + "'"
76         if context.get('to_date',False):
77             where_date += " AND l.date <= '" + context['to_date'] + "'"
78         cr.execute("SELECT a.id, COALESCE(SUM(l.amount_currency),0) FROM account_analytic_account a LEFT JOIN account_analytic_line l ON (a.id=l.account_id "+where_date+") WHERE l.amount_currency<0 and a.id =ANY(%s) GROUP BY a.id",(ids2,))
79         r = dict(cr.fetchall())
80         return self._compute_currency_for_level_tree(cr, uid, ids, ids2, r, acc_set, context)
81
82     def _debit_calc(self, cr, uid, ids, name, arg, context={}):
83         res = {}
84         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
85         acc_set = ",".join(map(str, ids2))
86
87         for i in ids:
88             res.setdefault(i,0.0)
89
90         if not ids2:
91             return res
92
93         where_date = ''
94         if context.get('from_date',False):
95             where_date += " AND l.date >= '" + context['from_date'] + "'"
96         if context.get('to_date',False):
97             where_date += " AND l.date <= '" + context['to_date'] + "'"
98         cr.execute("SELECT a.id, COALESCE(SUM(l.amount_currency),0) FROM account_analytic_account a LEFT JOIN account_analytic_line l ON (a.id=l.account_id "+where_date+") WHERE l.amount_currency>0 and a.id =ANY(%s) GROUP BY a.id" ,(ids2,))
99         r= dict(cr.fetchall())
100         return self._compute_currency_for_level_tree(cr, uid, ids, ids2, r, acc_set, context)
101
102     def _balance_calc(self, cr, uid, ids, name, arg, context={}):
103         res = {}
104         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
105         acc_set = ",".join(map(str, ids2))
106
107         for i in ids:
108             res.setdefault(i,0.0)
109
110         if not ids2:
111             return res
112
113         where_date = ''
114         if context.get('from_date',False):
115             where_date += " AND l.date >= '" + context['from_date'] + "'"
116         if context.get('to_date',False):
117             where_date += " AND l.date <= '" + context['to_date'] + "'"
118         cr.execute("SELECT a.id, COALESCE(SUM(l.amount_currency),0) FROM account_analytic_account a LEFT JOIN account_analytic_line l ON (a.id=l.account_id "+where_date+") WHERE a.id =ANY(%s) GROUP BY a.id",(ids2,))
119
120         for account_id, sum in cr.fetchall():
121             res[account_id] = sum
122
123         return self._compute_currency_for_level_tree(cr, uid, ids, ids2, res, acc_set, context)
124
125     def _quantity_calc(self, cr, uid, ids, name, arg, context={}):
126         #XXX must convert into one uom
127         res = {}
128         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
129         acc_set = ",".join(map(str, ids2))
130
131         for i in ids:
132             res.setdefault(i,0.0)
133
134         if not ids2:
135             return res
136
137         where_date = ''
138         if context.get('from_date',False):
139             where_date += " AND l.date >= '" + context['from_date'] + "'"
140         if context.get('to_date',False):
141             where_date += " AND l.date <= '" + context['to_date'] + "'"
142
143         cr.execute('SELECT a.id, COALESCE(SUM(l.unit_amount), 0) \
144                 FROM account_analytic_account a \
145                     LEFT JOIN account_analytic_line l ON (a.id = l.account_id ' + where_date + ') \
146                 WHERE a.id =ANY(%s) GROUP BY a.id',(ids2,))
147
148         for account_id, sum in cr.fetchall():
149             res[account_id] = sum
150
151         for id in ids:
152             if id not in ids2:
153                 continue
154             for child in self.search(cr, uid, [('parent_id', 'child_of', [id])]):
155                 if child != id:
156                     res.setdefault(id, 0.0)
157                     res[id] += res.get(child, 0.0)
158         return dict([(i, res[i]) for i in ids])
159
160     def name_get(self, cr, uid, ids, context={}):
161         if not len(ids):
162             return []
163         reads = self.read(cr, uid, ids, ['name','parent_id'], context)
164         res = []
165         for record in reads:
166             name = record['name']
167             if record['parent_id']:
168                 name = record['parent_id'][1]+' / '+name
169             res.append((record['id'], name))
170         return res
171
172     def _complete_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
173         res = self.name_get(cr, uid, ids)
174         return dict(res)
175
176     def _get_company_currency(self, cr, uid, ids, field_name, arg, context={}):
177         result = {}
178         for rec in self.browse(cr, uid, ids, context):
179             result[rec.id] = (rec.company_id.currency_id.id,rec.company_id.currency_id.code) or False
180         return result
181
182     def _get_account_currency(self, cr, uid, ids, field_name, arg, context={}):
183         result=self._get_company_currency(cr, uid, ids, field_name, arg, context={})
184         return result
185
186     _columns = {
187         'name' : fields.char('Account Name', size=128, required=True),
188         'complete_name': fields.function(_complete_name_calc, method=True, type='char', string='Full Account Name'),
189         'code' : fields.char('Account Code', size=24),
190         'type': fields.selection([('view','View'), ('normal','Normal')], 'Account Type'),
191         'description' : fields.text('Description'),
192         'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
193         'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
194         'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
195         'balance' : fields.function(_balance_calc, method=True, type='float', string='Balance',store=True),
196         'debit' : fields.function(_debit_calc, method=True, type='float', string='Debit',store=True),
197         'credit' : fields.function(_credit_calc, method=True, type='float', string='Credit',store=True),
198         'quantity': fields.function(_quantity_calc, method=True, type='float', string='Quantity',store=True),
199         'quantity_max': fields.float('Maximum Quantity', help='Sets the higher limit of quantity of hours.'),
200         'partner_id' : fields.many2one('res.partner', 'Associated Partner'),
201         'contact_id' : fields.many2one('res.partner.address', 'Contact'),
202         'user_id' : fields.many2one('res.users', 'Account Manager'),
203         'date_start': fields.date('Date Start'),
204         'date': fields.date('Date End'),
205         'company_id': fields.many2one('res.company', 'Company', required=True),
206         'company_currency_id': fields.function(_get_company_currency, method=True, type='many2one', relation='res.currency', string='Currency'),
207         'state': fields.selection([('draft','Draft'),('open','Open'), ('pending','Pending'),('cancelled', 'Cancelled'),('close','Closed'),('template', 'Template')], 'State', required=True,readonly=True,
208                                   help='* When an account is created its in \'Draft\' state.\
209                                   \n* If any associated partner is there, it can be in \'Open\' state.\
210                                   \n* If any pending balance is there it can be in \'Pending\'. \
211                                   \n* And finally when all the transactions are over, it can be in \'Close\' state. \
212                                   \n* The project can be in either if the states \'Template\' and \'Running\'.\n If it is template then we can make projects based on the template projects. If its in \'Running\' state it is a normal project.\
213                                  \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'),
214        'currency_id': fields.function(_get_account_currency, method=True, type='many2one', relation='res.currency', string='Account currency', store=True),
215     }
216
217     def _default_company(self, cr, uid, context={}):
218         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
219         if user.company_id:
220             return user.company_id.id
221         return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
222     _defaults = {
223         'type' : lambda *a : 'normal',
224         'company_id': _default_company,
225         'state' : lambda *a : 'open',
226         'user_id' : lambda self,cr,uid,ctx : uid,
227         'partner_id': lambda self,cr, uid, ctx: ctx.get('partner_id', False),
228         'contact_id': lambda self,cr, uid, ctx: ctx.get('contact_id', False),
229                 'date_start': lambda *a: time.strftime('%Y-%m-%d')
230     }
231
232     def check_recursion(self, cr, uid, ids, parent=None):
233         return super(account_analytic_account, self).check_recursion(cr, uid, ids, parent=parent)
234
235     _order = 'parent_id desc,code'
236     _constraints = [
237         (check_recursion, 'Error! You can not create recursive analytic accounts.', ['parent_id'])
238     ]
239
240     def create(self, cr, uid, vals, context=None):
241         parent_id = vals.get('parent_id', 0)
242         if ('code' not in vals or not vals['code']) and not parent_id:
243             vals['code'] = self.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account')
244         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
245
246     def copy(self, cr, uid, id, default=None, context={}):
247         if not default:
248             default = {}
249         default['code'] = False
250         default['line_ids'] = []
251         return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
252
253
254     def on_change_parent(self, cr, uid, id, parent_id):
255         if not parent_id:
256             return {}
257         parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
258         childs = self.search(cr, uid, [('parent_id', '=', parent_id)])
259         numchild = len(childs)
260         if parent['partner_id']:
261             partner = parent['partner_id'][0]
262         else:
263             partner = False
264         res = {'value' : {'code' : '%s - %03d' % (parent['code'] or '', numchild + 1),}}
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 not context:
273             context={}
274         account = self.search(cr, uid, [('code', '=', name)]+args, limit=limit, context=context)
275         if not account:
276             account = self.search(cr, uid, [('name', 'ilike', '%%%s%%' % name)]+args, limit=limit, context=context)
277             newacc = account
278             while newacc:
279                 newacc = self.search(cr, uid, [('parent_id', 'in', newacc)]+args, limit=limit, context=context)
280                 account+=newacc
281         return self.name_get(cr, uid, account, context=context)
282
283 account_analytic_account()
284
285
286 class account_analytic_line(osv.osv):
287     _name = 'account.analytic.line'
288     _description = 'Analytic lines'
289     def _amount_currency(self, cr, uid, ids, field_name, arg, context={}):
290         result = {}
291         for rec in self.browse(cr, uid, ids, context):
292             cmp_cur_id=rec.company_id.currency_id.id
293             aa_cur_id=rec.account_id.currency_id.id
294             # Always provide the amount in currency
295             if cmp_cur_id != aa_cur_id:
296                 cur_obj = self.pool.get('res.currency')
297                 ctx = {}
298                 if rec.date and rec.amount:
299                     ctx['date'] = rec.date
300                     result[rec.id] = cur_obj.compute(cr, uid, rec.company_id.currency_id.id,
301                         rec.account_id.currency_id.id, rec.amount,
302                         context=ctx)
303             else:
304                 result[rec.id]=rec.amount
305         return result
306         
307     def _get_account_currency(self, cr, uid, ids, field_name, arg, context={}):
308         result = {}
309         for rec in self.browse(cr, uid, ids, context):
310             # Always provide second currency
311             result[rec.id] = (rec.account_id.currency_id.id,rec.account_id.currency_id.code)
312         return result
313     def _get_account_line(self, cr, uid, ids, context={}):
314         aac_ids = {}
315         for acc in self.pool.get('account.analytic.account').browse(cr, uid, ids):
316             aac_ids[acc.id] = True
317         aal_ids = []
318         if aac_ids:
319             aal_ids = self.pool.get('account.analytic.line').search(cr, uid, [('account_id','in',aac_ids.keys())], context=context)
320         return aal_ids
321
322     _columns = {
323         'name' : fields.char('Description', size=256, required=True),
324         'date' : fields.date('Date', required=True),
325         'amount' : fields.float('Amount', required=True, help='Calculated by multiplying the quantity and the price given in the Product\'s cost price.'),
326         'unit_amount' : fields.float('Quantity', help='Specifies the amount of quantity to count.'),
327         'account_id' : fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True),
328         'user_id' : fields.many2one('res.users', 'User',),
329         'company_id': fields.many2one('res.company','Company',required=True),
330         'currency_id': fields.function(_get_account_currency, method=True, type='many2one', relation='res.currency', string='Account currency',
331                 store={
332                     'account.analytic.account': (_get_account_line, ['company_id'], 50),
333                     'account.analytic.line': (lambda self,cr,uid,ids,c={}: ids, ['amount','unit_amount'],10),
334                 },
335                 help="The related account currency if not equal to the company one."),
336         'amount_currency': fields.function(_amount_currency, method=True, digits_compute= dp.get_precision('Account'), string='Amount currency',
337                 store={
338                     'account.analytic.account': (_get_account_line, ['company_id'], 50),
339                     'account.analytic.line': (lambda self,cr,uid,ids,c={}: ids, ['amount','unit_amount'],10),
340                 },
341                 help="The amount expressed in the related account currency if not equal to the company one."),
342
343     }
344     _defaults = {
345         'date': lambda *a: time.strftime('%Y-%m-%d'),
346         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', c),
347     }
348     _order = 'date'
349 account_analytic_line()
350
351