1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from osv import fields,osv
27 from mx.DateTime import RelativeDateTime, today, DateTime, localtime
28 from tools import config
29 from tools.translate import _
31 class mrp_repair(osv.osv):
33 _description = 'Repairs Order'
35 def _amount_untaxed(self, cr, uid, ids, field_name, arg, context):
37 cur_obj=self.pool.get('res.currency')
38 for repair in self.browse(cr, uid, ids):
40 for line in repair.operations:
41 res[repair.id] += line.price_subtotal
42 for line in repair.fees_lines:
43 res[repair.id] += line.price_subtotal
44 cur = repair.pricelist_id.currency_id
45 res[repair.id] = cur_obj.round(cr, uid, cur, res[repair.id])
48 def _amount_tax(self, cr, uid, ids, field_name, arg, context):
50 cur_obj=self.pool.get('res.currency')
51 for repair in self.browse(cr, uid, ids):
53 cur=repair.pricelist_id.currency_id
54 for line in repair.operations:
56 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
58 for line in repair.fees_lines:
60 for c in self.pool.get('account.tax').compute(cr, uid, line.tax_id, line.price_unit, line.product_uom_qty, repair.partner_invoice_id.id, line.product_id, repair.partner_id):
62 res[repair.id]=cur_obj.round(cr, uid, cur, val)
65 def _amount_total(self, cr, uid, ids, field_name, arg, context):
67 untax = self._amount_untaxed(cr, uid, ids, field_name, arg, context)
68 tax = self._amount_tax(cr, uid, ids, field_name, arg, context)
69 cur_obj=self.pool.get('res.currency')
71 repair=self.browse(cr, uid, [id])[0]
72 cur=repair.pricelist_id.currency_id
73 res[id] = cur_obj.round(cr, uid, cur, untax.get(id, 0.0) + tax.get(id, 0.0))
77 'name' : fields.char('Repair Ref',size=24, required=True),
78 'product_id': fields.many2one('product.product', string='Product to Repair', required=True, readonly=True, states={'draft':[('readonly',False)]}),
79 'partner_id' : fields.many2one('res.partner', 'Partner', select=True, help='This field allow you to choose the parner that will be invoiced and delivered'),
80 'address_id': fields.many2one('res.partner.address', 'Delivery Address', domain="[('partner_id','=',partner_id)]"),
81 'prodlot_id': fields.many2one('stock.production.lot', 'Lot Number', select=True, domain="[('product_id','=',product_id)]"),
82 'state': fields.selection([
83 ('draft','Quotation'),
84 ('confirmed','Confirmed'),
85 ('ready','Ready to Repair'),
86 ('under_repair','Under Repair'),
87 ('2binvoiced','To be Invoiced'),
88 ('invoice_except','Invoice Exception'),
91 ], 'Repair State', readonly=True, help="Gives the state of the Repair Order"),
92 'location_id': fields.many2one('stock.location', 'Current Location', required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
93 'location_dest_id': fields.many2one('stock.location', 'Delivery Location', readonly=True, states={'draft':[('readonly',False)]}),
94 'move_id': fields.many2one('stock.move', 'Move',required=True, domain="[('product_id','=',product_id)]", readonly=True, states={'draft':[('readonly',False)]}),
95 'guarantee_limit': fields.date('Guarantee limit', help="The garantee limit is computed as: last move date + warranty defined on selected product. If the current date is below the garantee limit, each operation and fee you will add will be set as 'not to invoiced' by default. Note that you can change manually afterwards."),
96 'operations' : fields.one2many('mrp.repair.line', 'repair_id', 'Operation Lines', readonly=True, states={'draft':[('readonly',False)]}),
97 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help='The pricelist comes from the selected partner, by default.'),
98 'partner_invoice_id':fields.many2one('res.partner.address', 'Invoicing Address', domain="[('partner_id','=',partner_id)]"),
99 'invoice_method':fields.selection([
100 ("none","No Invoice"),
101 ("b4repair","Before Repair"),
102 ("after_repair","After Repair")
104 select=True, required=True, states={'draft':[('readonly',False)]}, readonly=True, help='This field allow you to change the workflow of the repair order. If value selected is different from \'No Invoice\', it also allow you to select the pricelist and invoicing address.'),
105 'invoice_id': fields.many2one('account.invoice', 'Invoice', readonly=True),
106 'picking_id': fields.many2one('stock.picking', 'Packing',readonly=True),
107 'fees_lines' : fields.one2many('mrp.repair.fee', 'repair_id', 'Fees Lines', readonly=True, states={'draft':[('readonly',False)]}),
108 'internal_notes' : fields.text('Internal Notes'),
109 'quotation_notes' : fields.text('Quotation Notes'),
110 'deliver_bool': fields.boolean('Deliver', help="Check this box if you want to manage the delivery once the product is repaired. If cheked, it will create a packing with selected product. Note that you can select the locations in the Info tab, if you have the extended view."),
111 'invoiced': fields.boolean('Invoiced', readonly=True),
112 'repaired' : fields.boolean('Repaired', readonly=True),
113 'amount_untaxed': fields.function(_amount_untaxed, method=True, string='Untaxed Amount'),
114 'amount_tax': fields.function(_amount_tax, method=True, string='Taxes'),
115 'amount_total': fields.function(_amount_total, method=True, string='Total'),
119 'state': lambda *a: 'draft',
120 'deliver_bool': lambda *a: True,
121 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
122 'invoice_method': lambda *a: 'none',
123 'pricelist_id': lambda self, cr, uid,context : self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
126 def copy(self, cr, uid, id, default=None, context=None):
135 'name': self.pool.get('ir.sequence').get(cr, uid, 'mrp.repair'),
137 return super(mrp_repair, self).copy(cr, uid, id, default, context)
140 def onchange_product_id(self, cr, uid, ids, product_id=None):
144 'guarantee_limit' :False,
145 'location_id': False,
146 'location_dest_id': False,
150 def onchange_move_id(self, cr, uid, ids, prod_id=False, move_id=False):
156 move = self.pool.get('stock.move').browse(cr, uid, move_id)
157 product = self.pool.get('product.product').browse(cr, uid, prod_id)
158 date = move.date_planned
159 limit = mx.DateTime.strptime(date, '%Y-%m-%d %H:%M:%S') + RelativeDateTime(months=product.warranty)
160 data['value']['guarantee_limit'] = limit.strftime('%Y-%m-%d')
161 data['value']['location_id'] = move.location_dest_id.id
162 data['value']['location_dest_id'] = move.location_dest_id.id
164 data['value']['partner_id'] = move.address_id.partner_id and move.address_id.partner_id.id
166 data['value']['partner_id'] = False
167 data['value']['address_id'] = move.address_id and move.address_id.id
168 d = self.onchange_partner_id(cr, uid, ids, data['value']['partner_id'], data['value']['address_id'])
169 data['value'].update(d['value'])
172 def button_dummy(self, cr, uid, ids, context=None):
175 def onchange_partner_id(self, cr, uid, ids, part, address_id):
179 'partner_invoice_id': False,
180 'pricelist_id': self.pool.get('product.pricelist').search(cr,uid,[('type','=','sale')])[0]
183 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'default'])
184 partner = self.pool.get('res.partner').browse(cr, uid, part)
185 pricelist = partner.property_product_pricelist and partner.property_product_pricelist.id or False
187 'address_id': address_id or addr['delivery'],
188 'partner_invoice_id': addr['invoice'],
189 'pricelist_id': pricelist
193 def onchange_lot_id(self, cr, uid, ids, lot, product_id):
196 'location_id': False,
197 'location_dest_id': False,
199 'guarantee_limit': False
204 lot_info = self.pool.get('stock.production.lot').browse(cr, uid, lot)
205 move_ids = self.pool.get('stock.move').search(cr, uid, [('prodlot_id', '=', lot)])
207 if not len(move_ids):
210 def get_last_move(lst_move):
211 while lst_move.move_dest_id and lst_move.move_dest_id.state == 'done':
212 lst_move = lst_move.move_dest_id
215 move_id = move_ids[0]
216 move = get_last_move(self.pool.get('stock.move').browse(cr, uid, move_id))
217 data['value']['move_id'] = move.id
218 d = self.onchange_move_id(cr, uid, ids, product_id, move.id)
219 data['value'].update(d['value'])
222 def action_cancel_draft(self, cr, uid, ids, *args):
225 mrp_line_obj = self.pool.get('mrp.repair.line')
226 for repair in self.browse(cr, uid, ids):
227 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'draft'})
228 self.write(cr, uid, ids, {'state':'draft'})
229 wf_service = netsvc.LocalService("workflow")
231 wf_service.trg_create(uid, 'mrp.repair', id, cr)
234 def action_confirm(self, cr, uid, ids, *args):
235 mrp_line_obj = self.pool.get('mrp.repair.line')
236 for o in self.browse(cr, uid, ids):
237 if (o.invoice_method == 'b4repair'):
238 self.write(cr, uid, [o.id], {'state': '2binvoiced'})
240 self.write(cr, uid, [o.id], {'state': 'confirmed'})
241 mrp_line_obj.write(cr, uid, [l.id for l in o.operations], {'state': 'confirmed'})
244 def action_cancel(self, cr, uid, ids, context=None):
246 mrp_line_obj = self.pool.get('mrp.repair.line')
247 for repair in self.browse(cr, uid, ids):
248 mrp_line_obj.write(cr, uid, [l.id for l in repair.operations], {'state': 'cancel'})
249 self.write(cr,uid,ids,{'state':'cancel'})
252 def wkf_invoice_create(self, cr, uid, ids, *args):
253 return self.action_invoice_create(cr, uid, ids)
255 def action_invoice_create(self, cr, uid, ids, group=False, context=None):
258 inv_obj = self.pool.get('account.invoice')
259 invoice_line_obj = self.pool.get('account.invoice.line')
260 repair_fee_obj = self.pool.get('mrp.repair.fee')
261 repair_line_obj = self.pool.get('mrp.repair.line')
262 for repair in self.browse(cr, uid, ids, context=context):
264 if repair.state in ('draft','cancel') or repair.invoice_id:
266 if not (repair.partner_id.id and repair.partner_invoice_id.id):
267 raise osv.except_osv(_('No partner !'),_('You have to select a partner and invoicing address in the repair form !'))
268 comment=repair.quotation_notes
269 if (repair.invoice_method != 'none'):
270 if group and repair.partner_invoice_id.id in invoices_group:
271 inv_id = invoices_group[repair.partner_invoice_id.id]
272 invoice = inv_obj.browse(cr, uid,inv_id)
274 'name': invoice.name +', '+repair.name,
275 'origin': invoice.origin+', '+repair.name,
276 'comment':(comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
278 inv_obj.write(cr, uid, [inv_id],invoice_vals,context=context)
280 if not repair.partner_id.property_account_receivable:
281 raise osv.except_osv(_('Error !'), _('No account defined for partner "%s".') % repair.partner_id.name )
282 account_id = repair.partner_id.property_account_receivable.id
285 'origin':repair.name,
286 'type': 'out_invoice',
287 'account_id': account_id,
288 'partner_id': repair.partner_id.id,
289 'address_invoice_id': repair.address_id.id,
290 'currency_id': repair.pricelist_id.currency_id.id,
291 'comment': repair.quotation_notes,
292 'fiscal_position': repair.partner_id.property_account_position.id
294 inv_id = inv_obj.create(cr, uid, inv)
295 invoices_group[repair.partner_invoice_id.id] = inv_id
296 self.write(cr, uid, repair.id , {'invoiced':True,'invoice_id' : inv_id})
298 for operation in repair.operations:
299 if operation.to_invoice == True:
301 name = repair.name + '-' + operation.name
303 name = operation.name
305 if operation.product_id.property_account_income:
306 account_id = operation.product_id.property_account_income.id
307 elif operation.product_id.categ_id.property_account_income_categ:
308 account_id = operation.product_id.categ_id.property_account_income_categ.id
310 raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % operation.product_id.name )
312 invoice_line_id = invoice_line_obj.create(cr, uid, {
313 'invoice_id': inv_id,
315 'origin':repair.name,
316 'account_id': account_id,
317 'quantity' : operation.product_uom_qty,
318 'invoice_line_tax_id': [(6,0,[x.id for x in operation.tax_id])],
319 'uos_id' : operation.product_uom.id,
320 'price_unit' : operation.price_unit,
321 'price_subtotal' : operation.product_uom_qty*operation.price_unit,
322 'product_id' : operation.product_id and operation.product_id.id or False
324 repair_line_obj.write(cr, uid, [operation.id], {'invoiced':True,'invoice_line_id':invoice_line_id})
325 for fee in repair.fees_lines:
326 if fee.to_invoice == True:
328 name = repair.name + '-' + fee.name
331 if not fee.product_id:
332 raise osv.except_osv(_('Warning !'), _('No product defined on Fees!'))
333 if fee.product_id.property_account_income:
334 account_id = fee.product_id.property_account_income.id
335 elif fee.product_id.categ_id.property_account_income_categ:
336 account_id = fee.product_id.categ_id.property_account_income_categ.id
338 raise osv.except_osv(_('Error !'), _('No account defined for product "%s".') % fee.product_id.name)
339 invoice_fee_id = invoice_line_obj.create(cr, uid, {
340 'invoice_id': inv_id,
342 'origin':repair.name,
343 'account_id': account_id,
344 'quantity': fee.product_uom_qty,
345 'invoice_line_tax_id': [(6,0,[x.id for x in fee.tax_id])],
346 'uos_id': fee.product_uom.id,
347 'product_id': fee.product_id and fee.product_id.id or False,
348 'price_unit': fee.price_unit,
349 'price_subtotal': fee.product_uom_qty*fee.price_unit
351 repair_fee_obj.write(cr, uid, [fee.id], {'invoiced':True,'invoice_line_id':invoice_fee_id})
352 res[repair.id]=inv_id
353 #self.action_invoice_end(cr, uid, ids)
356 def action_repair_ready(self, cr, uid, ids, context=None):
357 self.write(cr, uid, ids, {'state':'ready'})
360 def action_invoice_cancel(self, cr, uid, ids, context=None):
361 self.write(cr, uid, ids, {'state':'invoice_except'})
364 def action_repair_start(self, cr, uid, ids, context=None):
365 self.write(cr, uid, ids, {'state':'under_repair'})
368 def action_invoice_end(self, cr, uid, ids, context=None):
369 for order in self.browse(cr, uid, ids):
371 if (order.invoice_method=='b4repair'):
372 val['state'] = 'ready'
374 #val['state'] = 'done'
376 self.write(cr, uid, [order.id], val)
379 def action_repair_end(self, cr, uid, ids, context=None):
380 for order in self.browse(cr, uid, ids):
383 if (not order.invoiced and order.invoice_method=='after_repair'):
384 val['state'] = '2binvoiced'
385 elif (not order.invoiced and order.invoice_method=='b4repair'):
386 val['state'] = 'ready'
388 #val['state'] = 'done'
390 self.write(cr, uid, [order.id], val)
393 def wkf_repair_done(self, cr, uid, ids, *args):
394 res=self.action_repair_done(cr,uid,ids)
397 def action_repair_done(self, cr, uid, ids, context=None):
399 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
400 for repair in self.browse(cr, uid, ids, context=context):
401 for move in repair.operations:
402 move_id = self.pool.get('stock.move').create(cr, uid, {
404 'product_id': move.product_id.id,
405 'product_qty': move.product_uom_qty,
406 'product_uom': move.product_uom.id,
407 'address_id': repair.address_id and repair.address_id.id or False,
408 'location_id': move.location_id.id,
409 'location_dest_id': move.location_dest_id.id,
410 'tracking_id': False,
413 self.pool.get('mrp.repair.line').write(cr, uid, [move.id], {'move_id': move_id})
415 if repair.deliver_bool:
416 picking = self.pool.get('stock.picking').create(cr, uid, {
417 'origin': repair.name,
420 'address_id': repair.address_id and repair.address_id.id or False,
421 'note': repair.internal_notes,
422 'invoice_state': 'none',
425 wf_service = netsvc.LocalService("workflow")
426 wf_service.trg_validate(uid, 'stock.picking', picking, 'button_confirm', cr)
428 move_id = self.pool.get('stock.move').create(cr, uid, {
430 'picking_id': picking,
431 'product_id': repair.product_id.id,
433 'product_uom': repair.product_id.uom_id.id,
434 #'product_uos_qty': line.product_uom_qty,
435 #'product_uos': line.product_uom.id,
436 'prodlot_id': repair.prodlot_id and repair.prodlot_id.id or False,
437 'address_id': repair.address_id and repair.address_id.id or False,
438 'location_id': repair.location_id.id,
439 'location_dest_id': repair.location_dest_id.id,
440 'tracking_id': False,
441 'state': 'assigned', # FIXME done ?
443 self.write(cr, uid, [repair.id], {'state':'done', 'picking_id':picking})
444 res[repair.id] = picking
446 self.write(cr, uid, [repair.id], {'state':'done'})
453 class ProductChangeMixin(object):
454 def product_id_change(self, cr, uid, ids, pricelist, product, uom=False, product_uom_qty=0, partner_id=False, guarantee_limit=False):
458 if not product_uom_qty:
460 result['product_uom_qty'] = product_uom_qty
463 product_obj = self.pool.get('product.product').browse(cr, uid, product)
465 partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
466 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, product_obj.taxes_id)
468 result['name'] = product_obj.partner_ref
469 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id or False
472 'title':'No Pricelist !',
474 'You have to select a pricelist in the Repair form !\n'
475 'Please set one before choosing a product.'
478 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
479 product, product_uom_qty, partner_id, {'uom': uom,})[pricelist]
483 'title':'No valid pricelist line found !',
485 "Couldn't find a pricelist line matching this product and quantity.\n"
486 "You have to change either the product, the quantity or the pricelist."
489 result.update({'price_unit': price, 'price_subtotal' :price*product_uom_qty})
491 return {'value': result, 'warning': warning}
494 class mrp_repair_line(osv.osv, ProductChangeMixin):
495 _name = 'mrp.repair.line'
496 _description = 'Repair Operations Lines'
498 def copy_data(self, cr, uid, id, default=None, context=None):
499 if not default: default = {}
500 default.update( {'invoice_line_id':False,'move_id':False,'invoiced':False,'state':'draft'})
501 return super(mrp_repair_line, self).copy_data(cr, uid, id, default, context)
503 def _amount_line(self, cr, uid, ids, field_name, arg, context):
505 cur_obj=self.pool.get('res.currency')
506 for line in self.browse(cr, uid, ids):
507 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
508 cur = line.repair_id.pricelist_id.currency_id
509 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
513 'name' : fields.char('Description',size=64,required=True),
514 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref',ondelete='cascade', select=True),
515 'type': fields.selection([('add','Add'),('remove','Remove')],'Type', required=True),
516 'to_invoice': fields.boolean('To Invoice'),
517 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok','=',True)], required=True),
518 'invoiced': fields.boolean('Invoiced',readonly=True),
519 'price_unit': fields.float('Unit Price', required=True, digits=(16, int(config['price_accuracy']))),
520 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
521 'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
522 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16,2), required=True),
523 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
524 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
525 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
526 'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
527 'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True),
528 'state': fields.selection([('draft','Draft'),('confirmed','Confirmed'),('done','Done'),('cancel','Canceled')], 'Status', required=True, readonly=True),
531 'state': lambda *a: 'draft',
532 'product_uom_qty':lambda *a:1,
535 def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit):
538 'location_id': False,
539 'location_dest_id': False
542 produc_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Production')])[0]
544 stock_id = self.pool.get('stock.location').search(cr, uid, [('name','=','Stock')])[0]
546 if guarantee_limit and today() > mx.DateTime.strptime(guarantee_limit, '%Y-%m-%d'):
549 'to_invoice': to_invoice,
550 'location_id': stock_id,
551 'location_dest_id' : produc_id
556 'location_id': produc_id,
557 'location_dest_id':False
563 class mrp_repair_fee(osv.osv, ProductChangeMixin):
564 _name = 'mrp.repair.fee'
565 _description = 'Repair Fees line'
566 def copy_data(self, cr, uid, id, default=None, context=None):
567 if not default: default = {}
568 default.update( {'invoice_line_id':False,'invoiced':False})
569 return super(mrp_repair_fee, self).copy_data(cr, uid, id, default, context)
570 def _amount_line(self, cr, uid, ids, field_name, arg, context):
572 cur_obj=self.pool.get('res.currency')
573 for line in self.browse(cr, uid, ids):
574 res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0
575 cur = line.repair_id.pricelist_id.currency_id
576 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
580 'repair_id': fields.many2one('mrp.repair', 'Repair Order Ref', required=True, ondelete='cascade', select=True),
581 'name': fields.char('Description', size=64, select=True,required=True),
582 'product_id': fields.many2one('product.product', 'Product'),
583 'product_uom_qty': fields.float('Quantity', digits=(16,2), required=True),
584 'price_unit': fields.float('Unit Price', required=True),
585 'product_uom': fields.many2one('product.uom', 'Product UoM', required=True),
586 'price_subtotal': fields.function(_amount_line, method=True, string='Subtotal',digits=(16, int(config['price_accuracy']))),
587 'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
588 'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True),
589 'to_invoice': fields.boolean('To Invoice'),
590 'invoiced': fields.boolean('Invoiced',readonly=True),
593 'to_invoice': lambda *a: True,
597 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: