1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 MSF, TeMPO Consulting.
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.
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.
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/>.
20
##############################################################################
22
from osv import fields, osv
23
from tools.translate import _
25
from dateutil.relativedelta import relativedelta
27
class msf_accrual_line(osv.osv):
28
_name = 'msf.accrual.line'
31
def onchange_period(self, cr, uid, ids, period_id, context=None):
32
if period_id is False:
33
return {'value': {'date': False}}
35
period = self.pool.get('account.period').browse(cr, uid, period_id, context=context)
36
return {'value': {'date': period.date_stop, 'document_date': period.date_stop}}
38
def _get_functional_amount(self, cr, uid, ids, field_name, arg, context=None):
40
for accrual_line in self.browse(cr, uid, ids, context=context):
41
date_context = {'date': accrual_line.date}
42
res[accrual_line.id] = self.pool.get('res.currency').compute(cr,
44
accrual_line.currency_id.id,
45
accrual_line.functional_currency_id.id,
46
accrual_line.accrual_amount or 0.0,
51
def _get_entry_sequence(self, cr, uid, ids, field_name, arg, context=None):
55
for rec in self.browse(cr, uid, ids, context=context):
57
if rec.state != 'draft' and rec.analytic_distribution_id \
58
and rec.analytic_distribution_id.move_line_ids:
59
# get the NOT REV entry
60
# (same period as REV posting date is M+1)
62
for mv in rec.analytic_distribution_id.move_line_ids:
63
if mv.period_id.id == rec.period_id.id:
67
es = move_line_br.move_id \
68
and move_line_br.move_id.name or ''
73
'date': fields.date("Date"),
74
'document_date': fields.date("Document Date", required=True),
75
'period_id': fields.many2one('account.period', 'Period', required=True, domain=[('state', '=', 'draft'), ('is_system', '=', False)]),
76
'description': fields.char('Description', size=64, required=True),
77
'reference': fields.char('Reference', size=64),
78
'expense_account_id': fields.many2one('account.account', 'Expense Account', required=True, domain=[('type', '!=', 'view'), ('user_type_code', '=', 'expense')]),
79
'accrual_account_id': fields.many2one('account.account', 'Accrual Account', required=True, domain=[('type', '!=', 'view'), ('user_type_code', 'in', ['receivables', 'payables', 'debt'])]),
80
'accrual_amount': fields.float('Accrual Amount', required=True),
81
'currency_id': fields.many2one('res.currency', 'Currency', required=True),
82
'journal_id': fields.many2one('account.journal', 'Journal', required=True),
83
'third_party_type': fields.selection([
85
('res.partner', 'Partner'),
86
('hr.employee', 'Employee'),
87
], 'Third Party', required=False),
88
'partner_id': fields.many2one('res.partner', 'Third Party Partner', ondelete="restrict"),
89
'employee_id': fields.many2one('hr.employee', 'Third Party Employee', ondelete="restrict"),
90
'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution'),
91
'functional_amount': fields.function(_get_functional_amount, method=True, store=False, string="Functional Amount", type="float", readonly="True"),
92
'functional_currency_id': fields.many2one('res.currency', 'Functional Currency', required=True, readonly=True),
93
'state': fields.selection([('draft', 'Draft'),
95
('cancel', 'Cancelled')], 'Status', required=True),
96
# Field to store the third party's name for list view
97
'third_party_name': fields.char('Third Party', size=64),
98
'entry_sequence': fields.function(_get_entry_sequence, method=True,
99
store=False, string="Number", type="char", readonly="True"),
103
'third_party_type': 'res.partner',
104
'journal_id': lambda self,cr,uid,c: self.pool.get('account.journal').search(cr, uid, [('type', '=', 'accrual'),
105
('is_current_instance', '=', True)])[0],
106
'functional_currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id,
110
def _create_write_set_vals(self, cr, uid, vals, context=None):
111
if 'third_party_type' in vals:
112
if vals['third_party_type'] == 'hr.employee' and 'employee_id' in vals:
113
employee = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context)
114
vals['third_party_name'] = employee.name
115
elif vals['third_party_type'] == 'res.partner' and 'partner_id' in vals:
116
partner = self.pool.get('res.partner').browse(cr, uid, vals['partner_id'], context=context)
117
vals['third_party_name'] = partner.name
118
elif not vals['third_party_type']:
119
vals['partner_id'] = False
120
if 'period_id' in vals:
121
period = self.pool.get('account.period').browse(cr, uid, vals['period_id'], context=context)
122
vals['date'] = period.date_stop
123
if 'currency_id' in vals and 'date' in vals:
124
cr.execute("SELECT currency_id, name, rate FROM res_currency_rate WHERE currency_id = %s AND name <= %s ORDER BY name desc LIMIT 1" ,(vals['currency_id'], vals['date']))
126
currency_name = self.pool.get('res.currency').browse(cr, uid, vals['currency_id'], context=context).name
127
formatted_date = datetime.datetime.strptime(vals['date'], '%Y-%m-%d').strftime('%d/%b/%Y')
128
raise osv.except_osv(_('Warning !'), _("The currency '%s' does not have any rate set for date '%s'!") % (currency_name, formatted_date))
130
def create(self, cr, uid, vals, context=None):
134
if 'document_date' in vals and vals.get('period_id', False):
135
# US-192 check doc date regarding post date
136
# => read (as date readonly in form) to get posting date:
138
posting_date = self.pool.get('account.period').read(cr, uid,
139
vals['period_id'], ['date_stop', ],
140
context=context)['date_stop']
141
self.pool.get('finance.tools').check_document_date(cr, uid,
142
vals['document_date'], posting_date, context=context)
144
self._create_write_set_vals(cr, uid, vals, context=context)
145
return super(msf_accrual_line, self).create(cr, uid, vals, context=context)
147
def write(self, cr, uid, ids, vals, context=None):
150
if isinstance(ids, (int, long, )):
153
if 'document_date' in vals:
154
# US-192 check doc date reagarding post date
155
# => read date field (as readonly in form)
156
for r in self.read(cr, uid, ids, ['date', ], context=context):
157
self.pool.get('finance.tools').check_document_date(cr, uid,
158
vals['document_date'], r['date'], context=context)
160
self._create_write_set_vals(cr, uid, vals, context=context)
161
return super(msf_accrual_line, self).write(cr, uid, ids, vals, context=context)
163
def button_cancel(self, cr, uid, ids, context=None):
166
if isinstance(ids, (int, long)):
168
period_obj = self.pool.get('account.period')
169
move_obj = self.pool.get('account.move')
170
move_line_obj = self.pool.get('account.move.line')
171
for accrual_line in self.browse(cr, uid, ids, context=context):
172
# check for periods, distribution, etc.
173
if accrual_line.state != 'posted':
174
raise osv.except_osv(_('Warning !'), _("The line '%s' is already posted!") % accrual_line.description)
177
if accrual_line.period_id.state not in ('draft', 'field-closed'):
178
raise osv.except_osv(_('Warning !'), _("The period '%s' is not open!" % accrual_line.period_id.name))
180
move_date = accrual_line.period_id.date_stop
181
reversal_move_date = (datetime.datetime.strptime(move_date, '%Y-%m-%d') + relativedelta(days=1)).strftime('%Y-%m-%d')
182
# check if periods are open
183
reversal_period_ids = period_obj.find(cr, uid, reversal_move_date, context=context)
184
reversal_period_id = reversal_period_ids[0]
187
'ref': accrual_line.reference,
188
'period_id': accrual_line.period_id.id,
189
'journal_id': accrual_line.journal_id.id,
192
reversal_move_vals = {
193
'ref': accrual_line.reference,
194
'period_id': reversal_period_id,
195
'journal_id': accrual_line.journal_id.id,
196
'date': reversal_move_date
198
move_id = move_obj.create(cr, uid, move_vals, context=context)
199
reversal_move_id = move_obj.create(cr, uid, reversal_move_vals, context=context)
201
cancel_description = "CANCEL - " + accrual_line.description
202
reverse_cancel_description = "CANCEL - REV - " + accrual_line.description
205
booking_field = accrual_line.accrual_amount > 0 and 'debit_currency' or 'credit_currency' # reverse of initial entry
206
accrual_move_line_vals = {
210
'document_date': accrual_line.document_date,
211
'journal_id': accrual_line.journal_id.id,
212
'period_id': accrual_line.period_id.id,
213
'reference': accrual_line.reference,
214
'name': cancel_description,
215
'account_id': accrual_line.accrual_account_id.id,
216
'partner_id': ((accrual_line.partner_id) and accrual_line.partner_id.id) or False,
217
'employee_id': ((accrual_line.employee_id) and accrual_line.employee_id.id) or False,
218
booking_field: abs(accrual_line.accrual_amount),
219
'currency_id': accrual_line.currency_id.id,
221
booking_field = accrual_line.accrual_amount > 0 and 'credit_currency' or 'debit_currency'
222
expense_move_line_vals = {
226
'document_date': accrual_line.document_date,
227
'journal_id': accrual_line.journal_id.id,
228
'period_id': accrual_line.period_id.id,
229
'reference': accrual_line.reference,
230
'name': cancel_description,
231
'account_id': accrual_line.expense_account_id.id,
232
'partner_id': ((accrual_line.partner_id) and accrual_line.partner_id.id) or False,
233
'employee_id': ((accrual_line.employee_id) and accrual_line.employee_id.id) or False,
234
booking_field: abs(accrual_line.accrual_amount),
235
'currency_id': accrual_line.currency_id.id,
236
'analytic_distribution_id': accrual_line.analytic_distribution_id.id,
239
# and their reversal (source_date to keep the old change rate)
240
booking_field = accrual_line.accrual_amount > 0 and 'credit_currency' or 'debit_currency'
241
reversal_accrual_move_line_vals = {
243
'move_id': reversal_move_id,
244
'date': reversal_move_date,
245
'document_date': reversal_move_date,
246
'source_date': move_date,
247
'journal_id': accrual_line.journal_id.id,
248
'period_id': reversal_period_id,
249
'reference': accrual_line.reference,
250
'name': reverse_cancel_description,
251
'account_id': accrual_line.accrual_account_id.id,
252
'partner_id': ((accrual_line.partner_id) and accrual_line.partner_id.id) or False,
253
'employee_id': ((accrual_line.employee_id) and accrual_line.employee_id.id) or False,
254
booking_field: abs(accrual_line.accrual_amount),
255
'currency_id': accrual_line.currency_id.id,
257
booking_field = accrual_line.accrual_amount > 0 and 'debit_currency' or 'credit_currency'
258
reversal_expense_move_line_vals = {
260
'move_id': reversal_move_id,
261
'date': reversal_move_date,
262
'document_date': reversal_move_date,
263
'source_date': move_date,
264
'journal_id': accrual_line.journal_id.id,
265
'period_id': reversal_period_id,
266
'reference': accrual_line.reference,
267
'name': reverse_cancel_description,
268
'account_id': accrual_line.expense_account_id.id,
269
'partner_id': ((accrual_line.partner_id) and accrual_line.partner_id.id) or False,
270
'employee_id': ((accrual_line.employee_id) and accrual_line.employee_id.id) or False,
271
booking_field: abs(accrual_line.accrual_amount),
272
'currency_id': accrual_line.currency_id.id,
273
'analytic_distribution_id': accrual_line.analytic_distribution_id.id,
276
accrual_move_line_id = move_line_obj.create(cr, uid, accrual_move_line_vals, context=context)
277
expense_move_line_id = move_line_obj.create(cr, uid, expense_move_line_vals, context=context)
278
reversal_accrual_move_line_id = move_line_obj.create(cr, uid, reversal_accrual_move_line_vals, context=context)
279
reversal_expense_move_line_id = move_line_obj.create(cr, uid, reversal_expense_move_line_vals, context=context)
282
move_obj.post(cr, uid, [move_id, reversal_move_id], context=context)
283
# Reconcile the accrual move line with its reversal
284
move_line_obj.reconcile_partial(cr, uid, [accrual_move_line_id, reversal_accrual_move_line_id], context=context)
285
# validate the accrual line
286
self.write(cr, uid, [accrual_line.id], {'state': 'cancel'}, context=context)
289
def button_duplicate(self, cr, uid, ids, context=None):
291
Copy given lines and delete all links
296
if isinstance(ids, (int, long)):
299
for line in self.browse(cr, uid, ids, context=context):
301
'description': '(copy) ' + line.description,
303
if line.analytic_distribution_id:
304
# the distribution must be copied, not just the id
305
new_distrib_id = self.pool.get('analytic.distribution').copy(cr, uid, line.analytic_distribution_id.id, {}, context=context)
307
default_vals.update({'analytic_distribution_id': new_distrib_id})
308
self.copy(cr, uid, line.id, default_vals, context=context)
311
def button_analytic_distribution(self, cr, uid, ids, context=None):
313
Launch analytic distribution wizard on an invoice line
318
if isinstance(ids, (int, long)):
320
# Prepare some values
321
accrual_line = self.browse(cr, uid, ids[0], context=context)
322
# Search elements for currency
323
currency = accrual_line.currency_id and accrual_line.currency_id.id
324
# Get analytic distribution id from this line
325
distrib_id = accrual_line and accrual_line.analytic_distribution_id and accrual_line.analytic_distribution_id.id or False
326
# Prepare values for wizard
328
'total_amount': accrual_line.accrual_amount or 0.0,
329
'accrual_line_id': accrual_line.id,
330
'currency_id': currency or False,
332
'account_id': accrual_line.expense_account_id and accrual_line.expense_account_id.id or False,
333
'posting_date': accrual_line.date,
334
'document_date': accrual_line.document_date,
337
vals.update({'distribution_id': distrib_id,})
339
wiz_obj = self.pool.get('analytic.distribution.wizard')
340
wiz_id = wiz_obj.create(cr, uid, vals, context=context)
341
# Update some context values
345
'from_accrual_line': True
349
'name': _('Analytic distribution'),
350
'type': 'ir.actions.act_window',
351
'res_model': 'analytic.distribution.wizard',
359
def button_delete(self, cr, uid, ids, context=None):
360
return self.unlink(cr, uid, ids, context=context)
362
def unlink(self, cr, uid, ids, context=None):
365
for rec in self.browse(cr, uid, ids, context=context):
366
if rec.state != 'draft':
367
raise osv.except_osv(_('Warning'),
368
_('You can only delete draft accruals'))
369
return super(msf_accrual_line, self).unlink(cr, uid, ids,
373
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: