2
# -*- coding: utf-8 -*-
3
##############################################################################
5
# OpenERP, Open Source Management Solution
6
# Copyright (C) 2011 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 _
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
34
from msf_doc_import import ACCOUNTING_IMPORT_JOURNALS
36
class msf_doc_import_accounting(osv.osv_memory):
37
_name = 'msf.doc.import.accounting'
40
'date': fields.date(string="Date", required=True),
41
'file': fields.binary(string="File", filters='*.xml, *.xls', required=True),
42
'filename': fields.char(string="Imported filename", size=256),
43
'progression': fields.float(string="Progression", readonly=True),
44
'message': fields.char(string="Message", size=256, readonly=True),
45
'state': fields.selection([('draft', 'Created'), ('inprogress', 'In Progress'), ('error', 'Error'), ('done', 'Done')], string="State", readonly=True, required=True),
46
'error_ids': fields.one2many('msf.doc.import.accounting.errors', 'wizard_id', "Errors", readonly=True),
50
'date': lambda *a: strftime('%Y-%m-%d'),
51
'progression': lambda *a: 0.0,
52
'state': lambda *a: 'draft',
53
'message': lambda *a: _('Initialization…'),
56
def create_entries(self, cr, uid, ids, journal_id, context=None):
64
msf_fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
68
for w in self.browse(cr, uid, ids):
70
entries = self.pool.get('msf.doc.import.accounting.lines').search(cr, uid, [('wizard_id', '=', w.id)])
72
raise osv.except_osv(_('Error'), _('No lines…'))
74
b_entries = self.pool.get('msf.doc.import.accounting.lines').browse(cr, uid, entries)
76
self.write(cr, uid, [w.id], {'message': _('Grouping by currencies…'), 'progression': 10.0})
77
# Search all currencies (to create moves)
78
available_currencies = {}
79
for entry in b_entries:
80
if (entry.currency_id.id, entry.period_id.id) not in available_currencies:
81
available_currencies[(entry.currency_id.id, entry.period_id.id)] = []
82
available_currencies[(entry.currency_id.id, entry.period_id.id)].append(entry)
84
self.write(cr, uid, ids, {'message': _('Writing a move for each currency…'), 'progression': 20.0})
86
nb_currencies = float(len(available_currencies))
87
current_percent = 20.0
88
remaining_percent = 80.0
89
step = float(remaining_percent / nb_currencies)
90
for c_id, p_id in available_currencies:
94
'manual_currency_id': c_id,
95
'journal_id': journal_id,
96
'document_date': w.date,
102
move_id = self.pool.get('account.move').create(cr, uid, move_vals, context)
103
for l_num, l in enumerate(available_currencies[(c_id, p_id)]):
105
progression = 20.0 + ((float(l_num) / float(len(b_entries))) * step) + (float(num - 1) * step)
106
self.write(cr, uid, [w.id], {'progression': progression})
108
# Create analytic distribution
109
if l.account_id.is_analytic_addicted:
110
distrib_id = self.pool.get('analytic.distribution').create(cr, uid, {}, context)
112
'distribution_id': distrib_id,
116
'source_date': l.date,
117
'destination_id': l.destination_id.id,
119
common_vals.update({'analytic_id': l.cost_center_id.id,})
120
cc_res = self.pool.get('cost.center.distribution.line').create(cr, uid, common_vals)
121
common_vals.update({'analytic_id': l.funding_pool_id.id, 'cost_center_id': l.cost_center_id.id,})
122
fp_res = self.pool.get('funding.pool.distribution.line').create(cr, uid, common_vals)
126
'name': l.description,
128
'account_id': l.account_id.id,
130
'document_date': l.document_date,
132
'journal_id': journal_id,
133
'debit_currency': l.debit,
134
'credit_currency': l.credit,
136
'analytic_distribution_id': distrib_id,
137
'partner_id': l.partner_id and l.partner_id.id or False,
138
'employee_id': l.employee_id and l.employee_id.id or False,
139
'transfer_journal_id': l.transfer_journal_id and l.transfer_journal_id.id or False,
141
self.pool.get('account.move.line').create(cr, uid, move_line_vals, context, check=False)
142
# Validate the Journal Entry for lines to be valid (if possible)
143
self.write(cr, uid, [w.id], {'message': _('Validating journal entry…')})
144
self.pool.get('account.move').validate(cr, uid, [move_id], context=context)
146
progression = 20.0 + (float(num) * step)
147
self.write(cr, uid, [w.id], {'progression': progression})
151
def _import(self, dbname, uid, ids, context=None):
153
Do treatment before validation:
154
- check data from wizard
155
- check that file exists and that data are inside
156
- check integrity of data in files
161
# Prepare some values
163
if context.get('from_yml', False):
164
from_yml = context.get('from_yml')
165
# Do changes because of YAML tests
169
cr = pooler.get_db(dbname).cursor()
171
msf_fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
177
current_instance = self.pool.get('res.users').browse(cr, uid, uid).company_id.instance_id.id or False
181
self.write(cr, uid, ids, {'message': _('Cleaning up old imports…'), 'progression': 1.00})
182
# Clean up old temporary imported lines
183
old_lines_ids = self.pool.get('msf.doc.import.accounting.lines').search(cr, uid, [])
184
self.pool.get('msf.doc.import.accounting.lines').unlink(cr, uid, old_lines_ids)
187
for wiz in self.browse(cr, uid, ids):
189
self.write(cr, uid, [wiz.id], {'message': _('Checking file…'), 'progression': 2.00})
190
# UF-2045: Check that the given date is in an open period
191
wiz_period_ids = self.pool.get('account.period').get_period_from_date(cr, uid, wiz.date, context)
192
if not wiz_period_ids:
193
raise osv.except_osv(_('Warning'), _('No period found!'))
194
period = self.pool.get('account.period').browse(cr, uid, wiz_period_ids[0], context)
195
if not period or period.state in ['created', 'done']:
196
raise osv.except_osv(_('Warning'), _('Period is not open!'))
197
date = wiz.date or False
199
# Check that a file was given
201
raise osv.except_osv(_('Error'), _('Nothing to import.'))
203
self.write(cr, uid, [wiz.id], {'message': _('Copying file…'), 'progression': 3.00})
204
fileobj = NamedTemporaryFile('w+b', delete=False)
205
fileobj.write(decodestring(wiz.file))
207
content = SpreadsheetXML(xmlfile=fileobj.name)
209
raise osv.except_osv(_('Warning'), _('No content'))
211
self.write(cr, uid, [wiz.id], {'message': _('Processing line…'), 'progression': 4.00})
212
rows = content.getRows()
213
nb_rows = len([x for x in content.getRows()])
215
self.write(cr, uid, [wiz.id], {'message': _('Reading headers…'), 'progression': 5.00})
216
# Use the first row to find which column to use
218
col_names = ['Journal Code', 'Description', 'Reference', 'Document Date', 'Posting Date', 'G/L Account', 'Partner', 'Employee', 'Journal', 'Destination', 'Cost Centre', 'Funding Pool', 'Booking Debit', 'Booking Credit', 'Booking Currency']
219
for num, r in enumerate(rows):
220
header = [x and x.data for x in r.iter_cells()]
223
cols[el] = header.index(el)
225
# Number of line to bypass in line's count
228
# global journal code for the file
230
aj_obj = self.pool.get('account.journal')
234
raise osv.except_osv(_('Error'), _("'%s' column not found in file.") % (el or '',))
238
self.write(cr, uid, [wiz.id], {'message': _('Reading lines…'), 'progression': 6.00})
239
# Check file's content
240
for num, r in enumerate(rows):
242
percent = (float(num+1) / float(nb_rows+1)) * 100.0
243
progression = ((float(num+1) * 94) / float(nb_rows)) + 6
244
self.write(cr, uid, [wiz.id], {'message': _('Checking file…'), 'progression': progression})
245
# Prepare some values
253
r_destination = False
256
# UTP-1047: Use Document date column (contrary of UTP-766)
257
r_document_date = False
258
current_line_num = num + base_num
259
# Fetch all XML row values
260
line = self.pool.get('import.cell.data').get_line_values(cr, uid, ids, r)
261
# Check document date
262
if not line[cols['Document Date']]:
263
errors.append(_('Line %s. No document date specified!') % (current_line_num,))
265
r_document_date = line[cols['Document Date']].strftime('%Y-%m-%d')
266
# Bypass this line if NO debit AND NO credit
268
bd = line[cols['Booking Debit']]
269
except IndexError, e:
272
bc = line[cols['Booking Credit']]
273
except IndexError, e:
275
if not line[cols['Booking Debit']] and not line[cols['Booking Credit']]:
278
# Check that currency is active
279
if not line[cols['Booking Currency']]:
280
errors.append(_('Line %s. No currency specified!') % (current_line_num,))
282
curr_ids = self.pool.get('res.currency').search(cr, uid, [('name', '=', line[cols['Booking Currency']])])
284
errors.append(_('Line %s. Currency not found: %s') % (current_line_num, line[cols['Booking Currency']],))
286
for c in self.pool.get('res.currency').browse(cr, uid, curr_ids):
288
errors.append(_('Line %s. Currency is not active: %s') % (current_line_num, line[cols['Booking Currency']],))
290
r_currency = curr_ids[0]
291
if not line[cols['Booking Currency']] in money:
292
money[line[cols['Booking Currency']]] = {}
293
if not 'debit' in money[line[cols['Booking Currency']]]:
294
money[line[cols['Booking Currency']]]['debit'] = 0
295
if not 'credit' in money[line[cols['Booking Currency']]]:
296
money[line[cols['Booking Currency']]]['credit'] = 0
297
if not 'name' in money[line[cols['Booking Currency']]]:
298
money[line[cols['Booking Currency']]]['name'] = line[cols['Booking Currency']]
299
# Increment global debit/credit
300
if line[cols['Booking Debit']]:
301
money[line[cols['Booking Currency']]]['debit'] += line[cols['Booking Debit']]
302
r_debit = line[cols['Booking Debit']]
303
if line[cols['Booking Credit']]:
304
money[line[cols['Booking Currency']]]['credit'] += line[cols['Booking Credit']]
305
r_credit = line[cols['Booking Credit']]
307
# Check which journal it is to be posted to: should be of type OD, MIG or INT
308
if not line[cols['Journal Code']]:
309
errors.append(_('Line %s. No Journal Code specified') % (current_line_num,))
312
# check for a valid journal code
313
aj_ids = aj_obj.search(cr, uid, [('code', '=', line[cols['Journal Code']]), ('instance_id', '=', current_instance)])
315
errors.append(_('Line %s. Journal Code not found: %s.') % (current_line_num, line[cols['Journal Code']]))
318
aj_data = aj_obj.read(cr, uid, aj_ids, ['type'])[0]
319
if aj_data.get('type', False) is False or aj_data.get('type', False) not in ACCOUNTING_IMPORT_JOURNALS:
320
journal_list = ', '.join([x[1] for x in aj_obj.get_journal_type(cr, uid) if x[0] in ACCOUNTING_IMPORT_JOURNALS])
321
errors.append(_('Line %s. Import of entries only allowed on the following journal(s): %s') % (current_line_num, journal_list))
325
file_journal_id = aj_id
327
if file_journal_id != aj_id:
328
errors.append(_('Line %s. Only a single Journal Code can be specified per file') % (current_line_num,))
332
if not line[cols['G/L Account']]:
333
errors.append(_('Line %s. No G/L account specified!') % (current_line_num,))
335
account_ids = self.pool.get('account.account').search(cr, uid, [('code', '=', line[cols['G/L Account']])])
337
errors.append(_('Line %s. G/L account %s not found!') % (current_line_num, line[cols['G/L Account']],))
339
r_account = account_ids[0]
340
account = self.pool.get('account.account').browse(cr, uid, r_account)
341
# Check that Third party exists (if not empty)
344
if line[cols['Partner']]:
345
tp_ids = self.pool.get('res.partner').search(cr, uid, [('name', '=', line[cols['Partner']])])
347
tp_label = _('Partner')
348
tp_content = line[cols['Partner']]
350
r_partner = tp_ids[0]
351
if line[cols['Employee']]:
352
tp_ids = self.pool.get('hr.employee').search(cr, uid, [('name', '=', line[cols['Employee']])])
354
tp_label = _('Employee')
355
tp_content = line[cols['Employee']]
357
r_employee = tp_ids[0]
358
if line[cols['Journal']]:
359
tp_ids = self.pool.get('account.journal').search(cr, uid, ['|', ('name', '=', line[cols['Journal']]), ('code', '=', line[cols['Journal']]), ('instance_id', '=', current_instance)])
361
tp_label = _('Journal')
362
tp_content = line[cols['Journal']]
364
r_journal = tp_ids[0]
365
if tp_label and tp_content:
366
errors.append(_('Line %s. %s not found: %s') % (current_line_num, tp_label, tp_content,))
368
list_third_party = []
370
list_third_party.append(r_employee)
372
list_third_party.append(r_partner)
374
list_third_party.append(r_journal)
375
if len(list_third_party) > 1:
376
errors.append(_('Line %s. You cannot only add partner or employee or journal.') % (current_line_num,))
378
# Check analytic axis only if G/L account is analytic-a-holic
379
if account.is_analytic_addicted:
381
if not line[cols['Destination']]:
382
errors.append(_('Line %s. No destination specified!') % (current_line_num,))
384
destination_ids = self.pool.get('account.analytic.account').search(cr, uid, [('category', '=', 'DEST'), '|', ('name', '=', line[cols['Destination']]), ('code', '=', line[cols['Destination']])])
385
if not destination_ids:
386
errors.append(_('Line %s. Destination %s not found!') % (current_line_num, line[cols['Destination']],))
388
r_destination = destination_ids[0]
390
if not line[cols['Cost Centre']]:
391
errors.append(_('Line %s. No cost center specified!') % (current_line_num,))
393
cc_ids = self.pool.get('account.analytic.account').search(cr, uid, [('category', '=', 'OC'), '|', ('name', '=', line[cols['Cost Centre']]), ('code', '=', line[cols['Cost Centre']])])
395
errors.append(_('Line %s. Cost Center %s not found!') % (current_line_num, line[cols['Cost Centre']]))
398
# Check Funding Pool (added since UTP-1082)
400
if line[cols['Funding Pool']]:
401
fp_ids = self.pool.get('account.analytic.account').search(cr, uid, [('category', '=', 'FUNDING'), '|', ('name', '=', line[cols['Funding Pool']]), ('code', '=', line[cols['Funding Pool']])])
403
errors.append(_('Line %s. Funding Pool %s not found!') % (current_line_num, line[cols['Funding Pool']]))
406
# 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.
408
# Registering data regarding these "keys":
415
'description': line[cols['Description']] or '',
416
'ref': line[cols['Reference']] or '',
417
'account_id': r_account or False,
418
'debit': r_debit or 0.0,
419
'credit': r_credit or 0.0,
420
'cost_center_id': r_cc or False,
421
'destination_id': r_destination or False,
422
'document_date': r_document_date or False,
423
'funding_pool_id': r_fp or False,
424
'date': date or False,
425
'currency_id': r_currency or False,
427
'period_id': period and period.id or False,
428
'employee_id': r_employee or False,
429
'partner_id': r_partner or False,
430
'transfer_journal_id': r_journal or False,
432
# UTP-1056: Add employee possibility. So we need to check if employee and/or partner is authorized
433
partner_needs = self.pool.get('account.bank.statement.line').onchange_account(cr, uid, False, account_id=account.id, context=context)
434
if not partner_needs:
435
errors.append(_('Line %s. No info about given account: %s') % (current_line_num, account.code,))
438
partner_options = partner_needs['value']['partner_type']['options']
439
if r_partner and ('res.partner', 'Partner') not in partner_options:
440
errors.append(_('Line %s. You cannot use a partner for the given account: %s.') % (current_line_num, account.code))
442
if r_employee and ('hr.employee', 'Employee') not in partner_options:
443
errors.append(_('Line %s. You cannot use an employee for the given account: %s.') % (current_line_num, account.code))
445
if r_journal and ('account.journal', 'Journal') not in partner_options:
446
errors.append(_('Line %s. You cannot use a journal for the given account: %s.') % (current_line_num, account.code))
447
line_res = self.pool.get('msf.doc.import.accounting.lines').create(cr, uid, vals, context)
449
errors.append(_('Line %s. A problem occured for line registration. Please contact an Administrator.') % (current_line_num,))
452
# Check if all is ok for the file
453
## The lines should be balanced for each currency
455
if abs(money[c]['debit'] - money[c]['credit']) >= 10**-2:
456
raise osv.except_osv(_('Error'), _('Currency %s is not balanced: %s') % (money[c]['name'], (money[c]['debit'] - money[c]['credit']),))
458
self.write(cr, uid, ids, {'message': _('Check complete. Reading potential errors or write needed changes.'), 'progression': 100.0})
461
# If errors, cancel probable modifications
465
message = _('Import FAILED.')
467
error_ids = self.pool.get('msf.doc.import.accounting.errors').search(cr, uid, [], context)
469
self.pool.get('msf.doc.import.accounting.errors').unlink(cr, uid, error_ids ,context)
470
# create errors lines
472
self.pool.get('msf.doc.import.accounting.errors').create(cr, uid, {'wizard_id': wiz.id, 'name': e}, context)
476
self.write(cr, uid, ids, {'message': _('Writing changes…'), 'progression': 0.0})
477
# Create all journal entries
478
self.create_entries(cr, uid, ids, file_journal_id, context)
479
message = _('Import successful.')
482
self.write(cr, uid, ids, {'message': message, 'state': wiz_state, 'progression': 100.0})
488
except osv.except_osv as osv_error:
490
self.write(cr, uid, ids, {'message': _("An error occured. %s: %s") % (osv_error.name, osv_error.value,), 'state': 'done', 'progression': 100.0})
493
except Exception as e:
495
self.write(cr, uid, ids, {'message': _("An error occured: %s") % (e.args and e.args[0] or '',), 'state': 'done', 'progression': 100.0})
500
def button_validate(self, cr, uid, ids, context=None):
502
Launch process in a thread and return a wizard
507
if context.get('from_yml', False):
508
res = self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
509
self._import(cr, uid, ids, context=context)
511
# Launch a thread if we come from web
512
thread = threading.Thread(target=self._import, args=(cr.dbname, uid, ids, context))
514
res = self.write(cr, uid, ids, {'state': 'inprogress'}, context=context)
517
def button_update(self, cr, uid, ids, context=None):
523
msf_doc_import_accounting()
525
class msf_doc_import_accounting_lines(osv.osv):
526
_name = 'msf.doc.import.accounting.lines'
527
_rec_name = 'document_date'
530
'description': fields.text("Description", required=False, readonly=True),
531
'ref': fields.text("Reference", required=False, readonly=True),
532
'document_date': fields.date("Document date", required=True, readonly=True),
533
'date': fields.date("Posting date", required=True, readonly=True),
534
'account_id': fields.many2one('account.account', "G/L Account", required=True, readonly=True),
535
'destination_id': fields.many2one('account.analytic.account', "Destination", required=False, readonly=True),
536
'cost_center_id': fields.many2one('account.analytic.account', "Cost Center", required=False, readonly=True),
537
'funding_pool_id': fields.many2one('account.analytic.account', "Funding Pool", required=False, readonly=True),
538
'debit': fields.float("Debit", required=False, readonly=True),
539
'credit': fields.float("Credit", required=False, readonly=True),
540
'currency_id': fields.many2one('res.currency', "Currency", required=True, readonly=True),
541
'partner_id': fields.many2one('res.partner', "Partner", required=False, readonly=True),
542
'employee_id': fields.many2one('hr.employee', "Employee", required=False, readonly=True),
543
'transfer_journal_id': fields.many2one('account.journal', 'Journal', required=False, readonly=True),
544
'period_id': fields.many2one('account.period', "Period", required=True, readonly=True),
545
'wizard_id': fields.integer("Wizard", required=True, readonly=True),
549
'description': lambda *a: '',
550
'ref': lambda *a: '',
551
'document_date': lambda *a: strftime('%Y-%m-%d'),
552
'date': lambda *a: strftime('%Y-%m-%d'),
553
'debit': lambda *a: 0.0,
554
'credit': lambda *a: 0.0,
557
msf_doc_import_accounting_lines()
559
class msf_doc_import_accounting_errors(osv.osv_memory):
560
_name = 'msf.doc.import.accounting.errors'
563
'name': fields.text("Description", readonly=True, required=True),
564
'wizard_id': fields.many2one('msf.doc.import.accounting', "Wizard", required=True, readonly=True),
567
msf_doc_import_accounting_errors()
568
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: