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