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.

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