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.

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