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

« back to all changes in this revision

Viewing changes to msf_doc_import/account.py

  • Committer: Olivier DOSSMANN
  • Date: 2013-05-31 14:22:09 UTC
  • mto: This revision was merged to the branch mainline in revision 1687.
  • Revision ID: od@tempo-consulting.fr-20130531142209-sbcwvzuema11guzz
UF-1991 [FIX] Problem with wizard on "msg" field. Change it to "name".

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) 2011 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
from time import strftime
 
28
from tempfile import NamedTemporaryFile
 
29
from base64 import decodestring
 
30
from spreadsheet_xml.spreadsheet_xml import SpreadsheetXML
 
31
from csv import DictReader
 
32
import threading
 
33
import pooler
 
34
 
 
35
class msf_doc_import_accounting(osv.osv_memory):
 
36
    _name = 'msf.doc.import.accounting'
 
37
 
 
38
    _columns = {
 
39
        'date': fields.date(string="Migration date", required=True),
 
40
        'file': fields.binary(string="File", filters='*.xml, *.xls', required=True),
 
41
        'filename': fields.char(string="Imported filename", size=256),
 
42
        'progression': fields.float(string="Progression", readonly=True),
 
43
        'message': fields.char(string="Message", size=256, readonly=True),
 
44
        'state': fields.selection([('draft', 'Created'), ('inprogress', 'In Progress'), ('error', 'Error'), ('done', 'Done')], string="State", readonly=True, required=True),
 
45
        'error_ids': fields.one2many('msf.doc.import.accounting.errors', 'wizard_id', "Errors", readonly=True),
 
46
    }
 
47
 
 
48
    _defaults = {
 
49
        'date': lambda *a: strftime('%Y-%m-%d'),
 
50
        'progression': lambda *a: 0.0,
 
51
        'state': lambda *a: 'draft',
 
52
        'message': lambda *a: _('Initialisation…'),
 
53
    }
 
54
 
 
55
    def create_entries(self, cr, uid, ids, context=None):
 
56
        """
 
57
        Create journal entry 
 
58
        """
 
59
        # Checks
 
60
        if not context:
 
61
            context = {}
 
62
        # Prepare some values
 
63
        res = True
 
64
        journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'migration')])
 
65
        if not journal_ids:
 
66
            raise osv.except_osv(_('Warning'), _('No migration journal found!'))
 
67
        journal_id = journal_ids[0]
 
68
        # Fetch default funding pool: MSF Private Fund
 
69
        try: 
 
70
            msf_fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
 
71
        except ValueError:
 
72
            msf_fp_id = 0
 
73
        # Browse all wizards
 
74
        for w in self.browse(cr, uid, ids):
 
75
            # Search lines
 
76
            entries = self.pool.get('msf.doc.import.accounting.lines').search(cr, uid, [('wizard_id', '=', w.id)])
 
77
            if not entries:
 
78
                # Update wizard
 
79
                self.write(cr, uid, [w.id], {'message': _('No lines…'), 'progression': 100.0})
 
80
                return res
 
81
            # Browse result
 
82
            b_entries = self.pool.get('msf.doc.import.accounting.lines').browse(cr, uid, entries)
 
83
            # Update wizard
 
84
            self.write(cr, uid, [w.id], {'message': _('Grouping by currencies…'), 'progression': 10.0})
 
85
            # Search all currencies (to create moves)
 
86
            available_currencies = []
 
87
            for entry in b_entries:
 
88
                if (entry.currency_id.id, entry.period_id.id) not in available_currencies:
 
89
                    available_currencies.append((entry.currency_id.id, entry.period_id.id))
 
90
            # Delete duplicates
 
91
            if available_currencies and len(available_currencies) > 1:
 
92
                available_currencies = list(set(available_currencies))
 
93
            # Update wizard
 
94
            self.write(cr, uid, ids, {'message': _('Writing a move for each currency…'), 'progression': 20.0})
 
95
            num = 1
 
96
            nb_currencies = float(len(available_currencies))
 
97
            current_percent = 20.0
 
98
            remaining_percent = 80.0
 
99
            step = float(remaining_percent / nb_currencies)
 
100
            for c_id, p_id in available_currencies:
 
101
                # Create a move
 
102
                move_vals = {
 
103
                    'currency_id': c_id,
 
104
                    'manual_currency_id': c_id,
 
105
                    'journal_id': journal_id,
 
106
                    'document_date': w.date,
 
107
                    'date': w.date,
 
108
                    'period_id': p_id,
 
109
                    'status': 'manu',
 
110
                }
 
111
                move_id = self.pool.get('account.move').create(cr, uid, move_vals, context)
 
112
                for l_num, l in enumerate(b_entries):
 
113
                    # Update wizard
 
114
                    progression = 20.0 + ((float(l_num) / float(len(b_entries))) * step) + (float(num - 1) * step)
 
115
                    self.write(cr, uid, [w.id], {'progression': progression})
 
116
                    if l.currency_id.id == c_id and l.period_id.id == p_id:
 
117
                        distrib_id = False
 
118
                        # Create analytic distribution
 
119
                        if l.account_id.user_type_code == 'expense':
 
120
                            distrib_id = self.pool.get('analytic.distribution').create(cr, uid, {}, context)
 
121
                            common_vals = {
 
122
                                'distribution_id': distrib_id,
 
123
                                'currency_id': c_id,
 
124
                                'percentage': 100.0,
 
125
                                'date': l.date,
 
126
                                'source_date': l.date,
 
127
                                'destination_id': l.destination_id.id,
 
128
                            }
 
129
                            common_vals.update({'analytic_id': l.cost_center_id.id,})
 
130
                            cc_res = self.pool.get('cost.center.distribution.line').create(cr, uid, common_vals)
 
131
                            common_vals.update({'analytic_id': msf_fp_id, 'cost_center_id': l.cost_center_id.id,})
 
132
                            fp_res = self.pool.get('funding.pool.distribution.line').create(cr, uid, common_vals)
 
133
                        # Create move line
 
134
                        move_line_vals = {
 
135
                            'move_id': move_id,
 
136
                            'name': l.description,
 
137
                            'reference': l.ref,
 
138
                            'account_id': l.account_id.id,
 
139
                            'period_id': p_id,
 
140
                            'document_date': l.document_date,
 
141
                            'date': l.date,
 
142
                            'journal_id': journal_id,
 
143
                            'debit_currency': l.debit,
 
144
                            'credit_currency': l.credit,
 
145
                            'currency_id': c_id,
 
146
                            'analytic_distribution_id': distrib_id,
 
147
                            'partner_id': l.partner_id and l.partner_id.id or False,
 
148
                            'employee_id': l.employee_id and l.employee_id.id or False,
 
149
                        }
 
150
                        self.pool.get('account.move.line').create(cr, uid, move_line_vals, context, check=False)
 
151
                # Update wizard
 
152
                progression = 20.0 + (float(num) * step)
 
153
                self.write(cr, uid, [w.id], {'progression': progression})
 
154
                num += 1
 
155
        return res
 
156
 
 
157
    def _import(self, dbname, uid, ids, context=None):
 
158
        """
 
159
        Do treatment before validation:
 
160
        - check data from wizard
 
161
        - check that file exists and that data are inside
 
162
        - check integrity of data in files
 
163
        """
 
164
        # Some checks
 
165
        if not context:
 
166
            context = {}
 
167
        # Prepare some values
 
168
        cr = pooler.get_db(dbname).cursor()
 
169
        created = 0
 
170
        processed = 0
 
171
        errors = []
 
172
 
 
173
        # Update wizard
 
174
        self.write(cr, uid, ids, {'message': _('Cleaning up old imports…'), 'progression': 1.00})
 
175
        # Clean up old temporary imported lines
 
176
        old_lines_ids = self.pool.get('msf.doc.import.accounting.lines').search(cr, uid, [])
 
177
        self.pool.get('msf.doc.import.accounting.lines').unlink(cr, uid, old_lines_ids)
 
178
 
 
179
        # Check wizard data
 
180
        for wiz in self.browse(cr, uid, ids):
 
181
            # Update wizard
 
182
            self.write(cr, uid, [wiz.id], {'message': _('Checking file…'), 'progression': 2.00})
 
183
 
 
184
            # Check that a file was given
 
185
            if not wiz.file:
 
186
                raise osv.except_osv(_('Error'), _('Nothing to import.'))
 
187
            # Update wizard
 
188
            self.write(cr, uid, [wiz.id], {'message': _('Copying file…'), 'progression': 3.00})
 
189
            fileobj = NamedTemporaryFile('w+b', delete=False)
 
190
            fileobj.write(decodestring(wiz.file))
 
191
            fileobj.close()
 
192
            content = SpreadsheetXML(xmlfile=fileobj.name)
 
193
            if not content:
 
194
                raise osv.except_osv(_('Warning'), _('No content.'))
 
195
            # Update wizard
 
196
            self.write(cr, uid, [wiz.id], {'message': _('Processing line number…'), 'progression': 4.00})
 
197
            rows = content.getRows()
 
198
            nb_rows = len([x for x in content.getRows()])
 
199
            # Update wizard
 
200
            self.write(cr, uid, [wiz.id], {'message': _('Reading headers…'), 'progression': 5.00})
 
201
            # Use the first row to find which column to use
 
202
            cols = {}
 
203
            col_names = ['Description', 'Reference', 'Document Date', 'Posting Date', 'G/L Account', 'Third party', 'Destination', 'Cost Centre', 'Booking Debit', 'Booking Credit', 'Booking Currency']
 
204
            for num, r in enumerate(rows):
 
205
                header = [x and x.data for x in r.iter_cells()]
 
206
                for el in col_names:
 
207
                    if el in header:
 
208
                        cols[el] = header.index(el)
 
209
                break
 
210
            # Number of line to bypass in line's count
 
211
            base_num = 2
 
212
            for el in col_names:
 
213
                if not el in cols:
 
214
                    raise osv.except_osv(_('Error'), _("'%s' column not found in file.") % (el,))
 
215
            # All lines
 
216
            money = {}
 
217
            # Update wizard
 
218
            self.write(cr, uid, [wiz.id], {'message': _('Reading lines…'), 'progression': 6.00})
 
219
            # Check file's content
 
220
            for num, r in enumerate(rows):
 
221
                # Update wizard
 
222
                percent = (float(num+1) / float(nb_rows+1)) * 100.0
 
223
                progression = ((float(num+1) * 94) / float(nb_rows)) + 6
 
224
                self.write(cr, uid, [wiz.id], {'message': _('Checking file…'), 'progression': progression})
 
225
                # Prepare some values
 
226
                r_debit = 0
 
227
                r_credit = 0
 
228
                r_currency = False
 
229
                r_partner = False
 
230
                r_account = False
 
231
                r_destination = False
 
232
                r_cc = False
 
233
                r_document_date = False
 
234
                r_date = False
 
235
                r_period = False
 
236
                current_line_num = num + base_num
 
237
                # Fetch all XML row values
 
238
                line = self.pool.get('import.cell.data').get_line_values(cr, uid, ids, r)
 
239
                # Bypass this line if NO debit AND NO credit
 
240
                if not line[cols['Booking Debit']] and not line[cols['Booking Credit']]:
 
241
                    continue
 
242
                processed += 1
 
243
                # Check that currency is active
 
244
                if not line[cols['Booking Currency']]:
 
245
                    errors.append(_('Line %s. No currency specified!') % (current_line_num,))
 
246
                    continue
 
247
                curr_ids = self.pool.get('res.currency').search(cr, uid, [('name', '=', line[cols['Booking Currency']])])
 
248
                if not curr_ids:
 
249
                    errors.append(_('Line %s. Currency not found: %s') % (current_line_num, line[cols['Booking Currency']],))
 
250
                    continue
 
251
                for c in self.pool.get('res.currency').browse(cr, uid, curr_ids):
 
252
                    if not c.active:
 
253
                        errors.append(_('Line %s. Currency is not active: %s') % (current_line_num, line[cols['Booking Currency']],))
 
254
                        continue
 
255
                r_currency = curr_ids[0]
 
256
                if not line[cols['Booking Currency']] in money:
 
257
                    money[line[cols['Booking Currency']]] = {}
 
258
                if not 'debit' in money[line[cols['Booking Currency']]]:
 
259
                    money[line[cols['Booking Currency']]]['debit'] = 0
 
260
                if not 'credit' in money[line[cols['Booking Currency']]]:
 
261
                    money[line[cols['Booking Currency']]]['credit'] = 0
 
262
                if not 'name' in money[line[cols['Booking Currency']]]:
 
263
                    money[line[cols['Booking Currency']]]['name'] = line[cols['Booking Currency']]
 
264
                # Increment global debit/credit
 
265
                if line[cols['Booking Debit']]:
 
266
                    money[line[cols['Booking Currency']]]['debit'] += line[cols['Booking Debit']]
 
267
                    r_debit = line[cols['Booking Debit']]
 
268
                if line[cols['Booking Credit']]:
 
269
                    money[line[cols['Booking Currency']]]['credit'] += line[cols['Booking Credit']]
 
270
                    r_credit = line[cols['Booking Credit']]
 
271
                # Check document/posting dates
 
272
                if not line[cols['Document Date']]:
 
273
                    errors.append(_('Line %s. No document date specified!') % (current_line_num,))
 
274
                    continue
 
275
                if not line[cols['Posting Date']]:
 
276
                    errors.append(_('Line %s. No posting date specified!') % (current_line_num,))
 
277
                    continue
 
278
                if line[cols['Document Date']] > line[cols['Posting Date']]:
 
279
                    errors.append(_("Line %s. Document date '%s' should be inferior or equal to Posting date '%s'.") % (current_line_num, line[cols['Document Date']], line[cols['Posting Date']],))
 
280
                    continue
 
281
                # Fetch document date and posting date
 
282
                r_document_date = line[cols['Document Date']].strftime('%Y-%m-%d')
 
283
                r_date = line[cols['Posting Date']].strftime('%Y-%m-%d')
 
284
                # Check that a period exist and is open
 
285
                period_ids = self.pool.get('account.period').get_period_from_date(cr, uid, r_date, context)
 
286
                if not period_ids:
 
287
                    errors.append(_('Line %s. No period found for given date: %s') % (current_line_num, r_date))
 
288
                    continue
 
289
                r_period = period_ids[0]
 
290
                # Check G/L account
 
291
                if not line[cols['G/L Account']]:
 
292
                    errors.append(_('Line %s. No G/L account specified!') % (current_line_num,))
 
293
                    continue
 
294
                account_ids = self.pool.get('account.account').search(cr, uid, [('code', '=', line[cols['G/L Account']])])
 
295
                if not account_ids:
 
296
                    errors.append(_('Line %s. G/L account %s not found!') % (current_line_num, line[cols['G/L Account']],))
 
297
                    continue
 
298
                r_account = account_ids[0]
 
299
                account = self.pool.get('account.account').browse(cr, uid, r_account)
 
300
                # Check that Third party exists (if not empty)
 
301
                tp_label = 'Partner'
 
302
                if line[cols['Third party']]:
 
303
                    if account.type_for_register == 'advance':
 
304
                        tp_ids = self.pool.get('hr.employee').search(cr, uid, [('name', '=', line[cols['Third party']])])
 
305
                        tp_label = 'Employee'
 
306
                    else:
 
307
                        tp_ids = self.pool.get('res.partner').search(cr, uid, [('name', '=', line[cols['Third party']])])
 
308
                    if not tp_ids:
 
309
                        errors.append(_('Line %s. %s not found: %s') % (current_line_num, tp_label, line[cols['Third party']],))
 
310
                        continue
 
311
                    r_partner = tp_ids[0]
 
312
                # Check analytic axis only if G/L account is an expense account
 
313
                if account.user_type_code == 'expense':
 
314
                    # Check Destination
 
315
                    if not line[cols['Destination']]:
 
316
                        errors.append(_('Line %s. No destination specified!') % (current_line_num,))
 
317
                        continue
 
318
                    destination_ids = self.pool.get('account.analytic.account').search(cr, uid, [('category', '=', 'DEST'), '|', ('name', '=', line[cols['Destination']]), ('code', '=', line[cols['Destination']])])
 
319
                    if not destination_ids:
 
320
                        errors.append(_('Line %s. Destination %s not found!') % (current_line_num, line[cols['Destination']],))
 
321
                        continue
 
322
                    r_destination = destination_ids[0]
 
323
                    # Check Cost Center
 
324
                    if not line[cols['Cost Centre']]:
 
325
                        errors.append(_('Line %s. No cost center specified:') % (current_line_num,))
 
326
                        continue
 
327
                    cc_ids = self.pool.get('account.analytic.account').search(cr, uid, [('category', '=', 'OC'), '|', ('name', '=', line[cols['Cost Centre']]), ('code', '=', line[cols['Cost Centre']])])
 
328
                    if not cc_ids:
 
329
                        errors.append(_('Line %s. Cost Center %s not found!') % (current_line_num, line[cols['Cost Centre']]))
 
330
                        continue
 
331
                    r_cc = cc_ids[0]
 
332
                # NOTE: There is no need to check G/L account, Cost Center and Destination regarding document/posting date because this check is already done at Journal Entries validation.
 
333
 
 
334
                # Registering data regarding these "keys":
 
335
                # - G/L Account
 
336
                # - Third Party
 
337
                # - Destination
 
338
                # - Cost Centre
 
339
                # - Booking Currency
 
340
                vals = {
 
341
                    'description': line[cols['Description']] or '',
 
342
                    'ref': line[cols['Reference']] or '',
 
343
                    'account_id': r_account or False,
 
344
                    'debit': r_debit or 0.0,
 
345
                    'credit': r_credit or 0.0,
 
346
                    'cost_center_id': r_cc or False,
 
347
                    'destination_id': r_destination or False,
 
348
                    'document_date': r_document_date or False,
 
349
                    'date': r_date or False,
 
350
                    'currency_id': r_currency or False,
 
351
                    'wizard_id': wiz.id,
 
352
                    'period_id': r_period or False,
 
353
                }
 
354
                if account.type_for_register == 'advance':
 
355
                    vals.update({'employee_id': r_partner,})
 
356
                else:
 
357
                    vals.update({'partner_id': r_partner,})
 
358
                line_res = self.pool.get('msf.doc.import.accounting.lines').create(cr, uid, vals, context)
 
359
                if not line_res:
 
360
                    errors.append(_('Line %s. A problem occured for line registration. Please contact an Administrator.') % (current_line_num,))
 
361
                    continue
 
362
                created += 1
 
363
            # Check if all is ok for the file
 
364
            ## The lines should be balanced for each currency
 
365
            for c in money:
 
366
                if (money[c]['debit'] - money[c]['credit']) >= 10**-2:
 
367
                    raise osv.except_osv(_('Error'), _('Currency %s is not balanced: %s' ) % (money[c]['name'], (money[c]['debit'] - money[c]['credit']),))
 
368
 
 
369
        # Update wizard
 
370
        self.write(cr, uid, ids, {'message': _('Check complete. Reading potential errors or write needed changes.'), 'progression': 100.0})
 
371
 
 
372
        wiz_state = 'done'
 
373
        # If errors, cancel probable modifications
 
374
        if errors:
 
375
            #cr.rollback()
 
376
            created = 0
 
377
            message = 'Import FAILED.'
 
378
            # Delete old errors
 
379
            error_ids = self.pool.get('msf.doc.import.accounting.errors').search(cr, uid, [], context)
 
380
            if error_ids:
 
381
                self.pool.get('msf.doc.import.accounting.errors').unlink(cr, uid, error_ids ,context)
 
382
            # create errors lines
 
383
            for e in errors:
 
384
                self.pool.get('msf.doc.import.accounting.errors').create(cr, uid, {'wizard_id': wiz.id, 'name': e}, context)
 
385
            wiz_state = 'error'
 
386
        else:
 
387
            # Update wizard
 
388
            self.write(cr, uid, ids, {'message': _('Writing changes…'), 'progression': 0.0})
 
389
            # Create all journal entries
 
390
            self.create_entries(cr, uid, ids, context)
 
391
            message = 'Import successful.'
 
392
 
 
393
        # Update wizard
 
394
        self.write(cr, uid, ids, {'message': message, 'state': wiz_state, 'progression': 100.0})
 
395
 
 
396
        # Close cursor
 
397
        cr.commit()
 
398
        cr.close()
 
399
        return True
 
400
 
 
401
    def button_validate(self, cr, uid, ids, context=None):
 
402
        """
 
403
        Launch process in a thread and return a wizard
 
404
        """
 
405
        # Some checks
 
406
        if not context:
 
407
            context = {}
 
408
        # Launch a thread
 
409
        thread = threading.Thread(target=self._import, args=(cr.dbname, uid, ids, context))
 
410
        thread.start()
 
411
        
 
412
        return self.write(cr, uid, ids, {'state': 'inprogress'}, context)
 
413
 
 
414
    def button_update(self, cr, uid, ids, context=None):
 
415
        """
 
416
        Update view
 
417
        """
 
418
        return False
 
419
 
 
420
msf_doc_import_accounting()
 
421
 
 
422
class msf_doc_import_accounting_lines(osv.osv):
 
423
    _name = 'msf.doc.import.accounting.lines'
 
424
 
 
425
    _columns = {
 
426
        'description': fields.text("Description", required=False, readonly=True),
 
427
        'ref': fields.text("Reference", required=False, readonly=True),
 
428
        'document_date': fields.date("Document date", required=True, readonly=True),
 
429
        'date': fields.date("Posting date", required=True, readonly=True),
 
430
        'account_id': fields.many2one('account.account', "G/L Account", required=True, readonly=True),
 
431
        'destination_id': fields.many2one('account.analytic.account', "Destination", required=False, readonly=True),
 
432
        'cost_center_id': fields.many2one('account.analytic.account', "Cost Center", required=False, readonly=True),
 
433
        'debit': fields.float("Debit", required=False, readonly=True),
 
434
        'credit': fields.float("Credit", required=False, readonly=True),
 
435
        'currency_id': fields.many2one('res.currency', "Currency", required=True, readonly=True),
 
436
        'partner_id': fields.many2one('res.partner', "Partner", required=False, readonly=True),
 
437
        'employee_id': fields.many2one('hr.employee', "Employee", required=False, readonly=True),
 
438
        'period_id': fields.many2one('account.period', "Period", required=True, readonly=True),
 
439
        'wizard_id': fields.integer("Wizard", required=True, readonly=True),
 
440
    }
 
441
 
 
442
    _defaults = {
 
443
        'description': lambda *a: '',
 
444
        'ref': lambda *a: '',
 
445
        'document_date': lambda *a: strftime('%Y-%m-%d'),
 
446
        'date': lambda *a: strftime('%Y-%m-%d'),
 
447
        'debit': lambda *a: 0.0,
 
448
        'credit': lambda *a: 0.0,
 
449
    }
 
450
 
 
451
msf_doc_import_accounting_lines()
 
452
 
 
453
class msf_doc_import_accounting_errors(osv.osv_memory):
 
454
    _name = 'msf.doc.import.accounting.errors'
 
455
 
 
456
    _columns = {
 
457
        'name': fields.text("Description", readonly=True, required=True),
 
458
        'wizard_id': fields.many2one('msf.doc.import.accounting', "Wizard", required=True, readonly=True),
 
459
    }
 
460
 
 
461
msf_doc_import_accounting_errors()
 
462
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: