[FIX] stock: error on stock.partial.move creation
[odoo/odoo.git] / openerp / report / custom.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 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 os
23 import time
24
25 import openerp.tools as tools
26 from openerp.tools.safe_eval import safe_eval as eval
27 import print_xml
28 import render
29 from interface import report_int
30 import common
31 from openerp.osv.osv import except_osv
32 from openerp.osv.orm import browse_null
33 from openerp.osv.orm import browse_record_list
34 import openerp.pooler as pooler
35 from pychart import *
36 import misc
37 import cStringIO
38 from lxml import etree
39 from openerp.tools.translate import _
40
41 class external_pdf(render.render):
42     def __init__(self, pdf):
43         render.render.__init__(self)
44         self.pdf = pdf
45         self.output_type='pdf'
46     def _render(self):
47         return self.pdf
48
49 theme.use_color = 1
50
51
52 #TODO: devrait heriter de report_rml a la place de report_int 
53 # -> pourrait overrider que create_xml a la place de tout create
54 # heuu, ca marche pas ds tous les cas car graphs sont generes en pdf directment
55 # par pychart, et on passe donc pas par du rml
56 class report_custom(report_int):
57     def __init__(self, name):
58         report_int.__init__(self, name)
59     #
60     # PRE:
61     #    fields = [['address','city'],['name'], ['zip']]
62     #    conditions = [[('zip','==','3'),(,)],(,),(,)] #same structure as fields
63     #    row_canvas = ['Rue', None, None]
64     # POST:
65     #    [ ['ville','name','zip'] ]
66     #
67     def _row_get(self, cr, uid, objs, fields, conditions, row_canvas=None, group_by=None):
68         result = []
69         for obj in objs:
70             tobreak = False
71             for cond in conditions:
72                 if cond and cond[0]:
73                     c = cond[0]
74                     temp = c[0](eval('obj.'+c[1],{'obj': obj}))
75                     if not eval('\''+temp+'\''+' '+c[2]+' '+'\''+str(c[3])+'\''):
76                         tobreak = True
77             if tobreak:
78                 break
79             levels = {}
80             row = []
81             for i in range(len(fields)):
82                 if not fields[i]:
83                     row.append(row_canvas and row_canvas[i])
84                     if row_canvas[i]:
85                         row_canvas[i]=False
86                 elif len(fields[i])==1:
87                     if not isinstance(obj, browse_null):
88                         row.append(str(eval('obj.'+fields[i][0],{'obj': obj})))
89                     else:
90                         row.append(None)
91                 else:
92                     row.append(None)
93                     levels[fields[i][0]]=True
94             if not levels:
95                 result.append(row)
96             else:
97                 # Process group_by data first
98                 key = []
99                 if group_by is not None and fields[group_by] is not None:
100                     if fields[group_by][0] in levels.keys():
101                         key.append(fields[group_by][0])
102                     for l in levels.keys():
103                         if l != fields[group_by][0]:
104                             key.append(l)
105                 else:
106                     key = levels.keys()
107                 for l in key:
108                     objs = eval('obj.'+l,{'obj': obj})
109                     if not isinstance(objs, (browse_record_list, list)):
110                         objs = [objs]
111                     field_new = []
112                     cond_new = []
113                     for f in range(len(fields)):
114                         if (fields[f] and fields[f][0])==l:
115                             field_new.append(fields[f][1:])
116                             cond_new.append(conditions[f][1:])
117                         else:
118                             field_new.append(None)
119                             cond_new.append(None)
120                     if len(objs):
121                         result += self._row_get(cr, uid, objs, field_new, cond_new, row, group_by)
122                     else:
123                         result.append(row)
124         return result 
125
126
127     def create(self, cr, uid, ids, datas, context=None):
128         if not context:
129             context={}
130         self.pool = pooler.get_pool(cr.dbname)
131         report = self.pool.get('ir.report.custom').browse(cr, uid, [datas['report_id']])[0]
132         datas['model'] = report.model_id.model
133         if report.menu_id:
134             ids = self.pool.get(report.model_id.model).search(cr, uid, [])
135             datas['ids'] = ids
136
137         report_id = datas['report_id']
138         report = self.pool.get('ir.report.custom').read(cr, uid, [report_id], context=context)[0]
139         fields = self.pool.get('ir.report.custom.fields').read(cr, uid, report['fields_child0'], context=context)
140
141         fields.sort(lambda x,y : x['sequence'] - y['sequence'])
142
143         if report['field_parent']:
144             parent_field = self.pool.get('ir.model.fields').read(cr, uid, [report['field_parent'][0]], ['model'])
145         model_name = self.pool.get('ir.model').read(cr, uid, [report['model_id'][0]], ['model'], context=context)[0]['model']
146
147         fct = {
148             'id': lambda x: x,
149             'gety': lambda x: x.split('-')[0],
150            'in': lambda x: x.split(',')
151         }
152         new_fields = []
153         new_cond = []
154         for f in fields:
155             row = []
156             cond = []
157             for i in range(4):
158                 field_child = f['field_child'+str(i)]
159                 if field_child:
160                     row.append(
161                         self.pool.get('ir.model.fields').read(cr, uid, [field_child[0]], ['name'], context=context)[0]['name']
162                     )
163                     if f['fc'+str(i)+'_operande']:
164                         fct_name = 'id'
165                         cond_op =  f['fc'+str(i)+'_op']
166                         if len(f['fc'+str(i)+'_op'].split(',')) == 2:
167                             cond_op =  f['fc'+str(i)+'_op'].split(',')[1]
168                             fct_name = f['fc'+str(i)+'_op'].split(',')[0]
169                         cond.append((fct[fct_name], f['fc'+str(i)+'_operande'][1], cond_op, f['fc'+str(i)+'_condition']))
170                     else:
171                         cond.append(None)
172             new_fields.append(row)
173             new_cond.append(cond)
174         objs = self.pool.get(model_name).browse(cr, uid, ids)
175
176         # Group by
177         groupby = None
178         idx = 0
179         for f in fields:
180             if f['groupby']:
181                 groupby = idx
182             idx += 1
183
184
185         results = []
186         if report['field_parent']:
187             level = []
188             def build_tree(obj, level, depth):
189                 res = self._row_get(cr, uid,[obj], new_fields, new_cond)
190                 level.append(depth)
191                 new_obj = eval('obj.'+report['field_parent'][1],{'obj': obj})
192                 if not isinstance(new_obj, list) :
193                     new_obj = [new_obj]
194                 for o in  new_obj:
195                     if not isinstance(o, browse_null):
196                         res += build_tree(o, level, depth+1)
197                 return res
198
199             for obj in objs:
200                 results += build_tree(obj, level, 0)
201         else:
202             results = self._row_get(cr, uid,objs, new_fields, new_cond, group_by=groupby)
203
204         fct = {
205             'calc_sum': lambda l: reduce(lambda x,y: float(x)+float(y), filter(None, l), 0),
206             'calc_avg': lambda l: reduce(lambda x,y: float(x)+float(y), filter(None, l), 0) / (len(filter(None, l)) or 1.0),
207             'calc_max': lambda l: reduce(lambda x,y: max(x,y), [(i or 0.0) for i in l], 0),
208             'calc_min': lambda l: reduce(lambda x,y: min(x,y), [(i or 0.0) for i in l], 0),
209             'calc_count': lambda l: len(filter(None, l)),
210             'False': lambda l: '\r\n'.join(filter(None, l)),
211             'groupby': lambda l: reduce(lambda x,y: x or y, l)
212         }
213         new_res = []
214
215         prev = None
216         if groupby is not None:
217             res_dic = {}
218             for line in results:
219                 if not line[groupby] and prev in res_dic:
220                     res_dic[prev].append(line)
221                 else:
222                     prev = line[groupby]
223                     res_dic.setdefault(line[groupby], [])
224                     res_dic[line[groupby]].append(line)
225
226             #we use the keys in results since they are ordered, whereas in res_dic.heys() they aren't
227             for key in filter(None, [x[groupby] for x in results]):
228                 row = []
229                 for col in range(len(fields)):
230                     if col == groupby:
231                         row.append(fct['groupby'](map(lambda x: x[col], res_dic[key])))
232                     else:
233                         row.append(fct[str(fields[col]['operation'])](map(lambda x: x[col], res_dic[key])))
234                 new_res.append(row)
235             results = new_res
236         
237         if report['type']=='table':
238             if report['field_parent']:
239                 res = self._create_tree(uid, ids, report, fields, level, results, context)
240             else:
241                 sort_idx = 0
242                 for idx in range(len(fields)):
243                     if fields[idx]['name'] == report['sortby']:
244                         sort_idx = idx
245                         break
246                 try :
247                     results.sort(lambda x,y : cmp(float(x[sort_idx]),float(y[sort_idx])))
248                 except :
249                     results.sort(lambda x,y : cmp(x[sort_idx],y[sort_idx]))
250                 if report['limitt']:
251                     results = results[:int(report['limitt'])]
252                 res = self._create_table(uid, ids, report, fields, None, results, context)
253         elif report['type'] in ('pie','bar', 'line'):
254             results2 = []
255             prev = False
256             for r in results:
257                 row = []
258                 for j in range(len(r)):
259                     if j == 0 and not r[j]:
260                         row.append(prev)
261                     elif j == 0 and r[j]:
262                         prev = r[j]
263                         row.append(r[j])
264                     else:
265                         try:
266                             row.append(float(r[j]))
267                         except Exception:
268                             row.append(r[j])
269                 results2.append(row)
270             if report['type']=='pie':
271                 res = self._create_pie(cr,uid, ids, report, fields, results2, context)
272             elif report['type']=='bar':
273                 res = self._create_bars(cr,uid, ids, report, fields, results2, context)
274             elif report['type']=='line':
275                 res = self._create_lines(cr,uid, ids, report, fields, results2, context)
276         return self.obj.get(), 'pdf'
277
278     def _create_tree(self, uid, ids, report, fields, level, results, context):
279         pageSize=common.pageSize.get(report['print_format'], [210.0,297.0])
280         if report['print_orientation']=='landscape':
281             pageSize=[pageSize[1],pageSize[0]]
282
283         new_doc = etree.Element('report')
284         
285         config = etree.SubElement(new_doc, 'config')
286
287         def _append_node(name, text):
288             n = etree.SubElement(config, name)
289             n.text = text
290
291         _append_node('date', time.strftime('%d/%m/%Y'))
292         _append_node('PageFormat', '%s' % report['print_format'])
293         _append_node('PageSize', '%.2fmm,%.2fmm' % tuple(pageSize))
294         _append_node('PageWidth', '%.2f' % (pageSize[0] * 2.8346,))
295         _append_node('PageHeight', '%.2f' %(pageSize[1] * 2.8346,))
296
297         length = pageSize[0]-30-reduce(lambda x,y:x+(y['width'] or 0), fields, 0)
298         count = 0
299         for f in fields:
300             if not f['width']: count+=1
301         for f in fields:
302             if not f['width']:
303                 f['width']=round((float(length)/count)-0.5)
304
305         _append_node('tableSize', '%s' %  ','.join(map(lambda x: '%.2fmm' % (x['width'],), fields)))
306         _append_node('report-header', '%s' % (report['title'],))
307         _append_node('report-footer', '%s' % (report['footer'],))
308
309         header = etree.SubElement(new_doc, 'header')
310         for f in fields:
311             field = etree.SubElement(header, 'field')
312             field.text = f['name']
313
314         lines = etree.SubElement(new_doc, 'lines')
315         level.reverse()
316         for line in results:
317             shift = level.pop()
318             node_line = etree.SubElement(lines, 'row')
319             prefix = '+'
320             for f in range(len(fields)):
321                 col = etree.SubElement(node_line, 'col')
322                 if f == 0:
323                     col.attrib.update(para='yes',
324                                       tree='yes',
325                                       space=str(3*shift)+'mm')
326                 if line[f] is not None:
327                     col.text = prefix+str(line[f]) or ''
328                 else:
329                     col.text = '/'
330                 prefix = ''
331
332         transform = etree.XSLT(
333             etree.parse(os.path.join(tools.config['root_path'],
334                                      'addons/base/report/custom_new.xsl')))
335         rml = etree.tostring(transform(new_doc))
336
337         self.obj = render.rml(rml)
338         self.obj.render()
339         return True
340
341
342     def _create_lines(self, cr, uid, ids, report, fields, results, context):
343         pool = pooler.get_pool(cr.dbname)
344         pdf_string = cStringIO.StringIO()
345         can = canvas.init(fname=pdf_string, format='pdf')
346         
347         can.show(80,380,'/16/H'+report['title'])
348         
349         ar = area.T(size=(350,350),
350         #x_coord = category_coord.T(['2005-09-01','2005-10-22'],0),
351         x_axis = axis.X(label = fields[0]['name'], format="/a-30{}%s"),
352         y_axis = axis.Y(label = ', '.join(map(lambda x : x['name'], fields[1:]))))
353         
354         process_date = {
355             'D': lambda x: reduce(lambda xx, yy: xx + '-' + yy, x.split('-')[1:3]),
356             'M': lambda x: x.split('-')[1],
357             'Y': lambda x: x.split('-')[0]
358         }
359
360         order_date = {
361             'D': lambda x: time.mktime((2005, int(x.split('-')[0]), int(x.split('-')[1]), 0, 0, 0, 0, 0, 0)),
362             'M': lambda x: x,
363             'Y': lambda x: x
364         }
365
366         abscissa = []
367         
368         idx = 0 
369         date_idx = None
370         fct = {}
371         for f in fields:
372             field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
373             if field_id:
374                 type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
375                 if type[0]['ttype'] == 'date':
376                     date_idx = idx
377                     fct[idx] = process_date[report['frequency']] 
378                 else:
379                     fct[idx] = lambda x : x
380             else:
381                 fct[idx] = lambda x : x
382             idx+=1
383
384         # plots are usually displayed year by year
385         # so we do so if the first field is a date
386         data_by_year = {}
387         if date_idx is not None:
388             for r in results:
389                 key = process_date['Y'](r[date_idx])
390                 if key not in data_by_year:
391                     data_by_year[key] = []
392                 for i in range(len(r)):
393                     r[i] = fct[i](r[i])
394                 data_by_year[key].append(r)
395         else:
396             data_by_year[''] = results
397
398         idx0 = 0
399         nb_bar = len(data_by_year)*(len(fields)-1)
400         colors = map(lambda x:line_style.T(color=x), misc.choice_colors(nb_bar))
401         abscissa = {}
402         for line in data_by_year.keys():
403             fields_bar = []
404             # sum data and save it in a list. An item for a fields
405             for d in data_by_year[line]:
406                 for idx in range(len(fields)-1):
407                     fields_bar.append({})
408                     if d[0] in fields_bar[idx]:
409                         fields_bar[idx][d[0]] += d[idx+1]
410                     else:
411                         fields_bar[idx][d[0]] = d[idx+1]
412             for idx  in range(len(fields)-1):
413                 data = {}
414                 for k in fields_bar[idx].keys():
415                     if k in data:
416                         data[k] += fields_bar[idx][k]
417                     else:
418                         data[k] = fields_bar[idx][k]
419                 data_cum = []
420                 prev = 0.0
421                 keys = data.keys()
422                 keys.sort()
423                 # cumulate if necessary
424                 for k in keys:
425                     data_cum.append([k, float(data[k])+float(prev)])
426                     if fields[idx+1]['cumulate']:
427                         prev += data[k]
428                 idx0 = 0
429                 plot = line_plot.T(label=fields[idx+1]['name']+' '+str(line), data = data_cum, line_style=colors[idx0*(len(fields)-1)+idx])
430                 ar.add_plot(plot)
431                 abscissa.update(fields_bar[idx])
432                 idx0 += 1
433         
434         abscissa = map(lambda x : [x, None], abscissa)
435         ar.x_coord = category_coord.T(abscissa,0)
436         ar.draw(can)
437
438         can.close()
439         self.obj = external_pdf(pdf_string.getvalue())
440         self.obj.render()
441         pdf_string.close()
442         return True
443
444
445
446     def _create_bars(self, cr, uid, ids, report, fields, results, context):
447         pool = pooler.get_pool(cr.dbname)
448         pdf_string = cStringIO.StringIO()
449         can = canvas.init(fname=pdf_string, format='pdf')
450         
451         can.show(80,380,'/16/H'+report['title'])
452         
453         process_date = {
454             'D': lambda x: reduce(lambda xx, yy: xx + '-' + yy, x.split('-')[1:3]),
455             'M': lambda x: x.split('-')[1],
456             'Y': lambda x: x.split('-')[0]
457         }
458
459         order_date = {
460             'D': lambda x: time.mktime((2005, int(x.split('-')[0]), int(x.split('-')[1]), 0, 0, 0, 0, 0, 0)),
461             'M': lambda x: x,
462             'Y': lambda x: x
463         }
464
465         ar = area.T(size=(350,350),
466             x_axis = axis.X(label = fields[0]['name'], format="/a-30{}%s"),
467             y_axis = axis.Y(label = ', '.join(map(lambda x : x['name'], fields[1:]))))
468
469         idx = 0 
470         date_idx = None
471         fct = {}
472         for f in fields:
473             field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
474             if field_id:
475                 type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
476                 if type[0]['ttype'] == 'date':
477                     date_idx = idx
478                     fct[idx] = process_date[report['frequency']] 
479                 else:
480                     fct[idx] = lambda x : x
481             else:
482                 fct[idx] = lambda x : x
483             idx+=1
484         
485         # plot are usually displayed year by year
486         # so we do so if the first field is a date
487         data_by_year = {}
488         if date_idx is not None:
489             for r in results:
490                 key = process_date['Y'](r[date_idx])
491                 if key not in data_by_year:
492                     data_by_year[key] = []
493                 for i in range(len(r)):
494                     r[i] = fct[i](r[i])
495                 data_by_year[key].append(r)
496         else:
497             data_by_year[''] = results
498
499
500         nb_bar = len(data_by_year)*(len(fields)-1)
501         colors = map(lambda x:fill_style.Plain(bgcolor=x), misc.choice_colors(nb_bar))
502         
503         abscissa = {}
504         for line in data_by_year.keys():
505             fields_bar = []
506             # sum data and save it in a list. An item for a fields
507             for d in data_by_year[line]:
508                 for idx in range(len(fields)-1):
509                     fields_bar.append({})
510                     if d[0] in fields_bar[idx]:
511                         fields_bar[idx][d[0]] += d[idx+1]
512                     else:
513                         fields_bar[idx][d[0]] = d[idx+1]
514             for idx  in range(len(fields)-1):
515                 data = {}
516                 for k in fields_bar[idx].keys():
517                     if k in data:
518                         data[k] += fields_bar[idx][k]
519                     else:
520                         data[k] = fields_bar[idx][k]
521                 data_cum = []
522                 prev = 0.0
523                 keys = data.keys()
524                 keys.sort()
525                 # cumulate if necessary
526                 for k in keys:
527                     data_cum.append([k, float(data[k])+float(prev)])
528                     if fields[idx+1]['cumulate']:
529                         prev += data[k]
530                         
531                 idx0 = 0
532                 plot = bar_plot.T(label=fields[idx+1]['name']+' '+str(line), data = data_cum, cluster=(idx0*(len(fields)-1)+idx,nb_bar), fill_style=colors[idx0*(len(fields)-1)+idx])
533                 ar.add_plot(plot)
534                 abscissa.update(fields_bar[idx])
535             idx0 += 1
536         abscissa = map(lambda x : [x, None], abscissa)
537         abscissa.sort()
538         ar.x_coord = category_coord.T(abscissa,0)
539         ar.draw(can)
540
541         can.close()
542         self.obj = external_pdf(pdf_string.getvalue())
543         self.obj.render()
544         pdf_string.close()
545         return True
546
547     def _create_pie(self, cr, uid, ids, report, fields, results, context):
548         pdf_string = cStringIO.StringIO()
549         can = canvas.init(fname=pdf_string, format='pdf')
550         ar = area.T(size=(350,350), legend=legend.T(),
551                     x_grid_style = None, y_grid_style = None)
552         colors = map(lambda x:fill_style.Plain(bgcolor=x), misc.choice_colors(len(results)))
553
554         if reduce(lambda x,y : x+y, map(lambda x : x[1],results)) == 0.0:
555             raise except_osv(_('Error'), _("The sum of the data (2nd field) is null.\nWe can't draw a pie chart !"))
556
557         plot = pie_plot.T(data=results, arc_offsets=[0,10,0,10],
558                           shadow = (2, -2, fill_style.gray50),
559                           label_offset = 25,
560                           arrow_style = arrow.a3,
561                           fill_styles=colors)
562         ar.add_plot(plot)
563         ar.draw(can)
564         can.close()
565         self.obj = external_pdf(pdf_string.getvalue())
566         self.obj.render()
567         pdf_string.close()
568         return True
569
570     def _create_table(self, uid, ids, report, fields, tree, results, context):
571         pageSize=common.pageSize.get(report['print_format'], [210.0,297.0])
572         if report['print_orientation']=='landscape':
573             pageSize=[pageSize[1],pageSize[0]]
574
575         new_doc = etree.Element('report')
576         config = etree.SubElement(new_doc, 'config')
577
578         def _append_node(name, text):
579             n = etree.SubElement(config, name)
580             n.text = text
581
582         _append_node('date', time.strftime('%d/%m/%Y'))
583         _append_node('PageSize', '%.2fmm,%.2fmm' % tuple(pageSize))
584         _append_node('PageFormat', '%s' % report['print_format'])
585         _append_node('PageWidth', '%.2f' % (pageSize[0] * 2.8346,))
586         _append_node('PageHeight', '%.2f' %(pageSize[1] * 2.8346,))
587
588         length = pageSize[0]-30-reduce(lambda x,y:x+(y['width'] or 0), fields, 0)
589         count = 0
590         for f in fields:
591             if not f['width']: count+=1
592         for f in fields:
593             if not f['width']:
594                 f['width']=round((float(length)/count)-0.5)
595
596         _append_node('tableSize', '%s' %  ','.join(map(lambda x: '%.2fmm' % (x['width'],), fields)))
597         _append_node('report-header', '%s' % (report['title'],))
598         _append_node('report-footer', '%s' % (report['footer'],))
599
600         header = etree.SubElement(new_doc, 'header')
601         for f in fields:
602             field = etree.SubElement(header, 'field')
603             field.text = f['name']
604
605         lines = etree.SubElement(new_doc, 'lines')
606         for line in results:
607             node_line = etree.SubElement(lines, 'row')
608             for f in range(len(fields)):
609                 col = etree.SubElement(node_line, 'col', tree='no')
610                 if line[f] is not None:
611                     col.text = line[f] or ''
612                 else:
613                     col.text = '/'
614
615         transform = etree.XSLT(
616             etree.parse(os.path.join(tools.config['root_path'],
617                                      'addons/base/report/custom_new.xsl')))
618         rml = etree.tostring(transform(new_doc))
619
620         self.obj = render.rml(rml)
621         self.obj.render()
622         return True
623 report_custom('report.custom')
624
625
626 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
627