[MERGE] Sync with trunk
[odoo/odoo.git] / addons / account_analytic_plans / account_analytic_plans.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 openerp.osv import fields, osv
26 from openerp import tools
27 from openerp.tools.translate import _
28
29 class one2many_mod2(fields.one2many):
30     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
31         if context is None:
32             context = {}
33         res = {}
34         for id in ids:
35             res[id] = []
36         ids2 = None
37         if 'journal_id' in context:
38             journal = obj.pool.get('account.journal').browse(cr, user, context['journal_id'], context=context)
39             pnum = int(name[7]) -1
40             plan = journal.plan_id
41             if plan and len(plan.plan_ids) > pnum:
42                 acc_id = plan.plan_ids[pnum].root_analytic_id.id
43                 ids2 = obj.pool[self._obj].search(cr, user, [(self._fields_id,'in',ids),('analytic_account_id','child_of',[acc_id])], limit=self._limit)
44         if ids2 is None:
45             ids2 = obj.pool[self._obj].search(cr, user, [(self._fields_id,'in',ids)], limit=self._limit)
46         for r in obj.pool[self._obj]._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
47             res[r[self._fields_id]].append( r['id'] )
48         return res
49
50 class account_analytic_line(osv.osv):
51     _inherit = 'account.analytic.line'
52     _description = 'Analytic Line'
53
54     def _get_amount(self, cr, uid, ids, name, args, context=None):
55         res = {}
56         for id in ids:
57             res.setdefault(id, 0.0)
58         for line in self.browse(cr, uid, ids, context=context):
59             amount = line.move_id and line.move_id.amount_currency * (line.percentage / 100) or 0.0
60             res[line.id] = amount
61         return res
62
63     _columns = {
64         'amount_currency': fields.function(_get_amount, string="Amount Currency", type="float", store=True, help="The amount expressed in the related account currency if not equal to the company one.", readonly=True),
65         'percentage': fields.float('Percentage')
66     }
67
68
69 class account_analytic_plan(osv.osv):
70     _name = "account.analytic.plan"
71     _description = "Analytic Plan"
72     _columns = {
73         'name': fields.char('Analytic Plan', size=64, required=True, select=True),
74         'plan_ids': fields.one2many('account.analytic.plan.line', 'plan_id', 'Analytic Plans'),
75     }
76
77
78 class account_analytic_plan_line(osv.osv):
79     _name = "account.analytic.plan.line"
80     _description = "Analytic Plan Line"
81     _order = "sequence, id"
82     _columns = {
83         'plan_id': fields.many2one('account.analytic.plan','Analytic Plan',required=True),
84         'name': fields.char('Plan Name', size=64, required=True, select=True),
85         'sequence': fields.integer('Sequence'),
86         'root_analytic_id': fields.many2one('account.analytic.account', 'Root Account', help="Root account of this plan.", required=False),
87         'min_required': fields.float('Minimum Allowed (%)'),
88         'max_required': fields.float('Maximum Allowed (%)'),
89     }
90     _defaults = {
91         'min_required': 100.0,
92         'max_required': 100.0,
93     }
94
95
96 class account_analytic_plan_instance(osv.osv):
97     _name = "account.analytic.plan.instance"
98     _description = "Analytic Plan Instance"
99     _columns = {
100         'name': fields.char('Analytic Distribution', size=64),
101         'code': fields.char('Distribution Code', size=16),
102         'journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal' ),
103         'account_ids': fields.one2many('account.analytic.plan.instance.line', 'plan_id', 'Account Id'),
104         'account1_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account1 Id'),
105         'account2_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account2 Id'),
106         'account3_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account3 Id'),
107         'account4_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account4 Id'),
108         'account5_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account5 Id'),
109         'account6_ids': one2many_mod2('account.analytic.plan.instance.line', 'plan_id', 'Account6 Id'),
110         'plan_id': fields.many2one('account.analytic.plan', "Model's Plan"),
111     }
112
113     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
114         if context is None:
115             context = {}
116         journal_obj = self.pool.get('account.journal')
117         if context.get('journal_id', False):
118             journal = journal_obj.browse(cr, user, [context['journal_id']], context=context)[0]
119             analytic_journal = journal.analytic_journal_id and journal.analytic_journal_id.id or False
120             args.append('|')
121             args.append(('journal_id', '=', analytic_journal))
122             args.append(('journal_id', '=', False))
123         res = super(account_analytic_plan_instance, self).search(cr, user, args, offset=offset, limit=limit, order=order,
124                                                                  context=context, count=count)
125         return res
126
127     def copy(self, cr, uid, id, default=None, context=None):
128         if not default:
129             default = {}
130         default.update({'account1_ids':False, 'account2_ids':False, 'account3_ids':False,
131                 'account4_ids':False, 'account5_ids':False, 'account6_ids':False})
132         return super(account_analytic_plan_instance, self).copy(cr, uid, id, default, context=context)
133
134     def _default_journal(self, cr, uid, context=None):
135         if context is None:
136             context = {}
137         journal_obj = self.pool.get('account.journal')
138         if context.has_key('journal_id') and context['journal_id']:
139             journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
140             if journal.analytic_journal_id:
141                 return journal.analytic_journal_id.id
142         return False
143
144     _defaults = {
145         'plan_id': False,
146         'journal_id': _default_journal,
147     }
148     def name_get(self, cr, uid, ids, context=None):
149         res = []
150         for inst in self.browse(cr, uid, ids, context=context):
151             name = inst.name or '/'
152             if name and inst.code:
153                 name=name+' ('+inst.code+')'
154             res.append((inst.id, name))
155         return res
156
157     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
158         args = args or []
159         if name:
160             ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context or {})
161             if not ids:
162                 ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context or {})
163         else:
164             ids = self.search(cr, uid, args, limit=limit, context=context or {})
165         return self.name_get(cr, uid, ids, context or {})
166
167     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
168         if context is None:
169             context = {}
170         wiz_id = self.pool.get('ir.actions.act_window').search(cr, uid, [("name","=","analytic.plan.create.model.action")], context=context)
171         res = super(account_analytic_plan_instance,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
172         journal_obj = self.pool.get('account.journal')
173         analytic_plan_obj = self.pool.get('account.analytic.plan')
174         if (res['type']=='form'):
175             plan_id = False
176             if context.get('journal_id', False):
177                 plan_id = journal_obj.browse(cr, uid, int(context['journal_id']), context=context).plan_id
178             elif context.get('plan_id', False):
179                 plan_id = analytic_plan_obj.browse(cr, uid, int(context['plan_id']), context=context)
180
181             if plan_id:
182                 i=1
183                 res['arch'] = """<form string="%s">
184     <field name="name"/>
185     <field name="code"/>
186     <field name="journal_id"/>
187     <button name="%d" string="Save This Distribution as a Model" type="action" colspan="2"/>
188     """% (tools.to_xml(plan_id.name), wiz_id[0])
189                 for line in plan_id.plan_ids:
190                     res['arch']+="""
191                     <field name="account%d_ids" string="%s" nolabel="1" colspan="4">
192                     <tree string="%s" editable="bottom">
193                         <field name="rate"/>
194                         <field name="analytic_account_id" domain="[('parent_id','child_of',[%d])]" groups="analytic.group_analytic_accounting"/>
195                     </tree>
196                 </field>
197                 <newline/>"""%(i,tools.to_xml(line.name),tools.to_xml(line.name),line.root_analytic_id and line.root_analytic_id.id or 0)
198                     i+=1
199                 res['arch'] += "</form>"
200                 doc = etree.fromstring(res['arch'].encode('utf8'))
201                 xarch, xfields = self._view_look_dom_arch(cr, uid, doc, view_id, context=context)
202                 res['arch'] = xarch
203                 res['fields'] = xfields
204             return res
205         else:
206             return res
207
208     def create(self, cr, uid, vals, context=None):
209         journal_obj = self.pool.get('account.journal')
210         ana_plan_instance_obj = self.pool.get('account.analytic.plan.instance')
211         acct_anal_acct = self.pool.get('account.analytic.account')
212         acct_anal_plan_line_obj = self.pool.get('account.analytic.plan.line')
213         if context and 'journal_id' in context:
214             journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
215
216             pids = ana_plan_instance_obj.search(cr, uid, [('name','=',vals['name']), ('code','=',vals['code']), ('plan_id','<>',False)], context=context)
217             if pids:
218                 raise osv.except_osv(_('Error!'), _('A model with this name and code already exists.'))
219
220             res = acct_anal_plan_line_obj.search(cr, uid, [('plan_id','=',journal.plan_id.id)], context=context)
221             for i in res:
222                 total_per_plan = 0
223                 item = acct_anal_plan_line_obj.browse(cr, uid, i, context=context)
224                 temp_list = ['account1_ids','account2_ids','account3_ids','account4_ids','account5_ids','account6_ids']
225                 for l in temp_list:
226                     if vals.has_key(l):
227                         for tempo in vals[l]:
228                             if acct_anal_acct.search(cr, uid, [('parent_id', 'child_of', [item.root_analytic_id.id]), ('id', '=', tempo[2]['analytic_account_id'])], context=context):
229                                 total_per_plan += tempo[2]['rate']
230                 if total_per_plan < item.min_required or total_per_plan > item.max_required:
231                     raise osv.except_osv(_('Error!'),_('The total should be between %s and %s.') % (str(item.min_required), str(item.max_required)))
232
233         return super(account_analytic_plan_instance, self).create(cr, uid, vals, context=context)
234
235     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
236         if context is None:
237             context = {}
238         this = self.browse(cr, uid, ids[0], context=context)
239         invoice_line_obj = self.pool.get('account.invoice.line')
240         if this.plan_id and not vals.has_key('plan_id'):
241             #this instance is a model, so we have to create a new plan instance instead of modifying it
242             #copy the existing model
243             temp_id = self.copy(cr, uid, this.id, None, context=context)
244             #get the list of the invoice line that were linked to the model
245             lists = invoice_line_obj.search(cr, uid, [('analytics_id','=',this.id)], context=context)
246             #make them link to the copy
247             invoice_line_obj.write(cr, uid, lists, {'analytics_id':temp_id}, context=context)
248
249             #and finally modify the old model to be not a model anymore
250             vals['plan_id'] = False
251             if not vals.has_key('name'):
252                 vals['name'] = this.name and (str(this.name)+'*') or "*"
253             if not vals.has_key('code'):
254                 vals['code'] = this.code and (str(this.code)+'*') or "*"
255         return super(account_analytic_plan_instance, self).write(cr, uid, ids, vals, context=context)
256
257
258 class account_analytic_plan_instance_line(osv.osv):
259     _name = "account.analytic.plan.instance.line"
260     _description = "Analytic Instance Line"
261     _rec_name = "analytic_account_id"
262     _columns = {
263         'plan_id': fields.many2one('account.analytic.plan.instance', 'Plan Id'),
264         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, domain=[('type','<>','view')]),
265         'rate': fields.float('Rate (%)', required=True),
266     }
267     _defaults = {
268         'rate': 100.0
269     }
270     def name_get(self, cr, uid, ids, context=None):
271         if not ids:
272             return []
273         reads = self.read(cr, uid, ids, ['analytic_account_id'], context=context)
274         res = []
275         for record in reads:
276             res.append((record['id'], record['analytic_account_id']))
277         return res
278
279
280 class account_journal(osv.osv):
281     _inherit = "account.journal"
282     _name = "account.journal"
283     _columns = {
284         'plan_id': fields.many2one('account.analytic.plan', 'Analytic Plans'),
285     }
286
287
288 class account_invoice_line(osv.osv):
289     _inherit = "account.invoice.line"
290     _name = "account.invoice.line"
291     _columns = {
292         'analytics_id': fields.many2one('account.analytic.plan.instance', 'Analytic Distribution'),
293     }
294
295     def create(self, cr, uid, vals, context=None):
296         if 'analytics_id' in vals and isinstance(vals['analytics_id'], tuple):
297             vals['analytics_id'] = vals['analytics_id'][0]
298         return super(account_invoice_line, self).create(cr, uid, vals, context=context)
299
300     def move_line_get_item(self, cr, uid, line, context=None):
301         res = super(account_invoice_line, self).move_line_get_item(cr, uid, line, context=context)
302         res ['analytics_id'] = line.analytics_id and line.analytics_id.id or False
303         return res
304
305     def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
306         res_prod = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom_id, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context, company_id=company_id)
307         rec = self.pool.get('account.analytic.default').account_get(cr, uid, product, partner_id, uid, time.strftime('%Y-%m-%d'), context=context)
308         if rec and rec.analytics_id:
309             res_prod['value'].update({'analytics_id': rec.analytics_id.id})
310         return res_prod
311
312
313 class account_move_line(osv.osv):
314
315     _inherit = "account.move.line"
316     _name = "account.move.line"
317     _columns = {
318         'analytics_id':fields.many2one('account.analytic.plan.instance', 'Analytic Distribution'),
319     }
320
321     def _default_get_move_form_hook(self, cursor, user, data):
322         data = super(account_move_line, self)._default_get_move_form_hook(cursor, user, data)
323         if data.has_key('analytics_id'):
324             del(data['analytics_id'])
325         return data
326
327     def create_analytic_lines(self, cr, uid, ids, context=None):
328         if context is None:
329             context = {}
330         super(account_move_line, self).create_analytic_lines(cr, uid, ids, context=context)
331         analytic_line_obj = self.pool.get('account.analytic.line')
332         for line in self.browse(cr, uid, ids, context=context):
333            if line.analytics_id:
334                if not line.journal_id.analytic_journal_id:
335                    raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal.") % (line.journal_id.name,))
336
337                toremove = analytic_line_obj.search(cr, uid, [('move_id','=',line.id)], context=context)
338                if toremove:
339                     analytic_line_obj.unlink(cr, uid, toremove, context=context)
340                for line2 in line.analytics_id.account_ids:
341                    val = (line.credit or  0.0) - (line.debit or 0.0)
342                    amt=val * (line2.rate/100)
343                    al_vals={
344                        'name': line.name,
345                        'date': line.date,
346                        'account_id': line2.analytic_account_id.id,
347                        'unit_amount': line.quantity,
348                        'product_id': line.product_id and line.product_id.id or False,
349                        'product_uom_id': line.product_uom_id and line.product_uom_id.id or False,
350                        'amount': amt,
351                        'general_account_id': line.account_id.id,
352                        'move_id': line.id,
353                        'journal_id': line.journal_id.analytic_journal_id.id,
354                        'ref': line.ref,
355                        'percentage': line2.rate
356                    }
357                    analytic_line_obj.create(cr, uid, al_vals, context=context)
358         return True
359
360     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
361         if context is None:
362             context = {}
363         result = super(account_move_line, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
364         return result
365
366
367 class account_invoice(osv.osv):
368     _name = "account.invoice"
369     _inherit = "account.invoice"
370
371     def line_get_convert(self, cr, uid, x, part, date, context=None):
372         res=super(account_invoice,self).line_get_convert(cr, uid, x, part, date, context=context)
373         res['analytics_id'] = x.get('analytics_id', False)
374         return res
375
376     def _get_analytic_lines(self, cr, uid, id, context=None):
377         inv = self.browse(cr, uid, [id])[0]
378         cur_obj = self.pool.get('res.currency')
379         invoice_line_obj = self.pool.get('account.invoice.line')
380         acct_ins_obj = self.pool.get('account.analytic.plan.instance')
381         company_currency = inv.company_id.currency_id.id
382         if inv.type in ('out_invoice', 'in_refund'):
383             sign = 1
384         else:
385             sign = -1
386
387         iml = invoice_line_obj.move_line_get(cr, uid, inv.id, context=context)
388
389         for il in iml:
390             if il.get('analytics_id', False):
391
392                 if inv.type in ('in_invoice', 'in_refund'):
393                     ref = inv.reference
394                 else:
395                     ref = self._convert_ref(cr, uid, inv.number)
396                 obj_move_line = acct_ins_obj.browse(cr, uid, il['analytics_id'], context=context)
397                 ctx = context.copy()
398                 ctx.update({'date': inv.date_invoice})
399                 amount_calc = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context=ctx) * sign
400                 qty = il['quantity']
401                 il['analytic_lines'] = []
402                 for line2 in obj_move_line.account_ids:
403                     amt = amount_calc * (line2.rate/100)
404                     qtty = qty* (line2.rate/100)
405                     al_vals = {
406                         'name': il['name'],
407                         'date': inv['date_invoice'],
408                         'unit_amount': qtty,
409                         'product_id': il['product_id'],
410                         'account_id': line2.analytic_account_id.id,
411                         'amount': amt,
412                         'product_uom_id': il['uos_id'],
413                         'general_account_id': il['account_id'],
414                         'journal_id': self._get_journal_analytic(cr, uid, inv.type),
415                         'ref': ref,
416                     }
417                     il['analytic_lines'].append((0, 0, al_vals))
418         return iml
419
420
421 class account_analytic_plan(osv.osv):
422     _inherit = "account.analytic.plan"
423     _columns = {
424         'default_instance_id': fields.many2one('account.analytic.plan.instance', 'Default Entries'),
425     }
426
427 class analytic_default(osv.osv):
428     _inherit = "account.analytic.default"
429     _columns = {
430         'analytics_id': fields.many2one('account.analytic.plan.instance', 'Analytic Distribution'),
431     }
432
433
434 class sale_order_line(osv.osv):
435     _inherit = "sale.order.line"
436
437     # Method overridden to set the analytic account by default on criterion match
438     def invoice_line_create(self, cr, uid, ids, context=None):
439         create_ids = super(sale_order_line,self).invoice_line_create(cr, uid, ids, context=context)
440         inv_line_obj = self.pool.get('account.invoice.line')
441         acct_anal_def_obj = self.pool.get('account.analytic.default')
442         if ids:
443             sale_line = self.browse(cr, uid, ids[0], context=context)
444             for line in inv_line_obj.browse(cr, uid, create_ids, context=context):
445                 rec = acct_anal_def_obj.account_get(cr, uid, line.product_id.id, sale_line.order_id.partner_id.id, uid, time.strftime('%Y-%m-%d'), context)
446
447                 if rec:
448                     inv_line_obj.write(cr, uid, [line.id], {'analytics_id': rec.analytics_id.id}, context=context)
449         return create_ids
450
451
452
453 class account_bank_statement(osv.osv):
454     _inherit = "account.bank.statement"
455     _name = "account.bank.statement"
456
457     def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, st_line_number, context=None):
458         account_move_line_pool = self.pool.get('account.move.line')
459         account_bank_statement_line_pool = self.pool.get('account.bank.statement.line')
460         st_line = account_bank_statement_line_pool.browse(cr, uid, st_line_id, context=context)
461         result = super(account_bank_statement,self).create_move_from_st_line(cr, uid, st_line_id, company_currency_id, st_line_number, context=context)
462         move = st_line.move_ids and st_line.move_ids[0] or False
463         if move:
464             for line in move.line_id:
465                 account_move_line_pool.write(cr, uid, [line.id], {'analytics_id':st_line.analytics_id.id}, context=context)
466         return result
467
468     def button_confirm_bank(self, cr, uid, ids, context=None):
469         super(account_bank_statement,self).button_confirm_bank(cr, uid, ids, context=context)
470         for st in self.browse(cr, uid, ids, context=context):
471             for st_line in st.line_ids:
472                 if st_line.analytics_id:
473                     if not st.journal_id.analytic_journal_id:
474                         raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal.") % (st.journal_id.name,))
475                 if not st_line.amount:
476                     continue
477         return True
478
479
480
481 class account_bank_statement_line(osv.osv):
482     _inherit = "account.bank.statement.line"
483     _name = "account.bank.statement.line"
484     _columns = {
485         'analytics_id': fields.many2one('account.analytic.plan.instance', 'Analytic Distribution'),
486     }
487
488 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: