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.

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