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.

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