~unifield-team/unifield-wm/us-826

« back to all changes in this revision

Viewing changes to account_msf/invoice.py

UF-73: [MERGE] Merge with unifield-wm branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
2
 
# -*- coding: utf-8 -*-
3
 
##############################################################################
4
 
#
5
 
#    OpenERP, Open Source Management Solution
6
 
#    Copyright (C) 2012 TeMPO Consulting, MSF. All Rights Reserved
7
 
#    Developer: Olivier DOSSMANN
8
 
#
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.
13
 
#
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.
18
 
#
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/>.
21
 
#
22
 
##############################################################################
23
 
 
24
 
from osv import osv
25
 
from osv import fields
26
 
from tools.translate import _
27
 
import re
28
 
from lxml import etree
29
 
from time import strftime
30
 
 
31
 
class account_invoice_line(osv.osv):
32
 
    _name = 'account.invoice.line'
33
 
    _inherit = 'account.invoice.line'
34
 
 
35
 
    _columns = {
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),
38
 
    }
39
 
 
40
 
    _defaults = {
41
 
        'price_unit': lambda *a: 0.00,
42
 
    }
43
 
 
44
 
    def copy_data(self, cr, uid, id, default=None, context=None):
45
 
        """
46
 
        Copy an invoice line without its move lines
47
 
        """
48
 
        if default is None:
49
 
            default = {}
50
 
        default.update({'move_lines': False,})
51
 
        return super(account_invoice_line, self).copy_data(cr, uid, id, default, context)
52
 
 
53
 
    def move_line_get_item(self, cr, uid, line, context=None):
54
 
        """
55
 
        Add a link between move line and its invoice line
56
 
        """
57
 
        # some verification
58
 
        if not context:
59
 
            context = {}
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})
63
 
        return res
64
 
 
65
 
account_invoice_line()
66
 
 
67
 
class account_invoice(osv.osv):
68
 
    _name = 'account.invoice'
69
 
    _inherit = 'account.invoice'
70
 
 
71
 
    def _get_fake(self, cr, uid, ids, field_name=None, arg=None, context=None):
72
 
        """
73
 
        Fake method for 'ready_for_import_in_debit_note' field
74
 
        """
75
 
        res = {}
76
 
        for id in ids:
77
 
            res[id] = False
78
 
        return res
79
 
 
80
 
    def _search_ready_for_import_in_debit_note(self, cr, uid, obj, name, args, context=None):
81
 
        if not args:
82
 
            return []
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
85
 
        if not account_id:
86
 
            raise osv.except_osv(_('Error'), _('No default account for import invoice on Debit Note!'))
87
 
        dom1 = [
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'),
94
 
        ]
95
 
        return dom1+[('is_debit_note', '=', False)]
96
 
 
97
 
    def _get_fake_m2o_id(self, cr, uid, ids, field_name=None, arg=None, context=None):
98
 
        """
99
 
        Get many2one field content
100
 
        """
101
 
        res = {}
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
105
 
        return res
106
 
 
107
 
    def _get_have_donation_certificate(self, cr, uid, ids, field_name=None, arg=None, context=None):
108
 
        """
109
 
        If this invoice have a stock picking in which there is a Certificate of Donation, return True. Otherwise return False.
110
 
        """
111
 
        res = {}
112
 
        for i in self.browse(cr, uid, ids):
113
 
            res[i.id] = False
114
 
            if i.picking_id:
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')])
116
 
                if a_ids:
117
 
                    res[i.id] = True
118
 
        return res
119
 
 
120
 
    _columns = {
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?"),
133
 
    }
134
 
 
135
 
    _defaults = {
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),
139
 
    }
140
 
 
141
 
    def log(self, cr, uid, id, message, secondary=False, context=None):
142
 
        """
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.
145
 
        """
146
 
        if not context:
147
 
            context = {}
148
 
        # Prepare some values
149
 
        # Search donation view and return it
150
 
        try:
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')
157
 
        except ValueError:
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)
174
 
                if m and m.groups():
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)
181
 
 
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)
191
 
 
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):
194
 
        """
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.
199
 
        """
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
208
 
            if not account_id:
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)
215
 
            if p:
216
 
                c_id = False
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
221
 
                if c_id:
222
 
                    if not res.get('value', False):
223
 
                        res['value'] = {'currency_id': c_id}
224
 
                    else:
225
 
                        res['value'].update({'currency_id': c_id})
226
 
 
227
 
        return res
228
 
 
229
 
    def _refund_cleanup_lines(self, cr, uid, lines):
230
 
        """
231
 
        Remove useless fields
232
 
        """
233
 
        for line in lines:
234
 
            del line['move_lines']
235
 
            del line['import_invoice_id']
236
 
        res = super(account_invoice, self)._refund_cleanup_lines(cr, uid, lines)
237
 
        return res
238
 
 
239
 
    def button_debit_note_import_invoice(self, cr, uid, ids, context=None):
240
 
        """
241
 
        Launch wizard that permits to import invoice on a debit note
242
 
        """
243
 
        # Some verifications
244
 
        if not context:
245
 
            context = {}
246
 
        if isinstance(ids, (int, long)):
247
 
            ids = [ids]
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})
254
 
            context.update({
255
 
                'active_id': inv.id,
256
 
                'active_ids': ids,
257
 
            })
258
 
            return {
259
 
                'type': 'ir.actions.act_window',
260
 
                'res_model': 'debit.note.import.invoice',
261
 
                'name': 'Import invoice',
262
 
                'view_type': 'form',
263
 
                'view_mode': 'form',
264
 
                'res_id': w_id,
265
 
                'context': context,
266
 
                'target': 'new',
267
 
            }
268
 
 
269
 
    def button_donation_certificate(self, cr, uid, ids, context=None):
270
 
        """
271
 
        """
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
279
 
            return {
280
 
                'name': "Certificate of Donation",
281
 
                'type': 'ir.actions.act_window',
282
 
                'res_model': 'ir.attachment',
283
 
                'view_type': 'form',
284
 
                'view_mode': 'tree,form',
285
 
                'view_id': [view_id],
286
 
                'search_view_id': search_view_id,
287
 
                'domain': domain,
288
 
                'context': context,
289
 
                'target': 'current',
290
 
            }
291
 
        return False
292
 
 
293
 
    def copy(self, cr, uid, id, default=None, context=None):
294
 
        """
295
 
        Copy global distribution and give it to new invoice
296
 
        """
297
 
        if not context:
298
 
            context = {}
299
 
        if default is None:
300
 
            default = {}
301
 
        default.update({'partner_move_line': False, 'imported_invoices': False})
302
 
        return super(account_invoice, self).copy(cr, uid, id, default, context)
303
 
 
304
 
    def finalize_invoice_move_lines(self, cr, uid, inv, line):
305
 
        """
306
 
        Hook that changes move line data before write them.
307
 
        Add a link between partner move line and invoice.
308
 
        """
309
 
        def is_partner_line(dico):
310
 
            if isinstance(dico, dict):
311
 
                if dico:
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:
315
 
                        return True
316
 
            return False
317
 
        new_line = []
318
 
        for el in line:
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]))
322
 
            else:
323
 
                new_line.append(el)
324
 
        res = super(account_invoice, self).finalize_invoice_move_lines(cr, uid, inv, new_line)
325
 
        return res
326
 
 
327
 
    def line_get_convert(self, cr, uid, x, part, date, context=None):
328
 
        """
329
 
        Add these field into invoice line:
330
 
        - invoice_line_id
331
 
        """
332
 
        if not context:
333
 
            context = {}
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)})
336
 
        return res
337
 
 
338
 
    def action_reconcile_imported_invoice(self, cr, uid, ids, context=None):
339
 
        """
340
 
        Reconcile each imported invoice with its attached invoice line
341
 
        """
342
 
        # some verifications
343
 
        if isinstance(ids, (int, long)):
344
 
            ids = [ids]
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:
349
 
                    continue
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)
355
 
                if not rec:
356
 
                    return False
357
 
        return True
358
 
 
359
 
    def action_open_invoice(self, cr, uid, ids, context=None, *args):
360
 
        """
361
 
        Launch reconciliation of imported invoice lines from a debit note invoice
362
 
        """
363
 
        # some verifications
364
 
        if not context:
365
 
            context = {}
366
 
        if isinstance(ids, (int, long)):
367
 
            ids = [ids]
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):
370
 
            res = False
371
 
        return res
372
 
 
373
 
    def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
374
 
        """
375
 
        Rename Supplier/Customer to "Donor" if view_type == tree
376
 
        """
377
 
        if not context:
378
 
            context = {}
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']")
383
 
            name = _('Donor')
384
 
            if context.get('journal_type') == 'intermission':
385
 
                name = _('Partner')
386
 
            for node in nodes:
387
 
                node.set('string', name)
388
 
            res['arch'] = etree.tostring(doc)
389
 
        return res
390
 
 
391
 
    def action_cancel(self, cr, uid, ids, *args):
392
 
        """
393
 
        Reverse move if this object is a In-kind Donation. Otherwise do normal job: cancellation.
394
 
        """
395
 
        to_cancel = []
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
401
 
                if tmp_res:
402
 
                    # Change invoice state
403
 
                    self.write(cr, uid, [i.id], {'state': 'cancel', 'move_id':False})
404
 
                continue
405
 
            to_cancel.append(i.id)
406
 
        return super(account_invoice, self).action_cancel(cr, uid, to_cancel, args)
407
 
 
408
 
account_invoice()
409
 
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: