8dd73f6e7674a08e85e78aae7482a85b647cb262
[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         full_ids = []
102         if isinstance(ids,list):
103             full_ids.extend(ids)
104         else:
105             full_ids.append(ids)
106         for id in full_ids:
107             elmt = self.browse(cr, uid, id, context=context)
108             res.append((id, self._get_one_full_name(elmt)))
109         return res
110
111     def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
112         if context == None:
113             context = {}
114         res = {}
115         for elmt in self.browse(cr, uid, ids, context=context):
116             res[elmt.id] = self._get_one_full_name(elmt)
117         return res
118
119     def _get_one_full_name(self, elmt, level=6):
120         if level<=0:
121             return '...'
122         if elmt.parent_id:
123             parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/"
124         else:
125             parent_path = ''
126         return parent_path + elmt.name
127
128     def _child_compute(self, cr, uid, ids, name, arg, context=None):
129         result = {}
130         if context is None:
131             context = {}
132
133         for account in self.browse(cr, uid, ids, context=context):
134             result[account.id] = map(lambda x: x.id, [child for child in account.child_ids if child.state != 'template'])
135
136         return result
137
138     def _get_analytic_account(self, cr, uid, ids, context=None):
139         company_obj = self.pool.get('res.company')
140         analytic_obj = self.pool.get('account.analytic.account')
141         accounts = []
142         for company in company_obj.browse(cr, uid, ids, context=context):
143             accounts += analytic_obj.search(cr, uid, [('company_id', '=', company.id)])
144         return accounts
145
146     def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None):
147         if isinstance(ids, (int, long)):
148             ids=[ids]
149         for account in self.browse(cr, uid, ids, context=context):
150             if account.company_id:
151                 if account.company_id.currency_id.id != value:
152                     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."))
153         if value:
154             return cr.execute("""update account_analytic_account set currency_id=%s where id=%s""", (value, account.id, ))
155
156     def _currency(self, cr, uid, ids, field_name, arg, context=None):
157         result = {}
158         for rec in self.browse(cr, uid, ids, context=context):
159             if rec.company_id:
160                 result[rec.id] = rec.company_id.currency_id.id
161             else:
162                 result[rec.id] = rec.currency_id.id
163         return result
164
165     _columns = {
166         'name': fields.char('Account/Contract Name', size=128, required=True),
167         'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
168         'code': fields.char('Reference', size=24, select=True),
169         'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
170                                  help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\
171                                   "The type 'Analytic account' stands for usual accounts that you only want to use in accounting.\n"\
172                                   "If you select Contract or Project, it offers you the possibility to manage the validity and the invoicing options for this account.\n"\
173                                   "The special type 'Template of Contract' allows you to define a template with default data that you can reuse easily."),
174         'template_id': fields.many2one('account.analytic.account', 'Template of Contract'),
175         'description': fields.text('Description'),
176         'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
177         'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
178         'child_complete_ids': fields.function(_child_compute, relation='account.analytic.account', string="Account Hierarchy", type='many2many'),
179         'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
180         'balance': fields.function(_debit_credit_bal_qtty, type='float', string='Balance', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
181         'debit': fields.function(_debit_credit_bal_qtty, type='float', string='Debit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
182         'credit': fields.function(_debit_credit_bal_qtty, type='float', string='Credit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
183         'quantity': fields.function(_debit_credit_bal_qtty, type='float', string='Quantity', multi='debit_credit_bal_qtty'),
184         '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.)'),
185         'partner_id': fields.many2one('res.partner', 'Customer'),
186         'user_id': fields.many2one('res.users', 'Project Manager'),
187         'manager_id': fields.many2one('res.users', 'Account Manager'),
188         'date_start': fields.date('Start Date'),
189         'date': fields.date('Date End', select=True),
190         '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.
191         'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','To Renew'),('close','Closed')], 'Status', required=True,),
192         '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
193             store = {
194                 'res.company': (_get_analytic_account, ['currency_id'], 10),
195             }, string='Currency', type='many2one', relation='res.currency'),
196     }
197
198     def on_change_template(self, cr, uid, ids, template_id, context=None):
199         if not template_id:
200             return {}
201         res = {'value':{}}
202         template = self.browse(cr, uid, template_id, context=context)
203         res['value']['date_start'] = template.date_start
204         res['value']['date'] = template.date
205         res['value']['quantity_max'] = template.quantity_max
206         res['value']['description'] = template.description
207         return res
208
209     def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}):
210         res={}
211         if partner_id:
212             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
213             if partner.user_id:
214                 res['manager_id'] = partner.user_id.id
215             if not name:
216                 res['name'] = _('Contract: ') + partner.name
217         return {'value': res}
218
219     def _default_company(self, cr, uid, context=None):
220         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
221         if user.company_id:
222             return user.company_id.id
223         return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
224
225     def _get_default_currency(self, cr, uid, context=None):
226         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
227         return user.company_id.currency_id.id
228
229     _defaults = {
230         'type': 'normal',
231         'company_id': _default_company,
232         'code' : lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account'),
233         'state': 'open',
234         'user_id': lambda self, cr, uid, ctx: uid,
235         'partner_id': lambda self, cr, uid, ctx: ctx.get('partner_id', False),
236         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
237         'currency_id': _get_default_currency,
238     }
239
240     def check_recursion(self, cr, uid, ids, context=None, parent=None):
241         return super(account_analytic_account, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
242
243     _order = 'name asc'
244     _constraints = [
245         (check_recursion, 'Error! You cannot create recursive analytic accounts.', ['parent_id']),
246     ]
247
248     def copy(self, cr, uid, id, default=None, context=None):
249         if not default:
250             default = {}
251         analytic = self.browse(cr, uid, id, context=context)
252         default.update(
253             code=False,
254             line_ids=[],
255             name=_("%s (copy)") % (analytic['name']))
256         return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
257
258     def on_change_company(self, cr, uid, id, company_id):
259         if not company_id:
260             return {}
261         currency = self.pool.get('res.company').read(cr, uid, [company_id], ['currency_id'])[0]['currency_id']
262         return {'value': {'currency_id': currency}}
263
264     def on_change_parent(self, cr, uid, id, parent_id):
265         if not parent_id:
266             return {}
267         parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
268         if parent['partner_id']:
269             partner = parent['partner_id'][0]
270         else:
271             partner = False
272         res = {'value': {}}
273         if partner:
274             res['value']['partner_id'] = partner
275         return res
276
277     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
278         if not args:
279             args=[]
280         if context is None:
281             context={}
282         if context.get('current_model') == 'project.project':
283             project_obj = self.pool.get("account.analytic.account")
284             project_ids = project_obj.search(cr, uid, args)
285             return self.name_get(cr, uid, project_ids, context=context)
286         if name:
287             account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
288             if not account_ids:
289                 names=map(lambda i : i.strip(),name.split('/'))
290                 for i in range(len(names)):
291                     dom=[('name', operator, names[i])]
292                     if i>0:
293                         dom+=[('id','child_of',account_ids)]
294                     account_ids = self.search(cr, uid, dom, limit=limit, context=context)
295                 newacc = account_ids
296                 while newacc:
297                     newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
298                     account_ids += newacc
299                 if args:
300                     account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
301         else:
302             account_ids = self.search(cr, uid, args, limit=limit, context=context)
303         return self.name_get(cr, uid, account_ids, context=context)
304
305     def create(self, cr, uid, vals, context=None):
306         contract =  super(account_analytic_account, self).create(cr, uid, vals, context=context)
307         if contract:
308             self.create_send_note(cr, uid, [contract], context=context)
309         return contract
310
311     def create_send_note(self, cr, uid, ids, context=None):
312         for obj in self.browse(cr, uid, ids, context=context):
313             message = _("Contract <b>created</b>.")
314             if obj.partner_id:
315                 message = _("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name,)
316             self.message_post(cr, uid, [obj.id], body=message,
317                 subtype="analytic.mt_account_status", context=context)
318
319 account_analytic_account()
320
321
322 class account_analytic_line(osv.osv):
323     _name = 'account.analytic.line'
324     _description = 'Analytic Line'
325
326     _columns = {
327         'name': fields.char('Description', size=256, required=True),
328         'date': fields.date('Date', required=True, select=True),
329         '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')),
330         'unit_amount': fields.float('Quantity', help='Specifies the amount of quantity to count.'),
331         'account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='cascade', select=True, domain=[('type','<>','view')]),
332         'user_id': fields.many2one('res.users', 'User'),
333         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
334
335     }
336
337     def _get_default_date(self, cr, uid, context=None):
338         return fields.date.context_today(self, cr, uid, context=context)
339
340     def __get_default_date(self, cr, uid, context=None):
341         return self._get_default_date(cr, uid, context=context)
342
343     _defaults = {
344         'date': __get_default_date,
345         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
346         'amount': 0.00
347     }
348
349     _order = 'date desc'
350
351     def _check_no_view(self, cr, uid, ids, context=None):
352         analytic_lines = self.browse(cr, uid, ids, context=context)
353         for line in analytic_lines:
354             if line.account_id.type == 'view':
355                 return False
356         return True
357
358     _constraints = [
359         (_check_no_view, 'You cannot create analytic line on view account.', ['account_id']),
360     ]
361
362 account_analytic_line()
363
364 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: