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