~unifield-team/unifield-wm/us-826

« back to all changes in this revision

Viewing changes to analytic_distribution/analytic_distribution.py

  • Committer: jf
  • Date: 2014-05-28 13:16:31 UTC
  • mto: This revision was merged to the branch mainline in revision 2187.
  • Revision ID: jfb@tempo-consulting.fr-20140528131631-13qcl8f5h390rmtu
UFTP-244 [FIX] In sync context, do not auto create the link between account.account and account.destination.link for default destination
this link is created by a dedicated sync rule

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
#
20
20
##############################################################################
21
21
 
22
 
from osv import fields, osv
23
 
import decimal_precision as dp
24
 
from tools.misc import flatten
25
 
from time import strftime
 
22
from osv import osv
26
23
import netsvc
27
24
from tools.translate import _
28
25
 
29
 
class analytic_distribution1(osv.osv):
30
 
    _name = "analytic.distribution"
31
 
 
32
 
    _columns = {
33
 
        'analytic_lines': fields.one2many('account.analytic.line', 'distribution_id', 'Analytic Lines'),
34
 
        'invoice_ids': fields.one2many('account.invoice', 'analytic_distribution_id', string="Invoices"),
35
 
        'invoice_line_ids': fields.one2many('account.invoice.line', 'analytic_distribution_id', string="Invoice Lines"),
36
 
        'register_line_ids': fields.one2many('account.bank.statement.line', 'analytic_distribution_id', string="Register Lines"),
37
 
        'move_line_ids': fields.one2many('account.move.line', 'analytic_distribution_id', string="Move Lines"),
38
 
        'commitment_ids': fields.one2many('account.commitment', 'analytic_distribution_id', string="Commitments voucher"),
39
 
        'commitment_line_ids': fields.one2many('account.commitment.line', 'analytic_distribution_id', string="Commitment voucher lines"),
40
 
    }
41
 
 
42
 
    def copy(self, cr, uid, id, default=None, context=None):
43
 
        """
44
 
        Copy an analytic distribution without the one2many links
45
 
        """
46
 
        if default is None:
47
 
            default = {}
48
 
        default.update({
49
 
            'analytic_lines': False,
50
 
            'invoice_ids': False,
51
 
            'invoice_line_ids': False,
52
 
            'register_line_ids': False,
53
 
            'move_line_ids': False,
54
 
            'commitment_ids': False,
55
 
            'commitment_line_ids': False,
56
 
        })
57
 
        return super(osv.osv, self).copy(cr, uid, id, default, context=context)
58
 
 
59
 
    def _get_distribution_state(self, cr, uid, id, parent_id, account_id, context=None):
 
26
class analytic_distribution(osv.osv):
 
27
    _name = 'analytic.distribution'
 
28
    _inherit = 'analytic.distribution'
 
29
 
 
30
    def _get_distribution_state(self, cr, uid, distrib_id, parent_id, account_id, context=None):
60
31
        """
61
32
        Return distribution state
62
33
        """
63
34
        if context is None:
64
35
            context = {}
65
 
        # Have an analytic distribution on another account than expense account make no sense. So their analytic distribution is valid
 
36
        # Have an analytic distribution on another account than analytic-a-holic account make no sense. So their analytic distribution is valid
66
37
        logger = netsvc.Logger()
67
38
        if account_id:
68
 
            account =  self.pool.get('account.account').browse(cr, uid, account_id)
69
 
            if account and account.user_type and account.user_type.code != 'expense':
 
39
            account =  self.pool.get('account.account').read(cr, uid, account_id, ['is_analytic_addicted'])
 
40
            if account and not account.get('is_analytic_addicted', False):
70
41
                return 'valid'
71
 
        if not id:
 
42
        if not distrib_id:
72
43
            if parent_id:
73
44
                return self._get_distribution_state(cr, uid, parent_id, False, account_id, context)
74
 
            logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: NONE!") % (id or ''))
 
45
            logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: NONE!") % (distrib_id or ''))
75
46
            return 'none'
76
 
        distrib = self.browse(cr, uid, id)
 
47
        distrib = self.browse(cr, uid, distrib_id)
77
48
        # Search MSF Private Fund element, because it's valid with all accounts
78
49
        try:
79
 
            fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 
 
50
            fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution',
80
51
            'analytic_account_msf_private_funds')[1]
81
52
        except ValueError:
82
53
            fp_id = 0
83
 
        account = self.pool.get('account.account').browse(cr, uid, account_id)
 
54
        account = self.pool.get('account.account').read(cr, uid, account_id, ['destination_ids'])
84
55
        # Check Cost Center lines with destination/account link
85
56
        for cc_line in distrib.cost_center_lines:
86
 
            if cc_line.destination_id.id not in [x.id for x in account.destination_ids]:
87
 
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, destination not compatible with G/L account in CC lines") % (id or ''))
 
57
            if cc_line.destination_id.id not in account.get('destination_ids', []):
 
58
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, destination not compatible with G/L account in CC lines") % (distrib_id or ''))
88
59
                return 'invalid'
89
60
        # Check Funding pool lines regarding:
90
61
        # - destination / account
91
62
        # - If analytic account is MSF Private funds
92
63
        # - Cost center and funding pool compatibility
93
64
        for fp_line in distrib.funding_pool_lines:
94
 
            if fp_line.destination_id.id not in [x.id for x in account.destination_ids]:
95
 
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, destination not compatible with G/L account for FP lines") % (id or ''))
 
65
            if fp_line.destination_id.id not in account.get('destination_ids', []):
 
66
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, destination not compatible with G/L account for FP lines") % (distrib_id or ''))
96
67
                return 'invalid'
97
68
            # If fp_line is MSF Private Fund, all is ok
98
69
            if fp_line.analytic_id.id == fp_id:
99
70
                continue
100
71
            if (account_id, fp_line.destination_id.id) not in [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in fp_line.analytic_id.tuple_destination_account_ids]:
101
 
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, account/destination tuple not compatible with given FP analytic account") % (id or ''))
 
72
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, account/destination tuple not compatible with given FP analytic account") % (distrib_id or ''))
102
73
                return 'invalid'
103
74
            if fp_line.cost_center_id.id not in [x.id for x in fp_line.analytic_id.cost_center_ids]:
104
 
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, CC is not compatible with given FP analytic account") % (id or ''))
 
75
                logger.notifyChannel("analytic distribution", netsvc.LOG_WARNING, _("%s: Error, CC is not compatible with given FP analytic account") % (distrib_id or ''))
105
76
                return 'invalid'
106
77
        return 'valid'
107
78
 
108
 
analytic_distribution1()
109
 
 
110
 
class distribution_line(osv.osv):
111
 
    _name = "distribution.line"
112
 
 
113
 
    _columns = {
114
 
        'name': fields.char('Name', size=64),
115
 
        "distribution_id": fields.many2one('analytic.distribution', 'Associated Analytic Distribution', ondelete='cascade'),
116
 
        "analytic_id": fields.many2one('account.analytic.account', 'Analytical Account'),
117
 
        "amount": fields.float('Amount', digits_compute=dp.get_precision('Account')),
118
 
        "percentage": fields.float('Percentage', digits=(16,4)),
119
 
        "currency_id": fields.many2one('res.currency', 'Currency', required=True),
120
 
        "date": fields.date(string="Date"),
121
 
        "source_date": fields.date(string="Source Date", help="This date is for source_date for analytic lines"),
122
 
    }
123
 
 
124
 
    _defaults ={
125
 
        'name': 'Distribution Line',
126
 
        'date': lambda *a: strftime('%Y-%m-%d'),
127
 
        'source_date': lambda *a: strftime('%Y-%m-%d'),
128
 
    }
129
 
 
130
 
    def _check_percentage(self, cr, uid, ids, context=None):
131
 
        """
132
 
        Do not allow 0.0 percentage value
133
 
        """
134
 
        for l in self.browse(cr, uid, ids):
135
 
            if l.percentage == 0.0:
136
 
                return False
137
 
        return True
138
 
 
139
 
    _constraints = [
140
 
        (_check_percentage, '0 is not allowed as percentage value!', ['percentage']),
141
 
    ]
142
 
 
143
 
    def create_analytic_lines(self, cr, uid, ids, move_line_id, date, document_date, source_date=False, name=False, context=None):
144
 
        '''
145
 
        Creates an analytic lines from a distribution line and an account.move.line
146
 
        '''
147
 
        if isinstance(ids, (int, long)):
148
 
            ids = [ids]
149
 
 
150
 
        ret = {}
151
 
        move_line = self.pool.get('account.move.line').browse(cr, uid, move_line_id)
152
 
        company_currency_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
153
 
 
154
 
        for line in self.browse(cr, uid, ids):
155
 
            amount_cur = (move_line.credit_currency - move_line.debit_currency) * line.percentage / 100
156
 
            ctx = {'date': source_date or date}
157
 
            amount = self.pool.get('res.currency').compute(cr, uid, move_line.currency_id.id, company_currency_id, amount_cur, round=False, context=ctx)
158
 
            vals = {
159
 
                'account_id': line.analytic_id.id,
160
 
                'amount_currency': amount_cur,
161
 
                'amount': amount,
162
 
                'currency_id': move_line.currency_id.id,
163
 
                'general_account_id': move_line.account_id.id,
164
 
                'date': date,
165
 
                'source_date': source_date,
166
 
                'document_date': document_date,
167
 
                'journal_id': move_line.journal_id and move_line.journal_id.analytic_journal_id and move_line.journal_id.analytic_journal_id.id or False,
168
 
                'move_id': move_line.id,
169
 
                'name': name or move_line.name,
170
 
                'distrib_id': line.distribution_id.id,
171
 
                'distrib_line_id': '%s,%s'%(self._name, line.id),
172
 
            }
173
 
            if self._name == 'funding.pool.distribution.line':
174
 
                vals.update({
175
 
                    'destination_id': line.destination_id and line.destination_id.id or False,
176
 
                    'cost_center_id': line.cost_center_id and line.cost_center_id.id or False,
177
 
                })
178
 
            ret[line.id] = self.pool.get('account.analytic.line').create(cr, uid, vals)
179
 
 
180
 
        return ret
181
 
 
182
 
        
183
 
distribution_line()
184
 
 
185
 
class cost_center_distribution_line(osv.osv):
186
 
    _name = "cost.center.distribution.line"
187
 
    _inherit = "distribution.line"
188
 
    _columns = {
189
 
        "destination_id": fields.many2one('account.analytic.account', 'Destination', domain="[('type', '!=', 'view'), ('category', '=', 'DEST')]", required=True),
190
 
    }
191
 
    
192
 
cost_center_distribution_line()
193
 
 
194
 
class funding_pool_distribution_line(osv.osv):
195
 
    _name = "funding.pool.distribution.line"
196
 
    _inherit = "distribution.line"
197
 
    _columns = {
198
 
        "cost_center_id": fields.many2one('account.analytic.account', 'Cost Center Account', required=True),
199
 
        "destination_id": fields.many2one('account.analytic.account', 'Destination', domain="[('type', '!=', 'view'), ('category', '=', 'DEST')]", required=True),
200
 
    }
201
 
    
202
 
funding_pool_distribution_line()
203
 
 
204
 
class free_1_distribution_line(osv.osv):
205
 
    _name = "free.1.distribution.line"
206
 
    _inherit = "distribution.line"
207
 
    _columns = {
208
 
        "destination_id": fields.many2one('account.analytic.account', 'Destination', domain="[('type', '!=', 'view'), ('category', '=', 'DEST')]", required=False),
209
 
    }
210
 
    
211
 
free_1_distribution_line()
212
 
 
213
 
class free_2_distribution_line(osv.osv):
214
 
    _name = "free.2.distribution.line"
215
 
    _inherit = "distribution.line"
216
 
    _columns = {
217
 
        "destination_id": fields.many2one('account.analytic.account', 'Destination', domain="[('type', '!=', 'view'), ('category', '=', 'DEST')]", required=False),
218
 
    }
219
 
    
220
 
free_2_distribution_line()
221
 
 
222
 
class analytic_distribution(osv.osv):
223
 
    _name = 'analytic.distribution'
224
 
    _inherit = "analytic.distribution"
225
 
 
226
 
    def _get_lines_count(self, cr, uid, ids, name=False, args=False, context=None):
227
 
        """
228
 
        Get count of each analytic distribution lines type.
229
 
        Example: with an analytic distribution with 2 cost center, 3 funding pool and 1 Free 1:
230
 
        2 CC; 3 FP; 1 F1; 0 F2; 
231
 
        (Number of chars: 20 chars + 4 x some lines number)
232
 
        """
233
 
        # Some verifications
234
 
        if not context:
235
 
            context = {}
236
 
        # Prepare some values
237
 
        res = {}
238
 
        if not ids:
239
 
            return res
240
 
        if isinstance(ids, (int, long)):
241
 
            ids = [ids]
242
 
        # Browse given invoices
243
 
        for distrib in self.browse(cr, uid, ids, context=context):
244
 
            txt = ''
245
 
            txt += str(len(distrib.cost_center_lines) or '0') + ' CC; '
246
 
            txt += str(len(distrib.funding_pool_lines) or '0') + ' FP; '
247
 
            txt += str(len(distrib.free_1_lines) or '0') + ' F1; '
248
 
            txt += str(len(distrib.free_2_lines) or '0') + ' F2'
249
 
            if not txt:
250
 
                txt = ''
251
 
            res[distrib.id] = txt
252
 
        return res
253
 
 
254
 
    _columns = {
255
 
        'cost_center_lines': fields.one2many('cost.center.distribution.line', 'distribution_id', 'Cost Center Distribution'),
256
 
        'funding_pool_lines': fields.one2many('funding.pool.distribution.line', 'distribution_id', 'Funding Pool Distribution'),
257
 
        'free_1_lines': fields.one2many('free.1.distribution.line', 'distribution_id', 'Free 1 Distribution'),
258
 
        'free_2_lines': fields.one2many('free.2.distribution.line', 'distribution_id', 'Free 2 Distribution'),
259
 
        'name': fields.function(_get_lines_count, method=True, type='char', size=256, string="Name", readonly=True, store=False),
260
 
    }
261
 
 
262
 
    def update_distribution_line_amount(self, cr, uid, ids, amount=False, context=None):
263
 
        """
264
 
        Update amount on distribution lines for given distribution (ids)
265
 
        """
266
 
        # Some verifications
267
 
        if not context:
268
 
            context = {}
269
 
        if isinstance(ids, (int, long)):
270
 
            ids = [ids]
271
 
        if not amount:
272
 
            return False
273
 
        # Process distributions
274
 
        for distrib_id in ids:
275
 
            for dl_name in ['cost.center.distribution.line', 'funding.pool.distribution.line', 'free.1.distribution.line', 'free.2.distribution.line']:
276
 
                dl_obj = self.pool.get(dl_name)
277
 
                dl_ids = dl_obj.search(cr, uid, [('distribution_id', '=', distrib_id)], context=context)
278
 
                for dl in dl_obj.browse(cr, uid, dl_ids, context=context):
279
 
                    dl_vals = {
280
 
                        'amount': round(dl.percentage * amount) / 100.0,
281
 
                    }
282
 
                    dl_obj.write(cr, uid, [dl.id], dl_vals, context=context)
283
 
        return True
284
 
 
285
 
    def update_distribution_line_account(self, cr, uid, line_ids, account_id, context=None):
286
 
        """
287
 
        Update account on distribution line
288
 
        """
289
 
        # Some verifications
290
 
        if not context:
291
 
            context = {}
292
 
        if isinstance(line_ids, (int, long)):
293
 
            line_ids = [line_ids]
294
 
        if not account_id:
295
 
            return False
296
 
        # Prepare some values
297
 
        account = self.pool.get('account.analytic.account').browse(cr, uid, [account_id], context=context)[0]
298
 
 
299
 
        if account.category == 'OC':
300
 
            vals = {'cost_center_id': account_id}
301
 
        else:
302
 
            vals = {'analytic_id': account_id}
303
 
        return self.pool.get('funding.pool.distribution.line').write(cr, uid, line_ids, vals)
304
 
 
305
 
    def create_funding_pool_lines(self, cr, uid, ids, account_id=False, context=None):
306
 
        """
307
 
        Create funding pool lines regarding cost_center_lines from analytic distribution.
308
 
        If funding_pool_lines exists, then nothing appends.
309
 
        By default, add funding_pool_lines with MSF Private Fund element (written in an OpenERP demo file).
310
 
        For destination axis, get those from account_id default configuration (default_destination_id).
311
 
        """
312
 
        # Some verifications
313
 
        if not context:
314
 
            context = {}
315
 
        if isinstance(ids, (int, long)):
316
 
            ids = [ids]
317
 
        # Prepare some values
318
 
        res = {}
319
 
        # Browse distributions
320
 
        for distrib in self.browse(cr, uid, ids, context=context):
321
 
            if distrib.funding_pool_lines:
322
 
                res[distrib.id] = False
323
 
                continue
324
 
            # Browse cost center lines
325
 
            for line in distrib.cost_center_lines:
326
 
                # Search MSF Private Fund
327
 
                try:
328
 
                    pf_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 
329
 
                    'analytic_account_msf_private_funds')[1]
330
 
                except ValueError:
331
 
                    pf_id = 0
332
 
                if pf_id:
333
 
                    vals = {
334
 
                        'analytic_id': pf_id,
335
 
                        'amount': line.amount or 0.0,
336
 
                        'percentage': line.percentage or 0.0,
337
 
                        'currency_id': line.currency_id and line.currency_id.id or False,
338
 
                        'distribution_id': distrib.id or False,
339
 
                        'cost_center_id': line.analytic_id and line.analytic_id.id or False,
340
 
                        'destination_id': line.destination_id and line.destination_id.id or False,
341
 
                    }
342
 
                    # Search default destination if no one given
343
 
                    if account_id and not vals.get('destination_id'):
344
 
                        account = self.pool.get('account.account').browse(cr, uid, account_id)
345
 
                        if account and account.user_type and account.user_type.code == 'expense':
346
 
                            vals.update({'destination_id': account.default_destination_id and account.default_destination_id.id or False})
347
 
                    new_pf_line_id = self.pool.get('funding.pool.distribution.line').create(cr, uid, vals, context=context)
348
 
            res[distrib.id] = True
349
 
        return res
350
 
 
351
 
    def create_analytic_lines(self, cr, uid, ids, name, date, amount, journal_id, currency_id, document_date=False, ref=False, source_date=False, general_account_id=False, \
352
 
        move_id=False, invoice_line_id=False, commitment_line_id=False, context=None):
353
 
        """
354
 
        Create analytic lines from given elements:
355
 
         - date
356
 
         - name
357
 
         - amount
358
 
         - journal_id (analytic_journal_id)
359
 
         - currency_id
360
 
         - ref (optional)
361
 
         - source_date (optional)
362
 
         - general_account_id (optional)
363
 
         - move_id (optional)
364
 
         - invoice_line_id (optional)
365
 
         - commitment_line_id (optional)
366
 
        Return all created ids, otherwise return false (or [])
367
 
        """
368
 
        # Some verifications
369
 
        if not context:
370
 
            context = {}
371
 
        if isinstance(ids, (int, long)):
372
 
            ids = [ids]
373
 
        if not name or not date or not amount or not journal_id or not currency_id:
374
 
            return False
375
 
        if not document_date:
376
 
            document_date = date
377
 
        # Prepare some values
378
 
        res = []
379
 
        vals = {
380
 
            'name': name,
381
 
            'date': source_date or date,
382
 
            'document_date': document_date,
383
 
            'ref': ref or '',
384
 
            'journal_id': journal_id,
385
 
            'general_account_id': general_account_id or False,
386
 
            'move_id': move_id or False,
387
 
            'invoice_line_id': invoice_line_id or False,
388
 
            'user_id': uid,
389
 
            'currency_id': currency_id,
390
 
            'source_date': source_date or False,
391
 
            'commitment_line_id': commitment_line_id or False,
392
 
        }
393
 
        company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
394
 
        # Browse distribution(s)
395
 
        for distrib in self.browse(cr, uid, ids, context=context):
396
 
            vals.update({'distribution_id': distrib.id,})
397
 
            # create lines
398
 
            for distrib_lines in [distrib.funding_pool_lines, distrib.free_1_lines, distrib.free_2_lines]:
399
 
                for distrib_line in distrib_lines:
400
 
                    context.update({'date': source_date or date}) # for amount computing
401
 
                    anal_amount = (distrib_line.percentage * amount) / 100
402
 
                    vals.update({
403
 
                        'amount': -1 * self.pool.get('res.currency').compute(cr, uid, currency_id, company_currency, 
404
 
                            anal_amount, round=False, context=context),
405
 
                        'amount_currency': -1 * anal_amount,
406
 
                        'account_id': distrib_line.analytic_id.id,
407
 
                        'cost_center_id': False,
408
 
                        'destination_id': False,
409
 
                        'distrib_line_id': '%s,%s'%(distrib_line._name, distrib_line.id),
410
 
                    })
411
 
                    # Update values if we come from a funding pool
412
 
                    if distrib_line._name == 'funding.pool.distribution.line':
413
 
                        vals.update({'cost_center_id': distrib_line.cost_center_id and distrib_line.cost_center_id.id or False, 
414
 
                            'destination_id': distrib_line.destination_id and distrib_line.destination_id.id or False,})
415
 
                    # create analytic line
416
 
                    al_id = self.pool.get('account.analytic.line').create(cr, uid, vals, context=context)
417
 
                    res.append(al_id)
418
 
        return res
 
79
    def analytic_state_from_info(self, cr, uid, account_id, destination_id, cost_center_id, analytic_id, context=None):
 
80
        """
 
81
        Give analytic state from the given information.
 
82
        Return result and some info if needed.
 
83
        """
 
84
        # Checks
 
85
        if context is None:
 
86
            context = {}
 
87
        # Prepare some values
 
88
        res = 'valid'
 
89
        info = ''
 
90
        ana_obj = self.pool.get('account.analytic.account')
 
91
        account = self.pool.get('account.account').browse(cr, uid, account_id, context=context)
 
92
        fp = ana_obj.browse(cr, uid, analytic_id, context=context)
 
93
        try:
 
94
            fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
 
95
        except ValueError:
 
96
            fp_id = 0
 
97
        is_private_fund = False
 
98
        if analytic_id == fp_id:
 
99
            is_private_fund = True
 
100
        # DISTRIBUTION VERIFICATION
 
101
        # Check account user_type
 
102
        if account.user_type_code != 'expense':
 
103
            return res, _('Not an expense account')
 
104
        # Check that destination is compatible with account
 
105
        if destination_id not in [x.id for x in account.destination_ids]:
 
106
            return 'invalid', _('Destination not compatible with account')
 
107
        if not is_private_fund:
 
108
            # Check that cost center is compatible with FP (except if FP is MSF Private Fund)
 
109
            if cost_center_id not in [x.id for x in fp.cost_center_ids]:
 
110
                return 'invalid', _('Cost Center not compatible with FP')
 
111
            # Check that tuple account/destination is compatible with FP (except if FP is MSF Private Fund):
 
112
            if (account_id, destination_id) not in [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in fp.tuple_destination_account_ids]:
 
113
                return 'invalid', _('account/destination tuple not compatible with given FP analytic account')
 
114
        return res, info
419
115
 
420
116
analytic_distribution()
421
117
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: