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