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.

618 lines
26 KiB

  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Author: Nicolas Bessi, Guewen Baconnier
  5. # Copyright Camptocamp SA 2011
  6. # SQL inspired from OpenERP original code
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as
  10. # published by the Free Software Foundation, either version 3 of the
  11. # License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ##############################################################################
  22. # TODO refactor helper in order to act more like mixin
  23. # By using properties we will have a more simple signature in fuctions
  24. import logging
  25. from openerp.osv import osv
  26. from openerp.tools.translate import _
  27. from openerp.addons.account.report.common_report_header \
  28. import common_report_header
  29. _logger = logging.getLogger('financial.reports.webkit')
  30. MAX_MONSTER_SLICE = 50000
  31. class CommonReportHeaderWebkit(common_report_header):
  32. """Define common helper for financial report"""
  33. ######################################################################
  34. # From getter helper #
  35. ######################################################################
  36. def get_start_period_br(self, data):
  37. return self._get_info(data, 'period_from', 'account.period')
  38. def get_end_period_br(self, data):
  39. return self._get_info(data, 'period_to', 'account.period')
  40. def get_fiscalyear_br(self, data):
  41. return self._get_info(data, 'fiscalyear_id', 'account.fiscalyear')
  42. def _get_chart_account_id_br(self, data):
  43. return self._get_info(data, 'chart_account_id', 'account.account')
  44. def _get_accounts_br(self, data):
  45. return self._get_info(data, 'account_ids', 'account.account')
  46. def _get_info(self, data, field, model):
  47. info = data.get('form', {}).get(field)
  48. if info:
  49. return self.pool.get(model).browse(self.cursor, self.uid, info)
  50. return False
  51. def _get_journals_br(self, data):
  52. return self._get_info(data, 'journal_ids', 'account.journal')
  53. def _get_display_account(self, data):
  54. val = self._get_form_param('display_account', data)
  55. if val == 'bal_all':
  56. return _('All accounts')
  57. elif val == 'bal_mix':
  58. return _('With transactions or non zero balance')
  59. else:
  60. return val
  61. def _get_display_partner_account(self, data):
  62. val = self._get_form_param('result_selection', data)
  63. if val == 'customer':
  64. return _('Receivable Accounts')
  65. elif val == 'supplier':
  66. return _('Payable Accounts')
  67. elif val == 'customer_supplier':
  68. return _('Receivable and Payable Accounts')
  69. else:
  70. return val
  71. def _get_display_target_move(self, data):
  72. val = self._get_form_param('target_move', data)
  73. if val == 'posted':
  74. return _('All Posted Entries')
  75. elif val == 'all':
  76. return _('All Entries')
  77. else:
  78. return val
  79. def _get_display_account_raw(self, data):
  80. return self._get_form_param('display_account', data)
  81. def _get_filter(self, data):
  82. return self._get_form_param('filter', data)
  83. def _get_target_move(self, data):
  84. return self._get_form_param('target_move', data)
  85. def _get_initial_balance(self, data):
  86. return self._get_form_param('initial_balance', data)
  87. def _get_amount_currency(self, data):
  88. return self._get_form_param('amount_currency', data)
  89. def _get_date_from(self, data):
  90. return self._get_form_param('date_from', data)
  91. def _get_date_to(self, data):
  92. return self._get_form_param('date_to', data)
  93. def _get_form_param(self, param, data, default=False):
  94. return data.get('form', {}).get(param, default)
  95. #############################################
  96. # Account and account line filter helper #
  97. #############################################
  98. def sort_accounts_with_structure(self, root_account_ids, account_ids,
  99. context=None):
  100. """Sort accounts by code respecting their structure"""
  101. def recursive_sort_by_code(accounts, parent):
  102. sorted_accounts = []
  103. # add all accounts with same parent
  104. level_accounts = [account for account in accounts
  105. if account['parent_id']
  106. and account['parent_id'][0] == parent['id']]
  107. # add consolidation children of parent, as they are logically on
  108. # the same level
  109. if parent.get('child_consol_ids'):
  110. level_accounts.extend([account for account in accounts
  111. if account['id']
  112. in parent['child_consol_ids']])
  113. # stop recursion if no children found
  114. if not level_accounts:
  115. return []
  116. level_accounts = sorted(level_accounts, key=lambda a: a['code'])
  117. for level_account in level_accounts:
  118. sorted_accounts.append(level_account['id'])
  119. sorted_accounts.extend(
  120. recursive_sort_by_code(accounts, parent=level_account))
  121. return sorted_accounts
  122. if not account_ids:
  123. return []
  124. accounts_data = self.pool.get('account.account').read(
  125. self.cr, self.uid, account_ids,
  126. ['id', 'parent_id', 'level', 'code', 'child_consol_ids'],
  127. context=context)
  128. sorted_accounts = []
  129. root_accounts_data = [account_data for account_data in accounts_data
  130. if account_data['id'] in root_account_ids]
  131. for root_account_data in root_accounts_data:
  132. sorted_accounts.append(root_account_data['id'])
  133. sorted_accounts.extend(
  134. recursive_sort_by_code(accounts_data, root_account_data))
  135. # fallback to unsorted accounts when sort failed
  136. # sort fails when the levels are miscalculated by account.account
  137. # check lp:783670
  138. if len(sorted_accounts) != len(account_ids):
  139. _logger.warn('Webkit financial reports: Sort of accounts failed.')
  140. sorted_accounts = account_ids
  141. return sorted_accounts
  142. def get_all_accounts(self, account_ids, exclude_type=None, only_type=None,
  143. filter_report_type=None, context=None):
  144. """Get all account passed in params with their childrens
  145. @param exclude_type: list of types to exclude (view, receivable,
  146. payable, consolidation, other)
  147. @param only_type: list of types to filter on (view, receivable,
  148. payable, consolidation, other)
  149. @param filter_report_type: list of report type to filter on
  150. """
  151. context = context or {}
  152. accounts = []
  153. if not isinstance(account_ids, list):
  154. account_ids = [account_ids]
  155. acc_obj = self.pool.get('account.account')
  156. for account_id in account_ids:
  157. accounts.append(account_id)
  158. accounts += acc_obj._get_children_and_consol(
  159. self.cursor, self.uid, account_id, context=context)
  160. res_ids = list(set(accounts))
  161. res_ids = self.sort_accounts_with_structure(
  162. account_ids, res_ids, context=context)
  163. if exclude_type or only_type or filter_report_type:
  164. sql_filters = {'ids': tuple(res_ids)}
  165. sql_select = "SELECT a.id FROM account_account a"
  166. sql_join = ""
  167. sql_where = "WHERE a.id IN %(ids)s"
  168. if exclude_type:
  169. sql_where += " AND a.type not in %(exclude_type)s"
  170. sql_filters.update({'exclude_type': tuple(exclude_type)})
  171. if only_type:
  172. sql_where += " AND a.type IN %(only_type)s"
  173. sql_filters.update({'only_type': tuple(only_type)})
  174. if filter_report_type:
  175. sql_join += "INNER JOIN account_account_type t" \
  176. " ON t.id = a.user_type"
  177. sql_join += " AND t.report_type IN %(report_type)s"
  178. sql_filters.update({'report_type': tuple(filter_report_type)})
  179. sql = ' '.join((sql_select, sql_join, sql_where))
  180. self.cursor.execute(sql, sql_filters)
  181. fetch_only_ids = self.cursor.fetchall()
  182. if not fetch_only_ids:
  183. return []
  184. only_ids = [only_id[0] for only_id in fetch_only_ids]
  185. # keep sorting but filter ids
  186. res_ids = [res_id for res_id in res_ids if res_id in only_ids]
  187. return res_ids
  188. ##########################################
  189. # Periods and fiscal years helper #
  190. ##########################################
  191. def _get_opening_periods(self):
  192. """Return the list of all journal that can be use to create opening
  193. entries.
  194. We actually filter on this instead of opening period as older version
  195. of OpenERP did not have this notion"""
  196. return self.pool.get('account.period').search(self.cursor, self.uid,
  197. [('special', '=', True)])
  198. def exclude_opening_periods(self, period_ids):
  199. period_obj = self.pool.get('account.period')
  200. return period_obj.search(self.cr, self.uid, [['special', '=', False],
  201. ['id', 'in', period_ids]])
  202. def get_included_opening_period(self, period):
  203. """Return the opening included in normal period we use the assumption
  204. that there is only one opening period per fiscal year"""
  205. period_obj = self.pool.get('account.period')
  206. return period_obj.search(self.cursor, self.uid,
  207. [('special', '=', True),
  208. ('date_start', '>=', period.date_start),
  209. ('date_stop', '<=', period.date_stop),
  210. ('company_id', '=', period.company_id.id)],
  211. limit=1)
  212. def periods_contains_move_lines(self, period_ids):
  213. if not period_ids:
  214. return False
  215. mv_line_obj = self.pool.get('account.move.line')
  216. if isinstance(period_ids, (int, long)):
  217. period_ids = [period_ids]
  218. return mv_line_obj.search(self.cursor, self.uid,
  219. [('period_id', 'in', period_ids)], limit=1) \
  220. and True or False
  221. def _get_period_range_from_periods(self, start_period, stop_period,
  222. mode=None):
  223. """
  224. Deprecated. We have to use now the build_ctx_periods of period_obj
  225. otherwise we'll have inconsistencies, because build_ctx_periods does
  226. never filter on the the special
  227. """
  228. period_obj = self.pool.get('account.period')
  229. search_period = [('date_start', '>=', start_period.date_start),
  230. ('date_stop', '<=', stop_period.date_stop)]
  231. if mode == 'exclude_opening':
  232. search_period += [('special', '=', False)]
  233. res = period_obj.search(self.cursor, self.uid, search_period)
  234. return res
  235. def _get_period_range_from_start_period(self, start_period,
  236. include_opening=False,
  237. fiscalyear=False,
  238. stop_at_previous_opening=False):
  239. """We retrieve all periods before start period"""
  240. opening_period_id = False
  241. past_limit = []
  242. period_obj = self.pool.get('account.period')
  243. mv_line_obj = self.pool.get('account.move.line')
  244. # We look for previous opening period
  245. if stop_at_previous_opening:
  246. opening_search = [('special', '=', True),
  247. ('date_stop', '<', start_period.date_start)]
  248. if fiscalyear:
  249. opening_search.append(('fiscalyear_id', '=', fiscalyear.id))
  250. opening_periods = period_obj.search(self.cursor, self.uid,
  251. opening_search,
  252. order='date_stop desc')
  253. for opening_period in opening_periods:
  254. validation_res = mv_line_obj.search(self.cursor,
  255. self.uid,
  256. [('period_id', '=',
  257. opening_period)],
  258. limit=1)
  259. if validation_res:
  260. opening_period_id = opening_period
  261. break
  262. if opening_period_id:
  263. # we also look for overlapping periods
  264. opening_period_br = period_obj.browse(
  265. self.cursor, self.uid, opening_period_id)
  266. past_limit = [
  267. ('date_start', '>=', opening_period_br.date_stop)]
  268. periods_search = [('date_stop', '<=', start_period.date_stop)]
  269. periods_search += past_limit
  270. if not include_opening:
  271. periods_search += [('special', '=', False)]
  272. if fiscalyear:
  273. periods_search.append(('fiscalyear_id', '=', fiscalyear.id))
  274. periods = period_obj.search(self.cursor, self.uid, periods_search)
  275. if include_opening and opening_period_id:
  276. periods.append(opening_period_id)
  277. periods = list(set(periods))
  278. if start_period.id in periods:
  279. periods.remove(start_period.id)
  280. return periods
  281. def get_first_fiscalyear_period(self, fiscalyear):
  282. return self._get_st_fiscalyear_period(fiscalyear)
  283. def get_last_fiscalyear_period(self, fiscalyear):
  284. return self._get_st_fiscalyear_period(fiscalyear, order='DESC')
  285. def _get_st_fiscalyear_period(self, fiscalyear, special=False,
  286. order='ASC'):
  287. period_obj = self.pool.get('account.period')
  288. p_id = period_obj.search(self.cursor,
  289. self.uid,
  290. [('special', '=', special),
  291. ('fiscalyear_id', '=', fiscalyear.id)],
  292. limit=1,
  293. order='date_start %s' % (order,))
  294. if not p_id:
  295. raise osv.except_osv(_('No period found'), '')
  296. return period_obj.browse(self.cursor, self.uid, p_id[0])
  297. ###############################
  298. # Initial Balance helper #
  299. ###############################
  300. def _compute_init_balance(self, account_id=None, period_ids=None,
  301. mode='computed', default_values=False):
  302. if not isinstance(period_ids, list):
  303. period_ids = [period_ids]
  304. res = {}
  305. if not default_values:
  306. if not account_id or not period_ids:
  307. raise Exception('Missing account or period_ids')
  308. try:
  309. self.cursor.execute("SELECT sum(debit) AS debit, "
  310. " sum(credit) AS credit, "
  311. " sum(debit)-sum(credit) AS balance, "
  312. " sum(amount_currency) AS curr_balance"
  313. " FROM account_move_line"
  314. " WHERE period_id in %s"
  315. " AND account_id = %s",
  316. (tuple(period_ids), account_id))
  317. res = self.cursor.dictfetchone()
  318. except Exception:
  319. self.cursor.rollback()
  320. raise
  321. return {'debit': res.get('debit') or 0.0,
  322. 'credit': res.get('credit') or 0.0,
  323. 'init_balance': res.get('balance') or 0.0,
  324. 'init_balance_currency': res.get('curr_balance') or 0.0,
  325. 'state': mode}
  326. def _read_opening_balance(self, account_ids, start_period):
  327. """ Read opening balances from the opening balance
  328. """
  329. opening_period_selected = self.get_included_opening_period(
  330. start_period)
  331. if not opening_period_selected:
  332. raise osv.except_osv(
  333. _('Error'),
  334. _('No opening period found to compute the opening balances.\n'
  335. 'You have to configure a period on the first of January'
  336. ' with the special flag.'))
  337. res = {}
  338. for account_id in account_ids:
  339. res[account_id] = self._compute_init_balance(
  340. account_id, opening_period_selected, mode='read')
  341. return res
  342. def _compute_initial_balances(self, account_ids, start_period, fiscalyear):
  343. """We compute initial balance.
  344. If form is filtered by date all initial balance are equal to 0
  345. This function will sum pear and apple in currency amount if account as
  346. no secondary currency"""
  347. # if opening period is included in start period we do not need to
  348. # compute init balance we just read it from opening entries
  349. res = {}
  350. # PNL and Balance accounts are not computed the same way look for
  351. # attached doc We include opening period in pnl account in order to see
  352. # if opening entries were created by error on this account
  353. pnl_periods_ids = self._get_period_range_from_start_period(
  354. start_period, fiscalyear=fiscalyear, include_opening=True)
  355. bs_period_ids = self._get_period_range_from_start_period(
  356. start_period, include_opening=True, stop_at_previous_opening=True)
  357. opening_period_selected = self.get_included_opening_period(
  358. start_period)
  359. for acc in self.pool.get('account.account').browse(self.cursor,
  360. self.uid,
  361. account_ids):
  362. res[acc.id] = self._compute_init_balance(default_values=True)
  363. if acc.user_type.close_method == 'none':
  364. # we compute the initial balance for close_method == none only
  365. # when we print a GL during the year, when the opening period
  366. # is not included in the period selection!
  367. if pnl_periods_ids and not opening_period_selected:
  368. res[acc.id] = self._compute_init_balance(
  369. acc.id, pnl_periods_ids)
  370. else:
  371. res[acc.id] = self._compute_init_balance(acc.id, bs_period_ids)
  372. return res
  373. ################################################
  374. # Account move retrieval helper #
  375. ################################################
  376. def _get_move_ids_from_periods(self, account_id, period_start, period_stop,
  377. target_move):
  378. move_line_obj = self.pool.get('account.move.line')
  379. period_obj = self.pool.get('account.period')
  380. periods = period_obj.build_ctx_periods(
  381. self.cursor, self.uid, period_start.id, period_stop.id)
  382. if not periods:
  383. return []
  384. search = [
  385. ('period_id', 'in', periods), ('account_id', '=', account_id)]
  386. if target_move == 'posted':
  387. search += [('move_id.state', '=', 'posted')]
  388. return move_line_obj.search(self.cursor, self.uid, search)
  389. def _get_move_ids_from_dates(self, account_id, date_start, date_stop,
  390. target_move, mode='include_opening'):
  391. # TODO imporve perfomance by setting opening period as a property
  392. move_line_obj = self.pool.get('account.move.line')
  393. search_period = [('date', '>=', date_start),
  394. ('date', '<=', date_stop),
  395. ('account_id', '=', account_id)]
  396. # actually not used because OpenERP itself always include the opening
  397. # when we get the periods from january to december
  398. if mode == 'exclude_opening':
  399. opening = self._get_opening_periods()
  400. if opening:
  401. search_period += ['period_id', 'not in', opening]
  402. if target_move == 'posted':
  403. search_period += [('move_id.state', '=', 'posted')]
  404. return move_line_obj.search(self.cursor, self.uid, search_period)
  405. def get_move_lines_ids(self, account_id, main_filter, start, stop,
  406. target_move, mode='include_opening'):
  407. """Get account move lines base on form data"""
  408. if mode not in ('include_opening', 'exclude_opening'):
  409. raise osv.except_osv(
  410. _('Invalid query mode'),
  411. _('Must be in include_opening, exclude_opening'))
  412. if main_filter in ('filter_period', 'filter_no'):
  413. return self._get_move_ids_from_periods(account_id, start, stop,
  414. target_move)
  415. elif main_filter == 'filter_date':
  416. return self._get_move_ids_from_dates(account_id, start, stop,
  417. target_move)
  418. else:
  419. raise osv.except_osv(
  420. _('No valid filter'), _('Please set a valid time filter'))
  421. def _get_move_line_select(self):
  422. '''
  423. Get the columns to put in the SQL SELECT for _get_move_line_datas
  424. See _get_move_line_datas for available tables and aliases
  425. '''
  426. return """
  427. l.id AS id,
  428. l.date AS ldate,
  429. j.code AS jcode ,
  430. j.type AS jtype,
  431. l.currency_id,
  432. l.account_id,
  433. l.amount_currency,
  434. l.ref AS lref,
  435. l.name AS lname,
  436. COALESCE(l.debit, 0.0) - COALESCE(l.credit, 0.0) AS balance,
  437. l.debit,
  438. l.credit,
  439. l.period_id AS lperiod_id,
  440. per.code as period_code,
  441. per.special AS peropen,
  442. l.partner_id AS lpartner_id,
  443. p.name AS partner_name,
  444. m.name AS move_name,
  445. COALESCE(partialrec.name, fullrec.name, '') AS rec_name,
  446. COALESCE(partialrec.id, fullrec.id, NULL) AS rec_id,
  447. m.id AS move_id,
  448. c.name AS currency_code,
  449. i.id AS invoice_id,
  450. i.type AS invoice_type,
  451. i.number AS invoice_number,
  452. l.date_maturity
  453. """
  454. def _get_move_line_order(self):
  455. ''' Get the default SQL ORDER statement for _get_move_line_datas '''
  456. return 'per.special DESC, l.date ASC, per.date_start ASC, m.name ASC'
  457. def _get_move_line_datas(self, move_line_ids, order=None):
  458. if order is None:
  459. order = self._get_move_line_order()
  460. # Possible bang if move_line_ids is too long
  461. # We can not slice here as we have to do the sort.
  462. # If slice has to be done it means that we have to reorder in python
  463. # after all is finished. That quite crapy...
  464. # We have a defective desing here (mea culpa) that should be fixed
  465. #
  466. # TODO improve that by making a better domain or if not possible
  467. # by using python sort
  468. if not move_line_ids:
  469. return []
  470. if not isinstance(move_line_ids, list):
  471. move_line_ids = [move_line_ids]
  472. monster = """
  473. SELECT {select}
  474. FROM account_move_line l
  475. JOIN account_move m on (l.move_id=m.id)
  476. LEFT JOIN res_currency c on (l.currency_id=c.id)
  477. LEFT JOIN account_move_reconcile partialrec
  478. ON (l.reconcile_partial_id = partialrec.id)
  479. LEFT JOIN account_move_reconcile fullrec
  480. ON (l.reconcile_id = fullrec.id)
  481. LEFT JOIN res_partner p on (l.partner_id=p.id)
  482. LEFT JOIN account_invoice i on (m.id =i.move_id)
  483. LEFT JOIN account_period per on (per.id=l.period_id)
  484. JOIN account_journal j on (l.journal_id=j.id)
  485. WHERE l.id in %s
  486. ORDER BY {order}
  487. """.format(
  488. select=self._get_move_line_select(),
  489. order=order,
  490. )
  491. try:
  492. self.cursor.execute(monster, (tuple(move_line_ids),))
  493. res = self.cursor.dictfetchall()
  494. except Exception:
  495. self.cursor.rollback()
  496. raise
  497. return res or []
  498. def _get_moves_counterparts(self, move_ids, account_id, limit=3):
  499. if not move_ids:
  500. return {}
  501. if not isinstance(move_ids, list):
  502. move_ids = [move_ids]
  503. sql = """
  504. SELECT account_move.id,
  505. array_to_string(
  506. ARRAY(SELECT DISTINCT a.code
  507. FROM account_move_line m2
  508. LEFT JOIN account_account a ON (m2.account_id=a.id)
  509. WHERE m2.move_id =account_move_line.move_id
  510. AND m2.account_id<>%s limit %s) , ', ')
  511. FROM account_move
  512. JOIN account_move_line
  513. on (account_move_line.move_id = account_move.id)
  514. JOIN account_account
  515. on (account_move_line.account_id = account_account.id)
  516. WHERE move_id in %s"""
  517. try:
  518. self.cursor.execute(sql, (account_id, limit, tuple(move_ids)))
  519. res = self.cursor.fetchall()
  520. except Exception:
  521. self.cursor.rollback()
  522. raise
  523. return res and dict(res) or {}
  524. def is_initial_balance_enabled(self, main_filter):
  525. if main_filter not in ('filter_no', 'filter_year', 'filter_period'):
  526. return False
  527. return True
  528. def _get_initial_balance_mode(self, start_period):
  529. opening_period_selected = self.get_included_opening_period(
  530. start_period)
  531. opening_move_lines = self.periods_contains_move_lines(
  532. opening_period_selected)
  533. if opening_move_lines:
  534. return 'opening_balance'
  535. else:
  536. return 'initial_balance'