You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
17 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Author Joel Grand-Guillaume and Vincent Renaville Copyright 2013
  5. # Camptocamp SA
  6. # CSV data formating inspired from
  7. # http://docs.python.org/2.7/library/csv.html?highlight=csv#examples
  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. import itertools
  24. import tempfile
  25. import StringIO
  26. import cStringIO
  27. import base64
  28. import csv
  29. import codecs
  30. from openerp.osv import orm, fields
  31. from openerp.tools.translate import _
  32. class AccountUnicodeWriter(object):
  33. """
  34. A CSV writer which will write rows to CSV file "f",
  35. which is encoded in the given encoding.
  36. """
  37. def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
  38. # Redirect output to a queue
  39. self.queue = cStringIO.StringIO()
  40. # created a writer with Excel formating settings
  41. self.writer = csv.writer(self.queue, dialect=dialect,
  42. quoting=csv.QUOTE_ALL, **kwds)
  43. self.stream = f
  44. self.encoder = codecs.getincrementalencoder(encoding)()
  45. def writerow(self, row):
  46. # we ensure that we do not try to encode none or bool
  47. row = (x or u'' for x in row)
  48. encoded_row = [
  49. c.encode("utf-8") if isinstance(c, unicode) else c for c in row]
  50. self.writer.writerow(encoded_row)
  51. # Fetch UTF-8 output from the queue ...
  52. data = self.queue.getvalue()
  53. data = data.decode("utf-8")
  54. # ... and reencode it into the target encoding
  55. data = self.encoder.encode(data)
  56. # write to the target stream
  57. self.stream.write(data)
  58. # empty queue
  59. self.queue.truncate(0)
  60. def writerows(self, rows):
  61. for row in rows:
  62. self.writerow(row)
  63. class AccountCSVExport(orm.TransientModel):
  64. _name = 'account.csv.export'
  65. _description = 'Export Accounting'
  66. _columns = {
  67. 'data': fields.binary('CSV', readonly=True),
  68. 'company_id': fields.many2one('res.company', 'Company',
  69. invisible=True),
  70. 'fiscalyear_id': fields.many2one('account.fiscalyear', 'Fiscalyear',
  71. required=True),
  72. 'periods': fields.many2many(
  73. 'account.period', 'rel_wizard_period',
  74. 'wizard_id', 'period_id', 'Periods',
  75. help='All periods in the fiscal year if empty'),
  76. 'journal_ids': fields.many2many(
  77. 'account.journal',
  78. 'rel_wizard_journal',
  79. 'wizard_id',
  80. 'journal_id',
  81. 'Journals',
  82. help='If empty, use all journals, only used for journal entries'),
  83. 'account_ids': fields.many2many(
  84. 'account.account',
  85. 'rel_wizard_account',
  86. 'wizard_id',
  87. 'account_id',
  88. 'Accounts',
  89. help='If empty, use all accounts, only used for journal entries'),
  90. 'export_filename': fields.char('Export CSV Filename', size=128),
  91. }
  92. def _get_company_default(self, cr, uid, context=None):
  93. comp_obj = self.pool['res.company']
  94. return comp_obj._company_default_get(cr, uid, 'account.fiscalyear',
  95. context=context)
  96. def _get_fiscalyear_default(self, cr, uid, context=None):
  97. fiscalyear_obj = self.pool['account.fiscalyear']
  98. context['company_id'] = self._get_company_default(cr, uid, context)
  99. return fiscalyear_obj.find(cr, uid, dt=None, exception=True,
  100. context=context)
  101. _defaults = {'company_id': _get_company_default,
  102. 'fiscalyear_id': _get_fiscalyear_default,
  103. 'export_filename': 'account_export.csv'}
  104. def action_manual_export_account(self, cr, uid, ids, context=None):
  105. this = self.browse(cr, uid, ids)[0]
  106. rows = self.get_data(cr, uid, ids, "account", context)
  107. file_data = StringIO.StringIO()
  108. try:
  109. writer = AccountUnicodeWriter(file_data)
  110. writer.writerows(rows)
  111. file_value = file_data.getvalue()
  112. self.write(cr, uid, ids,
  113. {'data': base64.encodestring(file_value)},
  114. context=context)
  115. finally:
  116. file_data.close()
  117. return {
  118. 'type': 'ir.actions.act_window',
  119. 'res_model': 'account.csv.export',
  120. 'view_mode': 'form',
  121. 'view_type': 'form',
  122. 'res_id': this.id,
  123. 'views': [(False, 'form')],
  124. 'target': 'new',
  125. }
  126. def _get_header_account(self, cr, uid, ids, context=None):
  127. return [_(u'CODE'),
  128. _(u'NAME'),
  129. _(u'DEBIT'),
  130. _(u'CREDIT'),
  131. _(u'BALANCE'),
  132. ]
  133. def _get_rows_account(self, cr, uid, ids,
  134. fiscalyear_id,
  135. period_range_ids,
  136. journal_ids,
  137. account_ids,
  138. context=None):
  139. """
  140. Return list to generate rows of the CSV file
  141. """
  142. cr.execute("""
  143. select ac.code,ac.name,
  144. sum(debit) as sum_debit,
  145. sum(credit) as sum_credit,
  146. sum(debit) - sum(credit) as balance
  147. from account_move_line as aml,account_account as ac
  148. where aml.account_id = ac.id
  149. and period_id in %(period_ids)s
  150. and journal_id in %(journal_ids)s
  151. group by ac.id,ac.code,ac.name
  152. order by ac.code
  153. """,
  154. {'fiscalyear_id': fiscalyear_id,
  155. 'period_ids': tuple(period_range_ids),
  156. 'journal_ids': tuple(journal_ids)}
  157. )
  158. res = cr.fetchall()
  159. rows = []
  160. for line in res:
  161. rows.append(list(line))
  162. return rows
  163. def action_manual_export_analytic(self, cr, uid, ids, context=None):
  164. this = self.browse(cr, uid, ids)[0]
  165. rows = self.get_data(cr, uid, ids, "analytic", context)
  166. file_data = StringIO.StringIO()
  167. try:
  168. writer = AccountUnicodeWriter(file_data)
  169. writer.writerows(rows)
  170. file_value = file_data.getvalue()
  171. self.write(cr, uid, ids,
  172. {'data': base64.encodestring(file_value)},
  173. context=context)
  174. finally:
  175. file_data.close()
  176. return {
  177. 'type': 'ir.actions.act_window',
  178. 'res_model': 'account.csv.export',
  179. 'view_mode': 'form',
  180. 'view_type': 'form',
  181. 'res_id': this.id,
  182. 'views': [(False, 'form')],
  183. 'target': 'new',
  184. }
  185. def _get_header_analytic(self, cr, uid, ids, context=None):
  186. return [_(u'ANALYTIC CODE'),
  187. _(u'ANALYTIC NAME'),
  188. _(u'CODE'),
  189. _(u'ACCOUNT NAME'),
  190. _(u'DEBIT'),
  191. _(u'CREDIT'),
  192. _(u'BALANCE'),
  193. ]
  194. def _get_rows_analytic(self, cr, uid, ids,
  195. fiscalyear_id,
  196. period_range_ids,
  197. journal_ids,
  198. account_ids,
  199. context=None):
  200. """
  201. Return list to generate rows of the CSV file
  202. """
  203. cr.execute(""" select aac.code as analytic_code,
  204. aac.name as analytic_name,
  205. ac.code,ac.name,
  206. sum(debit) as sum_debit,
  207. sum(credit) as sum_credit,
  208. sum(debit) - sum(credit) as balance
  209. from account_move_line
  210. left outer join account_analytic_account as aac
  211. on (account_move_line.analytic_account_id = aac.id)
  212. inner join account_account as ac
  213. on account_move_line.account_id = ac.id
  214. and account_move_line.period_id in %(period_ids)s
  215. group by aac.id,aac.code,aac.name,ac.id,ac.code,ac.name
  216. order by aac.code
  217. """,
  218. {'fiscalyear_id': fiscalyear_id,
  219. 'period_ids': tuple(period_range_ids)}
  220. )
  221. res = cr.fetchall()
  222. rows = []
  223. for line in res:
  224. rows.append(list(line))
  225. return rows
  226. def action_manual_export_journal_entries(self, cr, uid, ids, context=None):
  227. """
  228. Here we use TemporaryFile to avoid full filling the OpenERP worker
  229. Memory
  230. We also write the data to the wizard with SQL query as write seams
  231. to use too much memory as well.
  232. Those improvements permitted to improve the export from a 100k line to
  233. 200k lines
  234. with default `limit_memory_hard = 805306368` (768MB) with more lines,
  235. you might encounter a MemoryError when trying to download the file even
  236. if it has been generated.
  237. To be able to export bigger volume of data, it is advised to set
  238. limit_memory_hard to 2097152000 (2 GB) to generate the file and let
  239. OpenERP load it in the wizard when trying to download it.
  240. Tested with up to a generation of 700k entry lines
  241. """
  242. this = self.browse(cr, uid, ids)[0]
  243. rows = self.get_data(cr, uid, ids, "journal_entries", context)
  244. with tempfile.TemporaryFile() as file_data:
  245. writer = AccountUnicodeWriter(file_data)
  246. writer.writerows(rows)
  247. with tempfile.TemporaryFile() as base64_data:
  248. file_data.seek(0)
  249. base64.encode(file_data, base64_data)
  250. base64_data.seek(0)
  251. cr.execute("""
  252. UPDATE account_csv_export
  253. SET data = %s
  254. WHERE id = %s""", (base64_data.read(), ids[0]))
  255. return {
  256. 'type': 'ir.actions.act_window',
  257. 'res_model': 'account.csv.export',
  258. 'view_mode': 'form',
  259. 'view_type': 'form',
  260. 'res_id': this.id,
  261. 'views': [(False, 'form')],
  262. 'target': 'new',
  263. }
  264. def _get_header_journal_entries(self, cr, uid, ids, context=None):
  265. return [
  266. # Standard Sage export fields
  267. _(u'DATE'),
  268. _(u'JOURNAL CODE'),
  269. _(u'ACCOUNT CODE'),
  270. _(u'PARTNER NAME'),
  271. _(u'REF'),
  272. _(u'DESCRIPTION'),
  273. _(u'DEBIT'),
  274. _(u'CREDIT'),
  275. _(u'FULL RECONCILE'),
  276. _(u'PARTIAL RECONCILE'),
  277. _(u'ANALYTIC ACCOUNT CODE'),
  278. # Other fields
  279. _(u'ENTRY NUMBER'),
  280. _(u'ACCOUNT NAME'),
  281. _(u'BALANCE'),
  282. _(u'AMOUNT CURRENCY'),
  283. _(u'CURRENCY'),
  284. _(u'ANALYTIC ACCOUNT NAME'),
  285. _(u'JOURNAL'),
  286. _(u'MONTH'),
  287. _(u'FISCAL YEAR'),
  288. _(u'TAX CODE CODE'),
  289. _(u'TAX CODE NAME'),
  290. _(u'TAX AMOUNT'),
  291. _(u'BANK STATEMENT'),
  292. ]
  293. def _get_rows_journal_entries(self, cr, uid, ids,
  294. fiscalyear_id,
  295. period_range_ids,
  296. journal_ids,
  297. account_ids,
  298. context=None):
  299. """
  300. Create a generator of rows of the CSV file
  301. """
  302. cr.execute("""
  303. SELECT
  304. account_move_line.date AS date,
  305. account_journal.name as journal,
  306. account_account.code AS account_code,
  307. res_partner.name AS partner_name,
  308. account_move_line.ref AS ref,
  309. account_move_line.name AS description,
  310. account_move_line.debit AS debit,
  311. account_move_line.credit AS credit,
  312. account_move_reconcile.name as full_reconcile,
  313. account_move_line.reconcile_partial_id AS partial_reconcile_id,
  314. account_analytic_account.code AS analytic_account_code,
  315. account_move.name AS entry_number,
  316. account_account.name AS account_name,
  317. account_move_line.debit - account_move_line.credit AS balance,
  318. account_move_line.amount_currency AS amount_currency,
  319. res_currency.name AS currency,
  320. account_analytic_account.name AS analytic_account_name,
  321. account_journal.name as journal,
  322. account_period.code AS month,
  323. account_fiscalyear.name as fiscal_year,
  324. account_tax_code.code AS aml_tax_code_code,
  325. account_tax_code.name AS aml_tax_code_name,
  326. account_move_line.tax_amount AS aml_tax_amount,
  327. account_bank_statement.name AS bank_statement
  328. FROM
  329. public.account_move_line
  330. JOIN account_account on
  331. (account_account.id=account_move_line.account_id)
  332. JOIN account_period on
  333. (account_period.id=account_move_line.period_id)
  334. JOIN account_fiscalyear on
  335. (account_fiscalyear.id=account_period.fiscalyear_id)
  336. JOIN account_journal on
  337. (account_journal.id = account_move_line.journal_id)
  338. LEFT JOIN res_currency on
  339. (res_currency.id=account_move_line.currency_id)
  340. LEFT JOIN account_move_reconcile on
  341. (account_move_reconcile.id = account_move_line.reconcile_id)
  342. LEFT JOIN res_partner on
  343. (res_partner.id=account_move_line.partner_id)
  344. LEFT JOIN account_move on
  345. (account_move.id=account_move_line.move_id)
  346. LEFT JOIN account_tax on
  347. (account_tax.id=account_move_line.account_tax_id)
  348. LEFT JOIN account_tax_code on
  349. (account_tax_code.id=account_move_line.tax_code_id)
  350. LEFT JOIN account_analytic_account on
  351. (account_analytic_account.id=account_move_line.analytic_account_id)
  352. LEFT JOIN account_bank_statement on
  353. (account_bank_statement.id=account_move_line.statement_id)
  354. WHERE account_period.id IN %(period_ids)s
  355. AND account_journal.id IN %(journal_ids)s
  356. AND account_account.id IN %(account_ids)s
  357. ORDER BY account_move_line.date
  358. """,
  359. {'period_ids': tuple(period_range_ids),
  360. 'journal_ids': tuple(journal_ids),
  361. 'account_ids': tuple(account_ids)}
  362. )
  363. while 1:
  364. # http://initd.org/psycopg/docs/cursor.html#cursor.fetchmany
  365. # Set cursor.arraysize to minimize network round trips
  366. cr.arraysize = 100
  367. rows = cr.fetchmany()
  368. if not rows:
  369. break
  370. for row in rows:
  371. yield row
  372. def get_data(self, cr, uid, ids, result_type, context=None):
  373. get_header_func = getattr(
  374. self, ("_get_header_%s" % (result_type)), None)
  375. get_rows_func = getattr(self, ("_get_rows_%s" % (result_type)), None)
  376. form = self.browse(cr, uid, ids[0], context=context)
  377. fiscalyear_id = form.fiscalyear_id.id
  378. if form.periods:
  379. period_range_ids = [x.id for x in form.periods]
  380. else:
  381. # If not period selected , we take all periods
  382. p_obj = self.pool.get("account.period")
  383. period_range_ids = p_obj.search(
  384. cr, uid, [('fiscalyear_id', '=', fiscalyear_id)],
  385. context=context)
  386. journal_ids = None
  387. if form.journal_ids:
  388. journal_ids = [x.id for x in form.journal_ids]
  389. else:
  390. j_obj = self.pool.get("account.journal")
  391. journal_ids = j_obj.search(cr, uid, [], context=context)
  392. account_ids = None
  393. if form.account_ids:
  394. account_ids = [x.id for x in form.account_ids]
  395. else:
  396. aa_obj = self.pool.get("account.account")
  397. account_ids = aa_obj.search(cr, uid, [], context=context)
  398. rows = itertools.chain((get_header_func(cr, uid, ids,
  399. context=context),),
  400. get_rows_func(cr, uid, ids,
  401. fiscalyear_id,
  402. period_range_ids,
  403. journal_ids,
  404. account_ids,
  405. context=context)
  406. )
  407. return rows