2
# -*- coding: utf-8 -*-
3
##############################################################################
5
# OpenERP, Open Source Management Solution
6
# Copyright (C) 2011 TeMPO Consulting, MSF. All Rights Reserved
7
# Developer: Olivier DOSSMANN
9
# This program is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU Affero General Public License as
11
# published by the Free Software Foundation, either version 3 of the
12
# License, or (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU Affero General Public License for more details.
19
# You should have received a copy of the GNU Affero General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
##############################################################################
25
from osv import fields
26
from tools.translate import _
27
from time import strftime
28
import decimal_precision as dp
29
from account_tools import get_period_from_date
30
from tools.misc import flatten
32
class account_commitment(osv.osv):
33
_name = 'account.commitment'
34
_description = "Account Commitment Voucher"
37
def _get_total(self, cr, uid, ids, name, args, context=None):
39
Give total of given commitments
44
if isinstance(ids, (int, long)):
49
for co in self.browse(cr, uid, ids, context=context):
51
for line in co.line_ids:
52
res[co.id] += line.amount
56
'journal_id': fields.many2one('account.analytic.journal', string="Journal", readonly=True, required=True),
57
'name': fields.char(string="Number", size=64, readonly=True, required=True),
58
'currency_id': fields.many2one('res.currency', string="Currency", readonly=True, required=True),
59
'partner_id': fields.many2one('res.partner', string="Supplier", readonly=True, required=True),
60
'period_id': fields.many2one('account.period', string="Period", readonly=True, required=True),
61
'state': fields.selection([('draft', 'Draft'), ('open', 'Validated'), ('done', 'Done')], readonly=True, string="State", required=True),
62
'date': fields.date(string="Commitment Date", readonly=True, required=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}),
63
'line_ids': fields.one2many('account.commitment.line', 'commit_id', string="Commitment Voucher Lines"),
64
'total': fields.function(_get_total, type='float', method=True, digits_compute=dp.get_precision('Account'), readonly=True, string="Total"),
65
'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),
66
'type': fields.selection([('manual', 'Manual'), ('external', 'Automatic - External supplier'), ('esc', 'Automatic - ESC supplier')], string="Type", readonly=True),
67
'from_yml_test': fields.boolean('Only used to pass addons unit test', readonly=True, help='Never set this field to true !'),
68
'notes': fields.text(string="Comment"),
72
'name': lambda s, cr, uid, c: s.pool.get('ir.sequence').get(cr, uid, 'account.commitment') or '',
73
'state': lambda *a: 'draft',
74
'date': lambda *a: strftime('%Y-%m-%d'),
75
'type': lambda *a: 'manual',
76
'from_yml_test': lambda *a: False,
77
'journal_id': lambda s, cr, uid, c: s.pool.get('account.analytic.journal').search(cr, uid, [('type', '=', 'engagement')], limit=1, context=c)[0]
80
def create(self, cr, uid, vals, context=None):
82
Update period_id regarding date.
87
if not 'period_id' in vals:
88
period_ids = get_period_from_date(self, cr, uid, vals.get('date', strftime('%Y-%m-%d')), context=context)
89
vals.update({'period_id': period_ids and period_ids[0]})
90
return super(account_commitment, self).create(cr, uid, vals, context=context)
92
def write(self, cr, uid, ids, vals, context=None):
94
Update analytic lines date if date in vals for validated commitment voucher.
99
if isinstance(ids, (int, long)):
101
# Browse elements if 'date' in vals
102
if vals.get('date', False):
103
date = vals.get('date')
104
period_ids = get_period_from_date(self, cr, uid, date, context=context)
105
vals.update({'period_id': period_ids and period_ids[0]})
106
for c in self.browse(cr, uid, ids, context=context):
107
if c.state == 'open':
108
for cl in c.line_ids:
109
# Verify that date is compatible with all analytic account from distribution
110
if cl.analytic_distribution_id:
111
distrib = cl.analytic_distribution_id
112
elif cl.commit_id and cl.commit_id.analytic_distribution_id:
113
distrib = cl.commit_id.analytic_distribution_id
115
raise osv.except_osv(_('Warning'), _('No analytic distribution found for %s %s') % (cl.account_id.code, cl.initial_amount))
116
for distrib_lines in [distrib.cost_center_lines, distrib.funding_pool_lines, distrib.free_1_lines, distrib.free_2_lines]:
117
for distrib_line in distrib_lines:
118
if (distrib_line.analytic_id.date_start and date < distrib_line.analytic_id.date_start) or (distrib_line.analytic_id.date and date > distrib_line.analytic_id.date):
119
raise osv.except_osv(_('Error'), _('The analytic account %s is not active for given date.') % (distrib_line.analytic_id.name,))
120
self.pool.get('account.analytic.line').write(cr, uid, [x.id for x in cl.analytic_lines], {'date': date, 'source_date': date}, context=context)
122
res = super(account_commitment, self).write(cr, uid, ids, vals, context=context)
125
def copy(self, cr, uid, id, default=None, context=None):
127
Copy analytic_distribution
134
# Update default values
136
'name': self.pool.get('ir.sequence').get(cr, uid, 'account.commitment'),
140
res = super(account_commitment, self).copy(cr, uid, id, default, context)
141
# Update analytic distribution
143
c = self.browse(cr, uid, res, context=context)
144
if res and c.analytic_distribution_id:
145
new_distrib_id = self.pool.get('analytic.distribution').copy(cr, uid, c.analytic_distribution_id.id, {}, context=context)
147
self.write(cr, uid, [res], {'analytic_distribution_id': new_distrib_id}, context=context)
150
def unlink(self, cr, uid, ids, context=None):
152
Only delete "done" state commitments
157
if isinstance(ids, (int, long)):
160
# Check that elements are in done state
161
for co in self.browse(cr, uid, ids):
162
if co.state == 'done':
163
new_ids.append(co.id)
164
# Give user a message if no done commitments found
166
raise osv.except_osv(_('Warning'), _('You can only delete done commitments!'))
167
return super(account_commitment, self).unlink(cr, uid, new_ids, context)
169
def button_analytic_distribution(self, cr, uid, ids, context=None):
171
Launch analytic distribution wizard on a commitment
176
if isinstance(ids, (int, long)):
178
# Prepare some values
179
commitment = self.browse(cr, uid, ids[0], context=context)
180
amount = commitment.total or 0.0
181
# Search elements for currency
182
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
183
currency = commitment.currency_id and commitment.currency_id.id or company_currency
184
# Get analytic_distribution_id
185
distrib_id = commitment.analytic_distribution_id and commitment.analytic_distribution_id.id
186
# Prepare values for wizard
188
'total_amount': amount,
189
'commitment_id': commitment.id,
190
'currency_id': currency or False,
194
vals.update({'distribution_id': distrib_id,})
196
wiz_obj = self.pool.get('analytic.distribution.wizard')
197
wiz_id = wiz_obj.create(cr, uid, vals, context=context)
198
# Update some context values
205
'name': 'Global analytic distribution',
206
'type': 'ir.actions.act_window',
207
'res_model': 'analytic.distribution.wizard',
215
def button_compute(self, cr, uid, ids, context=None):
217
Compute commitment voucher total.
219
# trick to refresh view and update total amount
220
return self.write(cr, uid, ids, [], context=context)
222
def get_engagement_lines(self, cr, uid, ids, context=None):
224
Return all engagement lines from given commitments (in context)
229
if context.get('active_ids', False):
230
ids = context.get('active_ids')
231
if isinstance(ids, (int, long)):
233
# Prepare some values
236
for co in self.browse(cr, uid, ids):
237
for line in co.line_ids:
238
if line.analytic_lines:
239
valid_ids.append([x.id for x in line.analytic_lines])
240
valid_ids = flatten(valid_ids)
241
domain = [('id', 'in', valid_ids), ('account_id.category', '=', 'FUNDING')]
242
# Permit to only display engagement lines
243
context.update({'search_default_engagements': 1, 'display_fp': True})
245
'name': 'Analytic Entries',
246
'type': 'ir.actions.act_window',
247
'res_model': 'account.analytic.line',
249
'view_mode': 'tree,form',
255
def onchange_date(self, cr, uid, ids, date, period_id=False, context=None):
257
Update period regarding given date
264
# Prepare some values
266
periods = get_period_from_date(self, cr, uid, date, context=context)
268
vals['period_id'] = periods[0]
269
return {'value': vals}
271
def create_analytic_lines(self, cr, uid, ids, context=None):
273
Create analytic line for given commitment voucher.
278
if isinstance(ids, (int, long)):
281
for c in self.browse(cr, uid, ids, context=context):
282
for cl in c.line_ids:
283
# Continue if we come from yaml tests
284
if c.from_yml_test or cl.from_yml_test:
286
# Verify that analytic distribution is present
287
if cl.analytic_distribution_state != 'valid':
288
raise osv.except_osv(_('Error'), _('Analytic distribution is not valid for account "%s %s".') %
289
(cl.account_id and cl.account_id.code, cl.account_id and cl.account_id.name))
290
# Take analytic distribution either from line or from commitment voucher
291
distrib_id = cl.analytic_distribution_id and cl.analytic_distribution_id.id or c.analytic_distribution_id and c.analytic_distribution_id.id or False
293
raise osv.except_osv(_('Error'), _('No analytic distribution found!'))
294
# Search if analytic lines exists for this commitment voucher line
295
al_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', '=', cl.id)], context=context)
297
# Create engagement journal lines
298
self.pool.get('analytic.distribution').create_analytic_lines(cr, uid, [distrib_id], c.name, c.date,
299
cl.amount, c.journal_id and c.journal_id.id, c.currency_id and c.currency_id.id, c.purchase_id and c.purchase_id.name or False,
300
c.date, cl.account_id and cl.account_id.id or False, False, False, cl.id, context=context)
303
def action_commitment_open(self, cr, uid, ids, context=None):
305
To do when we validate a commitment.
310
if isinstance(ids, (int, long)):
312
# Browse commitments and create analytic lines
313
self.create_analytic_lines(cr, uid, ids, context=context)
314
# Validate commitment voucher
315
return self.write(cr, uid, ids, {'state': 'open'}, context=context)
317
def action_commitment_done(self, cr, uid, ids, context=None):
319
To do when a commitment is done.
324
if isinstance(ids, (int, long)):
327
for c in self.browse(cr, uid, ids, context=context):
328
# Search analytic lines that have commitment line ids
329
search_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', 'in', [x.id for x in c.line_ids])], context=context)
331
res = self.pool.get('account.analytic.line').unlink(cr, uid, search_ids, context=context)
332
# And finally update commitment voucher state and lines amount
334
raise osv.except_osv(_('Error'), _('An error occured on engagement lines deletion.'))
335
self.pool.get('account.commitment.line').write(cr, uid, [x.id for x in c.line_ids], {'amount': 0}, context=context)
336
self.write(cr, uid, [c.id], {'state':'done'}, context=context)
341
class account_commitment_line(osv.osv):
342
_name = 'account.commitment.line'
343
_description = "Account Commitment Voucher Line"
346
def _get_distribution_state(self, cr, uid, ids, name, args, context=None):
348
Get state of distribution:
349
- if compatible with the commitment voucher line, then "valid"
350
- if no distribution, take a tour of commitment voucher distribution, if compatible, then "valid"
351
- if no distribution on commitment voucher line and commitment voucher, then "none"
352
- all other case are "invalid"
357
if isinstance(ids, (int, long)):
359
# Prepare some values
361
# Browse all given lines
362
for line in self.browse(cr, uid, ids, context=context):
363
if line.from_yml_test:
364
res[line.id] = 'valid'
366
res[line.id] = self.pool.get('analytic.distribution')._get_distribution_state(cr, uid, line.analytic_distribution_id.id, line.commit_id.analytic_distribution_id.id, line.account_id.id)
369
def _have_analytic_distribution_from_header(self, cr, uid, ids, name, arg, context=None):
371
If Commitment have an analytic distribution, return False, else return True
376
if isinstance(ids, (int, long)):
379
for co in self.browse(cr, uid, ids, context=context):
381
if co.analytic_distribution_id:
386
'account_id': fields.many2one('account.account', string="Account", required=True),
387
'amount': fields.float(string="Amount left", digits_compute=dp.get_precision('Account'), required=False),
388
'initial_amount': fields.float(string="Initial amount", digits_compute=dp.get_precision('Account'), required=True),
389
'commit_id': fields.many2one('account.commitment', string="Commitment Voucher", on_delete="cascade"),
390
'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),
391
'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',
392
selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],
393
string="Distribution state", help="Informs from distribution state among 'none', 'valid', 'invalid."),
394
'have_analytic_distribution_from_header': fields.function(_have_analytic_distribution_from_header, method=True, type='boolean',
395
string='Header Distrib.?'),
396
'from_yml_test': fields.boolean('Only used to pass addons unit test', readonly=True, help='Never set this field to true !'),
397
'analytic_lines': fields.one2many('account.analytic.line', 'commitment_line_id', string="Analytic Lines"),
398
'first': fields.boolean(string="Is not created?", help="Useful for onchange method for views. Should be False after line creation.",
403
'initial_amount': lambda *a: 0.0,
404
'amount': lambda *a: 0.0,
405
'from_yml_test': lambda *a: False,
406
'first': lambda *a: True,
409
def onchange_initial_amount(self, cr, uid, ids, first, amount):
412
# Prepare some values
416
res['value'] = {'amount': amount}
419
def update_analytic_lines(self, cr, uid, ids, amount, account_id=False, context=None):
421
Update analytic lines from given commitment lines with an ugly method: delete all analytic lines and recreate them…
426
if isinstance(ids, (int, long)):
428
# Prepare some values
429
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
430
for cl in self.browse(cr, uid, ids, context=context):
431
# Browse distribution
432
distrib_id = cl.analytic_distribution_id and cl.analytic_distribution_id.id or False
434
distrib_id = cl.commit_id and cl.commit_id.analytic_distribution_id and cl.commit_id.analytic_distribution_id.id or False
436
analytic_line_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', '=', cl.id)], context=context)
437
self.pool.get('account.analytic.line').unlink(cr, uid, analytic_line_ids, context=context)
438
ref = cl.commit_id and cl.commit_id.purchase_id and cl.commit_id.purchase_id.name or ''
439
self.pool.get('analytic.distribution').create_analytic_lines(cr, uid, [distrib_id], cl.commit_id and cl.commit_id.name or 'Commitment voucher line', cl.commit_id.date, amount,
440
cl.commit_id.journal_id.id, cl.commit_id.currency_id.id, ref, cl.commit_id.date, account_id or cl.account_id.id, move_id=False, invoice_line_id=False,
441
commitment_line_id=cl.id, context=context)
444
def create(self, cr, uid, vals, context=None):
446
Verify that given account_id (in vals) is not 'view'.
447
Update initial amount with those given by 'amount' field.
453
# Change 'first' value to False (In order view correctly displayed)
454
if not 'first' in vals:
455
vals.update({'first': False})
456
# Copy initial_amount to amount
457
vals.update({'amount': vals.get('initial_amount', 0.0)})
458
if 'account_id' in vals:
459
account_id = vals.get('account_id')
460
account = self.pool.get('account.account').browse(cr, uid, [account_id], context=context)[0]
461
if account.type in ['view']:
462
raise osv.except_osv(_('Error'), _("You cannot create a commitment voucher line on a 'view' account type!"))
463
# Verify amount validity
464
if 'amount' in vals and vals.get('amount', 0.0) < 0.0:
465
raise osv.except_osv(_('Warning'), _('Amount Left should be equal or superior to 0!'))
466
if 'initial_amount' in vals and vals.get('initial_amount', 0.0) <= 0.0:
467
raise osv.except_osv(_('Warning'), _('Initial Amount should be superior to 0!'))
468
if 'initial_amount' in vals and 'amount' in vals:
469
if vals.get('initial_amount') < vals.get('amount'):
470
raise osv.except_osv(_('Warning'), _('Initial Amount should be superior to Amount Left'))
471
res = super(account_commitment_line, self).create(cr, uid, vals, context={})
473
for cl in self.browse(cr, uid, [res], context=context):
474
if 'amount' in vals and cl.commit_id and cl.commit_id.state and cl.commit_id.state == 'open':
475
self.update_analytic_lines(cr, uid, [cl.id], vals.get('amount'), context=context)
478
def write(self, cr, uid, ids, vals, context=None):
480
Verify that given account_id is not 'view'.
481
Update initial_amount if amount in vals and type is 'manual' and state is 'draft'.
482
Update analytic distribution if amount in vals.
488
if 'account_id' in vals:
489
account_id = vals.get('account_id')
490
account = self.pool.get('account.account').browse(cr, uid, [account_id], context=context)[0]
491
if account.type in ['view']:
492
raise osv.except_osv(_('Error'), _("You cannot write a commitment voucher line on a 'view' account type!"))
493
# Verify amount validity
494
if 'amount' in vals and vals.get('amount', 0.0) < 0.0:
495
raise osv.except_osv(_('Warning'), _('Amount Left should be equal or superior to 0!'))
496
if 'initial_amount' in vals and vals.get('initial_amount', 0.0) <= 0.0:
497
raise osv.except_osv(_('Warning'), _('Initial Amount should be superior to 0!'))
498
# Update analytic distribution if needed and initial_amount
499
for line in self.browse(cr, uid, ids, context=context):
500
# verify that initial amount is superior to amount left
501
message = _('Initial Amount should be superior to Amount Left')
502
if 'amount' in vals and 'initial_amount' in vals:
503
if vals.get('initial_amount') < vals.get('amount'):
504
raise osv.except_osv(_('Warning'), message)
505
elif 'amount' in vals:
506
if line.initial_amount < vals.get('amount'):
507
raise osv.except_osv(_('Warning'), message)
508
elif 'initial_amount' in vals:
509
if vals.get('initial_amount') < line.amount:
510
raise osv.except_osv(_('Warning'), message)
511
# verify analytic distribution only on 'open' commitments
512
if line.commit_id and line.commit_id.state and line.commit_id.state == 'open':
513
# Search distribution
514
distrib_id = line.analytic_distribution_id and line.analytic_distribution_id.id or False
516
distrib_id = line.commit_id.analytic_distribution_id and line.commit_id.analytic_distribution_id.id or False
518
if 'amount' in vals and vals.get('amount', 0.0) == '0.0':
519
# delete analytic lines that are null
521
distrib = self.pool.get('analytic.distribution').browse(cr, uid, [distrib_id], context=context)[0]
522
if distrib and distrib.analytic_lines:
523
self.pool.get('account.analytic.line').unlink(cr, uid, [x.id for x in distrib.analytic_lines], context=context)
524
elif 'amount' in vals:
525
# Verify expense account
527
if 'account_id' in vals and vals.get('account_id', False) and line.account_id.id != vals.get('account_id'):
528
account_id = vals.get('account_id')
529
# Update analytic lines
531
self.update_analytic_lines(cr, uid, [line.id], vals.get('amount'), account_id, context=context)
532
return super(account_commitment_line, self).write(cr, uid, ids, vals, context={})
534
def button_analytic_distribution(self, cr, uid, ids, context=None):
536
Launch analytic distribution wizard on a commitment voucher line
541
if isinstance(ids, (int, long)):
544
raise osv.except_osv(_('Error'), _('No invoice line given. Please save your commitment voucher line before.'))
545
# Prepare some values
546
commitment_voucher_line = self.browse(cr, uid, ids[0], context=context)
548
amount = commitment_voucher_line.amount or 0.0
549
# Search elements for currency
550
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
551
currency = commitment_voucher_line.commit_id.currency_id and commitment_voucher_line.commit_id.currency_id.id or company_currency
552
# Get analytic distribution id from this line
553
distrib_id = commitment_voucher_line and commitment_voucher_line.analytic_distribution_id and commitment_voucher_line.analytic_distribution_id.id or False
554
# Prepare values for wizard
556
'total_amount': amount,
557
'commitment_line_id': commitment_voucher_line.id,
558
'currency_id': currency or False,
560
'account_id': commitment_voucher_line.account_id and commitment_voucher_line.account_id.id or False,
563
vals.update({'distribution_id': distrib_id,})
565
wiz_obj = self.pool.get('analytic.distribution.wizard')
566
wiz_id = wiz_obj.create(cr, uid, vals, context=context)
567
# Update some context values
574
'name': 'Analytic distribution',
575
'type': 'ir.actions.act_window',
576
'res_model': 'analytic.distribution.wizard',
584
account_commitment_line()
585
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: