22
22
from osv import osv
23
23
from osv import fields
24
24
from tools.translate import _
25
from time import strftime
27
26
class analytic_line(osv.osv):
28
_name = "account.analytic.line"
29
27
_inherit = "account.analytic.line"
31
def _get_fake_is_fp_compat_with(self, cr, uid, ids, field_name, args, context=None):
33
Fake method for 'is_fp_compat_with' field
40
def _search_is_fp_compat_with(self, cr, uid, obj, name, args, context=None):
42
Return domain that permit to give all analytic line compatible with a given FP.
47
# We just support '=' operator
50
raise osv.except_osv(_('Warning'), _('Some search args are missing!'))
51
if arg[1] not in ['=',]:
52
raise osv.except_osv(_('Warning'), _('This filter is not implemented yet!'))
54
raise osv.except_osv(_('Warning'), _('Some search args are missing!'))
55
analytic_account = self.pool.get('account.analytic.account').browse(cr, uid, arg[2])
56
tuple_list = [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in analytic_account.tuple_destination_account_ids]
57
cost_center_ids = [x and x.id for x in analytic_account.cost_center_ids]
58
for cc in cost_center_ids:
64
res.append(('cost_center_id', '=', cc))
65
res.append(('general_account_id', '=', t[0]))
66
res.append(('destination_id', '=', t[1]))
69
def _journal_type_get(self, cr, uid, context=None):
73
return self.pool.get('account.analytic.journal').get_journal_type(cr, uid, context)
75
def _get_entry_sequence(self, cr, uid, ids, field_names, args, context=None):
77
Give right entry sequence. Either move_id.move_id.name,
78
or commitment_line_id.commit_id.name, or
79
if the line was imported, the stored name
84
for l in self.browse(cr, uid, ids, context):
86
res[l.id] = l.entry_sequence
90
res[l.id] = l.move_id.move_id.name
91
elif l.commitment_line_id:
92
res[l.id] = l.commitment_line_id.commit_id.name
93
elif l.imported_commitment:
94
res[l.id] = l.imported_entry_sequence
97
# on create the value is inserted by a sql query, so we can retreive it after the insertion
98
# the field has store=True so we don't create a loop
99
# on write the value is not updated by the query, the method always returns the value set at creation
100
res[l.id] = l.entry_sequence
103
def _get_period_id(self, cr, uid, ids, field_name, args, context=None):
105
Fetch period_id from:
112
# Prepare some values
114
period_obj = self.pool.get('account.period')
115
for al in self.browse(cr, uid, ids, context):
117
# UTP-943: Since this ticket, we search period regarding analytic line posting date.
118
period_ids = period_obj.get_period_from_date(cr, uid, date=al.date)
120
res[al.id] = period_ids[0]
123
def _search_period_id(self, cr, uid, obj, name, args, context=None):
125
Search period regarding date.
126
First fetch period date_start and date_stop.
127
Then check that analytic line have a posting date bewteen these two date.
128
Finally do this check as "OR" for each given period.
131
['&', ('date', '>=', '2013-01-01'), ('date', '<=', '2013-01-31')]
132
- January + February:
133
['|', '&', ('date', '>=', '2013-01-01'), ('date', '<=', '2013-01-31'), '&', ('date', '>=', '2013-02-01'), ('date', '<=', '2013-02-28')]
134
- January + February + March
135
['|', '|', '&', ('date', '>=', '2013-01-01'), ('date', '<=', '2013-01-31'), '&', ('date', '>=', '2013-02-01'), ('date', '<=', '2013-02-28'), '&', ('date', '>=', '2013-03-01'), ('date', '<=', '2013-03-31')]
143
period_obj = self.pool.get('account.period')
145
if len(arg) == 3 and arg[1] in ['=', 'in']:
147
if isinstance(periods, (int, long)):
150
for _ in range(len(periods) - 1):
153
period = period_obj.browse(cr, uid, [p_id])[0]
155
new_args.append(('date', '>=', period.date_start))
156
new_args.append(('date', '<=', period.date_stop))
159
def _get_from_commitment_line(self, cr, uid, ids, field_name, args, context=None):
161
Check if line comes from a 'engagement' journal type. If yes, True. Otherwise False.
166
for al in self.browse(cr, uid, ids, context=context):
168
if al.journal_id.type == 'engagement':
172
def _get_is_unposted(self, cr, uid, ids, field_name, args, context=None):
174
Check journal entry state. If unposted: True, otherwise False.
175
A line that comes from a commitment cannot be posted. So it's always to False.
180
for al in self.browse(cr, uid, ids, context=context):
182
if al.move_state != 'posted' and al.journal_id.type != 'engagement':
187
'commitment_line_id': fields.many2one('account.commitment.line', string='Commitment Voucher Line', ondelete='cascade'),
188
'is_fp_compat_with': fields.function(_get_fake_is_fp_compat_with, fnct_search=_search_is_fp_compat_with, method=True, type="char", size=254, string="Is compatible with some FP?"),
189
'move_state': fields.related('move_id', 'move_id', 'state', type='selection', size=64, relation="account.move.line", selection=[('draft', 'Unposted'), ('posted', 'Posted')], string='Journal Entry state', readonly=True, help="Indicates that this line come from an Unposted Journal Entry."),
190
'journal_type': fields.related('journal_id', 'type', type='selection', selection=_journal_type_get, string="Journal Type", readonly=True, \
191
help="Indicates the Journal Type of the Analytic journal item"),
192
'entry_sequence': fields.function(_get_entry_sequence, method=True, type='text', string="Entry Sequence", readonly=True, store=True),
193
'period_id': fields.function(_get_period_id, fnct_search=_search_period_id, method=True, string="Period", readonly=True, type="many2one", relation="account.period", store=False),
194
'from_commitment_line': fields.function(_get_from_commitment_line, method=True, type='boolean', string="Commitment?"),
195
'is_unposted': fields.function(_get_is_unposted, method=True, type='boolean', string="Unposted?"),
196
'imported_commitment': fields.boolean(string="From imported commitment?"),
197
'imported_entry_sequence': fields.text("Imported Entry Sequence"),
201
'imported_commitment': lambda *a: False,
204
def create(self, cr, uid, vals, context=None):
30
"distribution_id": fields.many2one('analytic.distribution', 'Analytic Distribution'),
33
def _check_date(self, cr, uid, vals, context={}):
35
Check if given account_id is active for given date
39
if not 'account_id' in vals:
40
raise osv.except_osv(_('Error'), _('No account_id found in given values!'))
41
if 'date' in vals and vals['date'] is not False:
42
account_obj = self.pool.get('account.analytic.account')
43
account = account_obj.browse(cr, uid, vals['account_id'], context=context)
44
if vals['date'] < account.date_start \
45
or (account.date != False and \
46
vals['date'] >= account.date):
47
raise osv.except_osv(_('Error !'), _("The analytic account selected '%s' is not active.") % account.name)
49
def create(self, cr, uid, vals, context={}):
206
51
Check date for given date and given account_id
212
res = super(analytic_line, self).create(cr, uid, vals, context=context)
213
# Check soft/hard closed contract
214
sql = """SELECT fcc.id
215
FROM financing_contract_funding_pool_line fcfpl, account_analytic_account a, financing_contract_format fcf, financing_contract_contract fcc
216
WHERE fcfpl.funding_pool_id = a.id
217
AND fcfpl.contract_id = fcf.id
218
AND fcc.format_id = fcf.id
220
AND fcc.state in ('soft_closed', 'hard_closed');"""
221
cr.execute(sql, tuple([vals.get('account_id')]))
222
sql_res = cr.fetchall()
224
account = self.pool.get('account.analytic.account').browse(cr, uid, vals.get('account_id'))
225
contract = self.pool.get('financing.contract.contract').browse(cr, uid, sql_res[0][0])
226
raise osv.except_osv(_('Warning'), _('Selected Funding Pool analytic account (%s) is blocked by a soft/hard closed contract: %s') % (account and account.code or '', contract and contract.name or ''))
229
def update_account(self, cr, uid, ids, account_id, date=False, context=None):
231
Update account on given analytic lines with account_id on given date
236
if isinstance(ids, (int, long)):
241
date = strftime('%Y-%m-%d')
243
account = self.pool.get('account.analytic.account').browse(cr, uid, [account_id], context)[0]
244
context.update({'from': 'mass_reallocation'}) # this permits reallocation to be accepted when rewrite analaytic lines
245
correction_journal_ids = self.pool.get('account.analytic.journal').search(cr, uid, [('type', '=', 'correction'), ('is_current_instance', '=', True)])
246
correction_journal_id = correction_journal_ids and correction_journal_ids[0] or False
247
if not correction_journal_id:
248
raise osv.except_osv(_('Error'), _('No analytic journal found for corrections!'))
250
for aline in self.browse(cr, uid, ids, context=context):
251
if account.category in ['OC', 'DEST']:
252
# Period verification
253
period = aline.move_id and aline.move_id.period_id or False
254
# Prepare some values
255
fieldname = 'cost_center_id'
256
if account.category == 'DEST':
257
fieldname = 'destination_id'
258
# if period is not closed, so override line.
259
if period and period.state not in ['done', 'mission-closed']:
260
# Update account # Date: UTP-943 speak about original date for non closed periods
261
self.write(cr, uid, [aline.id], {fieldname: account_id, 'date': aline.date,
262
'source_date': aline.source_date or aline.date}, context=context)
263
# else reverse line before recreating them with right values
266
rev_ids = self.pool.get('account.analytic.line').reverse(cr, uid, [aline.id], posting_date=date)
267
# UTP-943: Shoud have a correction journal on these lines
268
self.pool.get('account.analytic.line').write(cr, uid, rev_ids, {'journal_id': correction_journal_id, 'is_reversal': True, 'reversal_origin': aline.id, 'last_corrected_id': False})
269
# UTP-943: Check that period is open
270
correction_period_ids = self.pool.get('account.period').get_period_from_date(cr, uid, date, context=context)
271
if not correction_period_ids:
272
raise osv.except_osv(_('Error'), _('No period found for this date: %s') % (date,))
273
for p in self.pool.get('account.period').browse(cr, uid, correction_period_ids, context=context):
274
if p.state != 'draft':
275
raise osv.except_osv(_('Error'), _('Period (%s) is not open.') % (p.name,))
276
# then create new lines
277
cor_ids = self.pool.get('account.analytic.line').copy(cr, uid, aline.id, {fieldname: account_id, 'date': date,
278
'source_date': aline.source_date or aline.date, 'journal_id': correction_journal_id}, context=context)
279
self.pool.get('account.analytic.line').write(cr, uid, cor_ids, {'last_corrected_id': aline.id})
280
# finally flag analytic line as reallocated
281
self.pool.get('account.analytic.line').write(cr, uid, [aline.id], {'is_reallocated': True})
284
self.write(cr, uid, [aline.id], {'account_id': account_id}, context=context)
285
# Set line as corrected upstream if we are in COORDO/HQ instance
287
self.pool.get('account.move.line').corrected_upstream_marker(cr, uid, [aline.move_id.id], context=context)
290
def check_analytic_account(self, cr, uid, ids, account_id, context=None):
292
Analytic distribution validity verification with given account for given ids.
293
Return all valid ids.
298
if isinstance(ids, (int, long)):
301
account = self.pool.get('account.analytic.account').read(cr, uid, account_id, ['category', 'date_start', 'date'], context=context)
302
account_type = account and account.get('category', False) or False
307
msf_private_fund = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution',
308
'analytic_account_msf_private_funds')[1]
311
expired_date_ids = []
312
date_start = account and account.get('date_start', False) or False
313
date_stop = account and account.get('date', False) or False
314
# Date verification for all lines and fetch all necessary elements sorted by analytic distribution
315
for aline in self.browse(cr, uid, ids):
316
# UTP-800: Change date comparison regarding FP. If FP, use document date. Otherwise use date.
317
aline_cmp_date = aline.date
318
if account_type == 'FUNDING':
319
aline_cmp_date = aline.document_date
320
# Add line to expired_date if date is not in date_start - date_stop
321
if (date_start and aline_cmp_date < date_start) or (date_stop and aline_cmp_date > date_stop):
322
expired_date_ids.append(aline.id)
323
# Process regarding account_type
324
if account_type == 'OC':
325
for aline in self.browse(cr, uid, ids):
327
# - the line doesn't have any draft/open contract
328
check_accounts = self.pool.get('account.analytic.account').is_blocked_by_a_contract(cr, uid, [aline.account_id.id])
329
if check_accounts and aline.account_id.id in check_accounts:
332
if aline.account_id and aline.account_id.id == msf_private_fund:
334
elif aline.account_id and aline.cost_center_id and aline.account_id.cost_center_ids:
335
if account_id in [x and x.id for x in aline.account_id.cost_center_ids] or aline.account_id.id == msf_private_fund:
337
elif account_type == 'FUNDING':
338
fp = self.pool.get('account.analytic.account').read(cr, uid, account_id, ['cost_center_ids', 'tuple_destination_account_ids'], context=context)
339
cc_ids = fp and fp.get('cost_center_ids', []) or []
340
tuple_destination_account_ids = fp and fp.get('tuple_destination_account_ids', []) or []
341
tuple_list = [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in self.pool.get('account.destination.link').browse(cr, uid, tuple_destination_account_ids)]
342
# Browse all analytic line to verify them
343
for aline in self.browse(cr, uid, ids):
345
# - the line doesn't have any draft/open contract
346
check_accounts = self.pool.get('account.analytic.account').is_blocked_by_a_contract(cr, uid, [aline.account_id.id])
347
if check_accounts and aline.account_id.id in check_accounts:
349
# No verification if account is MSF Private Fund because of its compatibility with all elements.
350
if account_id == msf_private_fund:
354
# - the line have a cost_center_id field (we expect it's a line with a funding pool account)
355
# - the cost_center is in compatible cost center from the new funding pool
356
# - the general account is in compatible account/destination tuple
357
# - the destination is in compatible account/destination tuple
358
if aline.cost_center_id and aline.cost_center_id.id in cc_ids and aline.general_account_id and aline.destination_id and (aline.general_account_id.id, aline.destination_id.id) in tuple_list:
360
elif account_type == "DEST":
361
for aline in self.browse(cr, uid, ids, context=context):
362
if aline.general_account_id and account_id in [x.id for x in aline.general_account_id.destination_ids]:
365
# Case of FREE1 and FREE2 lines
368
# Delete elements that are in expired_date_ids
369
for e in expired_date_ids:
374
def check_dest_cc_fp_compatibility(self, cr, uid, ids,
375
dest_id=False, cc_id=False, fp_id=False,
376
from_import=False, from_import_general_account_id=False,
377
from_import_posting_date=False,
380
check compatibility of new dest/cc/fp to reallocate
381
:return list of not compatible entries tuples
382
:rtype: list of tuples [(id, entry_sequence, reason), ]
384
def check_date(aaa_br, posting_date):
385
if aaa_br.date_start and aaa_br.date:
386
return aaa_br.date > posting_date >= aaa_br.date_start or False
387
elif aaa_br.date_start:
388
return posting_date >= aaa_br.date_start or False
391
def check_entry(id, entry_sequence,
392
general_account_br, posting_date,
393
new_dest_id, new_dest_br,
394
new_cc_id, new_cc_br,
395
new_fp_id, new_fp_br):
396
if not general_account_br.is_analytic_addicted:
397
res.append((id, entry_sequence, ''))
400
# check cost center with general account
401
dest_ids = [d.id for d in general_account_br.destination_ids]
402
if not new_dest_id in dest_ids:
403
# not compatible with general account
404
res.append((id, entry_sequence, 'DEST'))
407
# check funding pool (expect for MSF Private Fund)
408
if not new_fp_id == msf_pf_id: # all OK for MSF Private Fund
409
# - cost center and funding pool compatibility
410
cc_ids = [cc.id for cc in new_fp_br.cost_center_ids]
411
if not new_cc_id in cc_ids:
412
# not compatible with CC
413
res.append((id, entry_sequence, 'CC'))
416
# - destination / account
417
acc_dest = (general_account_br.id, new_dest_id)
418
if acc_dest not in [x.account_id and x.destination_id and \
419
(x.account_id.id, x.destination_id.id) \
420
for x in new_fp_br.tuple_destination_account_ids]:
421
# not compatible with dest/account
422
res.append((id, entry_sequence, 'account/dest'))
426
if not check_date(new_dest_br, posting_date):
427
res.append((id, entry_sequence, 'DEST date'))
429
if not check_date(new_cc_br, posting_date):
430
res.append((id, entry_sequence, 'CC date'))
432
if new_fp_id != msf_pf_id and not \
433
check_date(new_fp_br, posting_date):
434
res.append((id, entry_sequence, 'FP date'))
441
if not dest_id or not cc_id or not fp_id or \
442
not from_import_general_account_id or \
443
not from_import_posting_date:
444
return [(False, '', '')] # tripplet required at import
447
if isinstance(ids, (int, long)):
449
if not dest_id and not cc_id and not fp_id:
450
return [(id, '', '') for id in ids] # all uncompatible
454
aaa_obj = self.pool.get('account.analytic.account')
456
dest_br = aaa_obj.browse(cr, uid, dest_id, context=context)
460
cc_br = aaa_obj.browse(cr, uid, cc_id, context=context)
464
fp_br = aaa_obj.browse(cr, uid, fp_id, context=context)
469
msf_pf_id = self.pool.get('ir.model.data').get_object_reference(cr, uid,
470
'analytic_distribution', 'analytic_account_msf_private_funds')[1]
473
account_br = self.pool.get('account.account').browse(cr, uid,
474
from_import_general_account_id, context=context)
475
check_entry(False, '', account_br, from_import_posting_date,
476
dest_id, dest_br, cc_id, cc_br, fp_id, fp_br)
478
for self_br in self.browse(cr, uid, ids, context=context):
479
new_dest_id = dest_id or self_br.destination_id.id
480
new_dest_br = dest_br or self_br.destination_id
481
new_cc_id = cc_id or self_br.cost_center_id.id
482
new_cc_br = cc_br or self_br.cost_center_id
483
new_fp_id = fp_id or self_br.account_id.id
484
new_fp_br = fp_br or self_br.account_id
486
check_entry(self_br.id, self_br.entry_sequence,
487
self_br.general_account_id, self_br.date,
488
new_dest_id, new_dest_br,
489
new_cc_id, new_cc_br,
490
new_fp_id, new_fp_br)
53
self._check_date(cr, uid, vals)
54
return super(analytic_line, self).create(cr, uid, vals, context=context)
56
def write(self, cr, uid, ids, vals, context={}):
58
Verify date for all given ids with account
62
if isinstance(ids, (int, long)):
65
if not 'account_id' in vals:
66
line = self.browse(cr, uid, [id], context=context)
67
account_id = line and line[0] and line[0].account_id.id or False
68
vals.update({'account_id': account_id})
69
self._check_date(cr, uid, vals, context=context)
70
return super(analytic_line, self).write(cr, uid, ids, vals, context=context)
495
73
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: