2
# -*- coding: utf-8 -*-
3
##############################################################################
5
# OpenERP, Open Source Management Solution
6
# Copyright (C) 2012 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 _
28
from lxml import etree
29
from time import strftime
31
class account_invoice_line(osv.osv):
32
_name = 'account.invoice.line'
33
_inherit = 'account.invoice.line'
36
'import_invoice_id': fields.many2one('account.invoice', string="From an import invoice", readonly=True),
37
'move_lines': fields.one2many('account.move.line', 'invoice_line_id', string="Journal Item", readonly=True),
41
'price_unit': lambda *a: 0.00,
44
def copy_data(self, cr, uid, id, default=None, context=None):
46
Copy an invoice line without its move lines
50
default.update({'move_lines': False,})
51
return super(account_invoice_line, self).copy_data(cr, uid, id, default, context)
53
def move_line_get_item(self, cr, uid, line, context=None):
55
Add a link between move line and its invoice line
60
# update default dict with invoice line ID
61
res = super(account_invoice_line, self).move_line_get_item(cr, uid, line, context=context)
62
res.update({'invoice_line_id': line.id})
65
account_invoice_line()
67
class account_invoice(osv.osv):
68
_name = 'account.invoice'
69
_inherit = 'account.invoice'
71
def _get_fake(self, cr, uid, ids, field_name=None, arg=None, context=None):
73
Fake method for 'ready_for_import_in_debit_note' field
80
def _search_ready_for_import_in_debit_note(self, cr, uid, obj, name, args, context=None):
83
account_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.import_invoice_default_account and \
84
self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.import_invoice_default_account.id or False
86
raise osv.except_osv(_('Error'), _('No default account for import invoice on Debit Note!'))
88
('account_id','=',account_id),
89
('reconciled','=',False),
90
('state', '=', 'open'),
91
('type', '=', 'out_invoice'),
92
('journal_id.type', 'in', ['sale']),
93
('partner_id.partner_type', '=', 'section'),
95
return dom1+[('is_debit_note', '=', False)]
97
def _get_fake_m2o_id(self, cr, uid, ids, field_name=None, arg=None, context=None):
99
Get many2one field content
102
name = field_name.replace("fake_", '')
103
for i in self.browse(cr, uid, ids):
104
res[i.id] = getattr(i, name, False) and getattr(getattr(i, name, False), 'id', False) or False
107
def _get_have_donation_certificate(self, cr, uid, ids, field_name=None, arg=None, context=None):
109
If this invoice have a stock picking in which there is a Certificate of Donation, return True. Otherwise return False.
112
for i in self.browse(cr, uid, ids):
115
a_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', 'stock.picking'), ('res_id', '=', i.picking_id.id), ('description', '=', 'Certificate of Donation')])
121
'is_debit_note': fields.boolean(string="Is a Debit Note?"),
122
'is_inkind_donation': fields.boolean(string="Is an In-kind Donation?"),
123
'is_intermission': fields.boolean(string="Is an Intermission Voucher?"),
124
'ready_for_import_in_debit_note': fields.function(_get_fake, fnct_search=_search_ready_for_import_in_debit_note, type="boolean",
125
method=True, string="Can be imported as invoice in a debit note?",),
126
'imported_invoices': fields.one2many('account.invoice.line', 'import_invoice_id', string="Imported invoices", readonly=True),
127
'partner_move_line': fields.one2many('account.move.line', 'invoice_partner_link', string="Partner move line", readonly=True),
128
'fake_account_id': fields.function(_get_fake_m2o_id, method=True, type='many2one', relation="account.account", string="Account", readonly="True"),
129
'fake_journal_id': fields.function(_get_fake_m2o_id, method=True, type='many2one', relation="account.journal", string="Journal", readonly="True"),
130
'fake_currency_id': fields.function(_get_fake_m2o_id, method=True, type='many2one', relation="res.currency", string="Currency", readonly="True"),
131
'picking_id': fields.many2one('stock.picking', string="Picking"),
132
'have_donation_certificate': fields.function(_get_have_donation_certificate, method=True, type='boolean', string="Have a Certificate of donation?"),
136
'is_debit_note': lambda obj, cr, uid, c: c.get('is_debit_note', False),
137
'is_inkind_donation': lambda obj, cr, uid, c: c.get('is_inkind_donation', False),
138
'is_intermission': lambda obj, cr, uid, c: c.get('is_intermission', False),
141
def log(self, cr, uid, id, message, secondary=False, context=None):
143
Change first "Invoice" word from message into "Debit Note" if this invoice is a debit note.
144
Change it to "In-kind donation" if this invoice is an In-kind donation.
148
# Prepare some values
149
# Search donation view and return it
151
# try / except for runbot
152
debit_res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_msf', 'view_debit_note_form')
153
inkind_res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_msf', 'view_inkind_donation_form')
154
intermission_res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_msf', 'view_intermission_form')
155
supplier_invoice_res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form')
156
customer_invoice_res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
158
return super(account_invoice, self).log(cr, uid, id, message, secondary, context)
159
debit_view_id = debit_res and debit_res[1] or False
160
debit_note_ctx = {'view_id': debit_view_id, 'type':'out_invoice', 'journal_type': 'sale', 'is_debit_note': True}
161
# Search donation view and return it
162
inkind_view_id = inkind_res and inkind_res[1] or False
163
inkind_ctx = {'view_id': inkind_view_id, 'type':'in_invoice', 'journal_type': 'inkind', 'is_inkind_donation': True}
164
# Search intermission view
165
intermission_view_id = intermission_res and intermission_res[1] or False
166
intermission_ctx = {'view_id': intermission_view_id, 'journal_type': 'intermission', 'is_intermission': True}
167
customer_view_id = customer_invoice_res[1] or False
168
customer_ctx = {'view_id': customer_view_id, 'type': 'out_invoice', 'journal_type': 'sale'}
169
message_changed = False
170
pattern = re.compile('^(Invoice)')
171
for el in [('is_debit_note', 'Debit Note', debit_note_ctx), ('is_inkind_donation', 'In-kind Donation', inkind_ctx), ('is_intermission', 'Intermission Voucher', intermission_ctx)]:
172
if self.read(cr, uid, id, [el[0]]).get(el[0], False) is True:
173
m = re.match(pattern, message)
175
message = re.sub(pattern, el[1], message, 1)
176
message_changed = True
177
context.update(el[2])
178
# UF-1112: Give all customer invoices a name as "Stock Transfer Voucher".
179
if not message_changed and self.read(cr, uid, id, ['type']).get('type', False) == 'out_invoice':
180
message = re.sub(pattern, 'Stock Transfer Voucher', message, 1)
182
context.update(customer_ctx)
183
# UF-1307: for supplier invoice log (from the incoming shipment), the context was not
184
# filled with all the information; this leaded to having a "Sale" journal in the supplier
185
# invoice if it was saved after coming from this link. Here's the fix.
186
if (not context.get('journal_type', False) and context.get('type', False) == 'in_invoice'):
187
supplier_view_id = supplier_invoice_res and supplier_invoice_res[1] or False
188
context.update({'journal_type': 'purchase',
189
'view_id': supplier_view_id})
190
return super(account_invoice, self).log(cr, uid, id, message, secondary, context)
192
def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
193
date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False, is_inkind_donation=False, is_intermission=False):
195
Update fake_account_id field regarding account_id result.
196
Get default donation account for Donation invoices.
197
Get default intermission account for Intermission Voucher IN/OUT invoices.
198
Get default currency from partner if this one is linked to a pricelist.
200
res = super(account_invoice, self).onchange_partner_id(cr, uid, ids, type, partner_id, date_invoice, payment_term, partner_bank_id, company_id)
201
if is_inkind_donation and partner_id:
202
partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
203
account_id = partner and partner.donation_payable_account and partner.donation_payable_account.id or False
204
res['value']['account_id'] = account_id
205
if is_intermission and partner_id:
206
intermission_default_account = self.pool.get('res.users').browse(cr, uid, uid).company_id.intermission_default_counterpart
207
account_id = intermission_default_account and intermission_default_account.id or False
209
raise osv.except_osv(_('Error'), _('Please configure a default intermission account in Company configuration.'))
210
res['value']['account_id'] = account_id
211
if res.get('value', False) and 'account_id' in res['value']:
212
res['value'].update({'fake_account_id': res['value'].get('account_id')})
213
if partner_id and type:
214
p = self.pool.get('res.partner').browse(cr, uid, partner_id)
217
if type in ['in_invoice', 'out_refund'] and p.property_product_pricelist_purchase:
218
c_id = p.property_product_pricelist_purchase.currency_id.id
219
elif type in ['out_invoice', 'in_refund'] and p.property_product_pricelist:
220
c_id = p.property_product_pricelist.currency_id.id
222
if not res.get('value', False):
223
res['value'] = {'currency_id': c_id}
225
res['value'].update({'currency_id': c_id})
229
def _refund_cleanup_lines(self, cr, uid, lines):
231
Remove useless fields
234
del line['move_lines']
235
del line['import_invoice_id']
236
res = super(account_invoice, self)._refund_cleanup_lines(cr, uid, lines)
239
def button_debit_note_import_invoice(self, cr, uid, ids, context=None):
241
Launch wizard that permits to import invoice on a debit note
246
if isinstance(ids, (int, long)):
248
# Browse all given invoices
249
for inv in self.browse(cr, uid, ids):
250
if inv.type != 'out_invoice' or inv.is_debit_note == False:
251
raise osv.except_osv(_('Error'), _('You can only do import invoice on a Debit Note!'))
252
w_id = self.pool.get('debit.note.import.invoice').create(cr, uid, {'invoice_id': inv.id, 'currency_id': inv.currency_id.id,
253
'partner_id': inv.partner_id.id})
259
'type': 'ir.actions.act_window',
260
'res_model': 'debit.note.import.invoice',
261
'name': 'Import invoice',
269
def button_donation_certificate(self, cr, uid, ids, context=None):
272
for inv in self.browse(cr, uid, ids):
273
pick_id = inv.picking_id and inv.picking_id.id or ''
274
domain = "[('res_model', '=', 'stock.picking'), ('res_id', '=', " + str(pick_id) + "), ('description', '=', 'Certificate of Donation')]"
275
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_msf', 'view_attachment_tree_2')
276
view_id = view_id and view_id[1] or False
277
search_view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_msf', 'view_attachment_search_2')
278
search_view_id = search_view_id and search_view_id[1] or False
280
'name': "Certificate of Donation",
281
'type': 'ir.actions.act_window',
282
'res_model': 'ir.attachment',
284
'view_mode': 'tree,form',
285
'view_id': [view_id],
286
'search_view_id': search_view_id,
293
def copy(self, cr, uid, id, default=None, context=None):
295
Copy global distribution and give it to new invoice
301
default.update({'partner_move_line': False, 'imported_invoices': False})
302
return super(account_invoice, self).copy(cr, uid, id, default, context)
304
def finalize_invoice_move_lines(self, cr, uid, inv, line):
306
Hook that changes move line data before write them.
307
Add a link between partner move line and invoice.
309
def is_partner_line(dico):
310
if isinstance(dico, dict):
312
# In case where no amount_currency filled in, then take debit - credit for amount comparison
313
amount = dico.get('amount_currency', False) or (dico.get('debit', 0.0) - dico.get('credit', 0.0))
314
if amount == inv.amount_total and dico.get('partner_id', False) == inv.partner_id.id:
319
if el[2] and is_partner_line(el[2]):
320
el[2].update({'invoice_partner_link': inv.id})
321
new_line.append((el[0], el[1], el[2]))
324
res = super(account_invoice, self).finalize_invoice_move_lines(cr, uid, inv, new_line)
327
def line_get_convert(self, cr, uid, x, part, date, context=None):
329
Add these field into invoice line:
334
res = super(account_invoice, self).line_get_convert(cr, uid, x, part, date, context)
335
res.update({'invoice_line_id': x.get('invoice_line_id', False)})
338
def action_reconcile_imported_invoice(self, cr, uid, ids, context=None):
340
Reconcile each imported invoice with its attached invoice line
343
if isinstance(ids, (int, long)):
345
# browse all given invoices
346
for inv in self.browse(cr, uid, ids):
347
for invl in inv.invoice_line:
348
if not invl.import_invoice_id:
350
imported_invoice = invl.import_invoice_id
351
# reconcile partner line from import invoice with this invoice line attached move line
352
import_invoice_partner_move_lines = self.pool.get('account.move.line').search(cr, uid, [('invoice_partner_link', '=', imported_invoice.id)])
353
invl_move_lines = [x.id or None for x in invl.move_lines]
354
rec = self.pool.get('account.move.line').reconcile_partial(cr, uid, [import_invoice_partner_move_lines[0], invl_move_lines[0]], 'auto', context=context)
359
def action_open_invoice(self, cr, uid, ids, context=None, *args):
361
Launch reconciliation of imported invoice lines from a debit note invoice
366
if isinstance(ids, (int, long)):
368
res = super(account_invoice, self).action_open_invoice(cr, uid, ids, context, args)
369
if res and not self.action_reconcile_imported_invoice(cr, uid, ids, context):
373
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
375
Rename Supplier/Customer to "Donor" if view_type == tree
379
res = super(account_invoice, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
380
if view_type == 'tree' and (context.get('journal_type', False) == 'inkind' or context.get('journal_type', False) == 'intermission'):
381
doc = etree.XML(res['arch'])
382
nodes = doc.xpath("//field[@name='partner_id']")
384
if context.get('journal_type') == 'intermission':
387
node.set('string', name)
388
res['arch'] = etree.tostring(doc)
391
def action_cancel(self, cr, uid, ids, *args):
393
Reverse move if this object is a In-kind Donation. Otherwise do normal job: cancellation.
396
for i in self.browse(cr, uid, ids):
397
if i.is_inkind_donation:
398
move_id = i.move_id.id
399
tmp_res = self.pool.get('account.move').reverse(cr, uid, [move_id], strftime('%Y-%m-%d'))
400
# If success change invoice to cancel and detach move_id
402
# Change invoice state
403
self.write(cr, uid, [i.id], {'state': 'cancel', 'move_id':False})
405
to_cancel.append(i.id)
406
return super(account_invoice, self).action_cancel(cr, uid, to_cancel, args)
409
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: