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 osv, fields
23
from tools.translate import _
25
class account_invoice(osv.osv):
26
_name = 'account.invoice'
27
_inherit = 'account.invoice'
29
def _check_active_product(self, cr, uid, ids, context=None):
31
Check if the Purchase order contains a line with an inactive products
33
inactive_lines = self.pool.get('account.invoice.line').search(cr, uid, [
34
('product_id.active', '=', False),
35
('invoice_id', 'in', ids),
36
('invoice_id.state', 'not in', ['draft', 'cancel', 'done'])
40
plural = len(inactive_lines) == 1 and _('A product has') or _('Some products have')
41
l_plural = len(inactive_lines) == 1 and _('line') or _('lines')
42
p_plural = len(inactive_lines) == 1 and _('this inactive product') or _('those inactive products')
43
raise osv.except_osv(_('Error'), _('%s been inactivated. If you want to validate this document you have to remove/correct the %s containing %s (see red %s of the document)') % (plural, l_plural, p_plural, l_plural))
48
(_check_active_product, "You cannot validate this invoice because it contains a line with an inactive product", ['invoice_line', 'state'])
52
'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution', select="1"), # select: optimisation purpose
55
def _check_analytic_distribution_state(self, cr, uid, ids, context=None):
57
Check if analytic distribution is valid
61
if isinstance(ids, (int, long)):
63
for inv in self.browse(cr, uid, ids, context=context):
64
for invl in inv.invoice_line:
65
if invl.analytic_distribution_state != 'valid':
66
raise osv.except_osv(_('Error'), _('Analytic distribution is not valid for "%s"') % invl.name)
69
def _hook_fields_for_refund(self, cr, uid, *args):
71
Add these fields to result:
72
- analytic_distribution_id
74
res = super(account_invoice, self)._hook_fields_for_refund(cr, uid, args)
75
res.append('analytic_distribution_id')
76
res.append('document_date')
79
def _hook_fields_m2o_for_refund(self, cr, uid, *args):
81
Add these fields to result:
82
- analytic_distribution_id
84
res = super(account_invoice, self)._hook_fields_m2o_for_refund(cr, uid, args)
85
res.append('analytic_distribution_id')
88
def _hook_refund_data(self, cr, uid, data, *args):
90
Copy analytic distribution for refund invoice
94
if 'analytic_distribution_id' in data:
95
if data.get('analytic_distribution_id', False):
96
data['analytic_distribution_id'] = self.pool.get('analytic.distribution').copy(cr, uid, data.get('analytic_distribution_id'), {}) or False
98
data['analytic_distribution_id'] = False
101
def _refund_cleanup_lines(self, cr, uid, lines):
103
Add right analytic distribution values on each lines
105
res = super(account_invoice, self)._refund_cleanup_lines(cr, uid, lines)
108
# Give analytic distribution on line
109
if 'analytic_distribution_id' in el[2]:
110
if el[2].get('analytic_distribution_id', False) and el[2].get('analytic_distribution_id')[0]:
111
distrib_id = el[2].get('analytic_distribution_id')[0]
112
el[2]['analytic_distribution_id'] = self.pool.get('analytic.distribution').copy(cr, uid, distrib_id, {}) or False
115
el[2]['analytic_distribution_id'] = False
116
# Give false analytic lines for 'line' in order not to give an error
117
if 'analytic_line_ids' in el[2]:
118
el[2]['analytic_line_ids'] = False
119
# Give false for (because not needed):
121
# - sale_order_line_id
122
for field in ['order_line_id', 'sale_order_line_id']:
124
el[2][field] = el[2].get(field, False) and el[2][field][0] or False
127
def copy(self, cr, uid, inv_id, default=None, context=None):
129
Copy global distribution and give it to new invoice
135
inv = self.browse(cr, uid, [inv_id], context=context)[0]
136
if inv.analytic_distribution_id:
137
new_distrib_id = self.pool.get('analytic.distribution').copy(cr, uid, inv.analytic_distribution_id.id, {}, context=context)
139
default.update({'analytic_distribution_id': new_distrib_id})
140
return super(account_invoice, self).copy(cr, uid, inv_id, default, context)
142
def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None, document_date=None):
144
Reverse lines for given invoice
146
if isinstance(ids, (int, long)):
148
for inv in self.browse(cr, uid, ids):
149
# Check for dates (refund must be done after invoice)
150
if date and date < inv.date_invoice:
151
raise osv.except_osv(_('Error'), _("Posting date for the refund is before the invoice's posting date!"))
152
if document_date and document_date < inv.document_date:
153
raise osv.except_osv(_('Error'), _("Document date for the refund is before the invoice's document date!"))
154
new_ids = super(account_invoice, self).refund(cr, uid, ids, date, period_id, description, journal_id)
157
self.write(cr, uid, new_ids, {'document_date': document_date})
160
def line_get_convert(self, cr, uid, x, part, date, context=None):
161
res = super(account_invoice, self).line_get_convert(cr, uid, x, part, date, context=context)
162
res['analytic_distribution_id'] = x.get('analytic_distribution_id', False)
165
def button_analytic_distribution(self, cr, uid, ids, context=None):
167
Launch analytic distribution wizard on an invoice
172
if isinstance(ids, (int, long)):
174
# Prepare some values
175
invoice = self.browse(cr, uid, ids[0], context=context)
177
# Search elements for currency
178
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
179
currency = invoice.currency_id and invoice.currency_id.id or company_currency
180
for line in invoice.invoice_line:
181
amount += line.price_subtotal
182
# Get analytic_distribution_id
183
distrib_id = invoice.analytic_distribution_id and invoice.analytic_distribution_id.id
184
# Prepare values for wizard
186
'total_amount': amount,
187
'invoice_id': invoice.id,
188
'currency_id': currency or False,
190
'posting_date': invoice.date_invoice,
191
'document_date': invoice.document_date,
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_reset_distribution(self, cr, uid, ids, context=None):
217
Reset analytic distribution on all invoice lines.
218
To do this, just delete the analytic_distribution id link on each invoice line.
222
if isinstance(ids, (int, long)):
224
# Prepare some values
225
invl_obj = self.pool.get(self._name + '.line') # PAY ATTENTION to wizard.account.invoice.line
226
# Search invoice lines
227
to_reset = invl_obj.search(cr, uid, [('invoice_id', 'in', ids)])
228
invl_obj.write(cr, uid, to_reset, {'analytic_distribution_id': False})
233
class account_invoice_line(osv.osv):
234
_name = 'account.invoice.line'
235
_inherit = 'account.invoice.line'
237
def _get_distribution_state(self, cr, uid, ids, name, args, context=None):
239
Get state of distribution:
240
- if compatible with the invoice line, then "valid"
241
- if no distribution, take a tour of invoice distribution, if compatible, then "valid"
242
- if no distribution on invoice line and invoice, then "none"
243
- all other case are "invalid"
248
if isinstance(ids, (int, long)):
250
# Prepare some values
252
# Browse all given lines
253
for line in self.browse(cr, uid, ids, context=context):
254
if line.from_yml_test:
255
res[line.id] = 'valid'
257
# UF-2115: test for elements
258
line_distribution_id = False
259
invoice_distribution_id = False
260
line_account_id = False
261
if line.analytic_distribution_id:
262
line_distribution_id = line.analytic_distribution_id.id
263
if line.invoice_id and line.invoice_id.analytic_distribution_id:
264
invoice_distribution_id = line.invoice_id.analytic_distribution_id.id
266
line_account_id = line.account_id.id
267
res[line.id] = self.pool.get('analytic.distribution')._get_distribution_state(cr, uid, line_distribution_id, invoice_distribution_id, line_account_id)
270
def _have_analytic_distribution_from_header(self, cr, uid, ids, name, arg, context=None):
272
If invoice have an analytic distribution, return False, else return True
277
if isinstance(ids, (int, long)):
280
for inv in self.browse(cr, uid, ids, context=context):
282
if inv.analytic_distribution_id:
286
def _get_is_allocatable(self, cr, uid, ids, name, arg, context=None):
288
If analytic-a-holic account, then this account is allocatable.
290
if isinstance(ids, (int, long)):
293
for invl in self.browse(cr, uid, ids):
295
if invl.account_id and not invl.account_id.is_analytic_addicted:
299
def _get_distribution_state_recap(self, cr, uid, ids, name, arg, context=None):
301
Get a recap from analytic distribution state and if it come from header or not.
303
if isinstance(ids, (int, long)):
306
for invl in self.browse(cr, uid, ids):
308
if not invl.is_allocatable:
311
if invl.have_analytic_distribution_from_header:
312
from_header = _(' (from header)')
313
res[invl.id] = '%s%s' % (self.pool.get('ir.model.fields').get_browse_selection(cr, uid, invl, 'analytic_distribution_state', context), from_header)
316
def _get_inactive_product(self, cr, uid, ids, field_name, args, context=None):
318
Fill the error message if the product of the line is inactive
321
for line in self.browse(cr, uid, ids, context=context):
322
res[line.id] = {'inactive_product': False,
323
'inactive_error': ''}
324
if line.invoice_id and line.invoice_id.state not in ('cancel', 'done') and line.product_id and not line.product_id.active:
326
'inactive_product': True,
327
'inactive_error': _('The product in line is inactive !')
331
def _get_analytic_lines(self, cr, uid, ids, field_name, arg, context=None):
337
# Prepare some values
339
for invl in self.browse(cr, uid, ids):
341
for ml in (invl.move_lines or []):
342
if ml.analytic_lines:
343
res[invl.id] = self.pool.get('account.analytic.line').get_corrections_history(cr, uid, [x.id for x in ml.analytic_lines])
347
'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution', select="1"), # select is for optimisation purposes
348
'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',
349
selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],
350
string="Distribution state", help="Informs from distribution state among 'none', 'valid', 'invalid."),
351
'have_analytic_distribution_from_header': fields.function(_have_analytic_distribution_from_header, method=True, type='boolean',
352
string='Header Distrib.?'),
353
'newline': fields.boolean('New line'),
354
'is_allocatable': fields.function(_get_is_allocatable, method=True, type='boolean', string="Is allocatable?", readonly=True, store=False),
355
'analytic_distribution_state_recap': fields.function(_get_distribution_state_recap, method=True, type='char', size=30,
356
string="Distribution",
357
help="Informs you about analaytic distribution state among 'none', 'valid', 'invalid', from header or not, or no analytic distribution"),
358
'inactive_product': fields.function(_get_inactive_product, method=True, type='boolean', string='Product is inactive', store=False, multi='inactive'),
359
'inactive_error': fields.function(_get_inactive_product, method=True, type='char', string='Comment', store=False, multi='inactive'),
360
'analytic_lines': fields.function(_get_analytic_lines, method=True, type='one2many', relation='account.analytic.line', store=False, string='Analytic lines', help='Give all analytic lines linked to this invoice line. With correction ones.'),
364
'newline': lambda *a: True,
365
'have_analytic_distribution_from_header': lambda *a: True,
366
'is_allocatable': lambda *a: True,
367
'analytic_distribution_state_recap': lambda *a: '',
368
'inactive_product': False,
369
'inactive_error': lambda *a: '',
372
def create(self, cr, uid, vals, context=None):
374
Set newline field to False.
376
vals.update({'newline': False,})
377
return super(account_invoice_line, self).create(cr, uid, vals, context)
379
def copy_data(self, cr, uid, l_id, default=None, context=None):
381
Copy global distribution and give it to new invoice line
388
# Copy analytic distribution
389
invl = self.browse(cr, uid, [l_id], context=context)[0]
390
if invl.analytic_distribution_id:
391
new_distrib_id = self.pool.get('analytic.distribution').copy(cr, uid, invl.analytic_distribution_id.id, {}, context=context)
393
default.update({'analytic_distribution_id': new_distrib_id})
394
return super(account_invoice_line, self).copy_data(cr, uid, l_id, default, context)
396
def move_line_get_item(self, cr, uid, line, context=None):
398
Give right analytic distribution when creating move lines
404
res = super(account_invoice_line, self).move_line_get_item(cr, uid, line, context=context)
405
# Update result by copying analytic distribution from invoice line
406
ana_obj = self.pool.get('analytic.distribution')
407
if line.analytic_distribution_id:
408
new_distrib_id = ana_obj.copy(cr, uid, line.analytic_distribution_id.id, {}, context=context)
410
res['analytic_distribution_id'] = new_distrib_id
411
# If no distribution on invoice line, take those from invoice and copy it!
412
elif line.invoice_id and line.invoice_id.analytic_distribution_id:
413
new_distrib_id = ana_obj.copy(cr, uid, line.invoice_id.analytic_distribution_id.id, {}, context=context)
415
res['analytic_distribution_id'] = new_distrib_id
418
def button_analytic_distribution(self, cr, uid, ids, context=None):
420
Launch analytic distribution wizard on an invoice line
425
if isinstance(ids, (int, long)):
428
raise osv.except_osv(_('Error'), _('No invoice line given. Please save your invoice line before.'))
429
# Prepare some values
430
invoice_line = self.browse(cr, uid, ids[0], context=context)
432
amount = invoice_line.price_subtotal or 0.0
433
# Search elements for currency
434
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
435
currency = invoice_line.invoice_id.currency_id and invoice_line.invoice_id.currency_id.id or company_currency
436
# Change amount sign if necessary
437
if invoice_line.invoice_id.type in ['out_invoice', 'in_refund']:
441
# Get analytic distribution id from this line
442
distrib_id = invoice_line and invoice_line.analytic_distribution_id and invoice_line.analytic_distribution_id.id or False
443
# Prepare values for wizard
445
'total_amount': amount,
446
'invoice_line_id': invoice_line.id,
447
'currency_id': currency or False,
449
'account_id': invoice_line.account_id and invoice_line.account_id.id or False,
450
'posting_date': invoice_line.invoice_id.date_invoice,
451
'document_date': invoice_line.invoice_id.document_date,
454
vals.update({'distribution_id': distrib_id,})
456
wiz_obj = self.pool.get('analytic.distribution.wizard')
457
wiz_id = wiz_obj.create(cr, uid, vals, context=context)
458
# Update some context values
465
'name': _('Analytic distribution'),
466
'type': 'ir.actions.act_window',
467
'res_model': 'analytic.distribution.wizard',
475
account_invoice_line()
476
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: