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.

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