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.

378 lines
16 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. from collections import defaultdict
  25. from datetime import datetime
  26. from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
  27. from .common_reports import CommonReportHeaderWebkit
  28. class CommonPartnersReportHeaderWebkit(CommonReportHeaderWebkit):
  29. """Define common helper for partner oriented financial report"""
  30. ######################################
  31. # Account move line retrieval helper #
  32. ######################################
  33. def get_partners_move_lines_ids(self, account_id, main_filter, start, stop,
  34. target_move,
  35. exclude_reconcile=False,
  36. partner_filter=False):
  37. filter_from = False
  38. if main_filter in ('filter_period', 'filter_no'):
  39. filter_from = 'period'
  40. elif main_filter == 'filter_date':
  41. filter_from = 'date'
  42. if filter_from:
  43. return self._get_partners_move_line_ids(
  44. filter_from, account_id, start, stop, target_move,
  45. exclude_reconcile=exclude_reconcile,
  46. partner_filter=partner_filter)
  47. def _get_first_special_period(self):
  48. """
  49. Returns the browse record of the period with the `special` flag, which
  50. is the special period of the first fiscal year used in the accounting.
  51. i.e. it searches the first fiscal year with at least one journal entry,
  52. and it returns the id of the first period for which `special` is True
  53. in this fiscal year.
  54. It is used for example in the partners reports, where we have to
  55. include the first, and only the first opening period.
  56. :return: browse record of the first special period.
  57. """
  58. move_line_obj = self.pool.get('account.move.line')
  59. first_entry_id = move_line_obj.search(
  60. self.cr, self.uid, [], order='date ASC', limit=1)
  61. # it means there is no entry at all, that's unlikely to happen, but
  62. # it may so
  63. if not first_entry_id:
  64. return
  65. first_entry = move_line_obj.browse(
  66. self.cr, self.uid, first_entry_id[0])
  67. fiscalyear = first_entry.period_id.fiscalyear_id
  68. special_periods = [
  69. period for period in fiscalyear.period_ids if period.special]
  70. # so, we have no opening period on the first year, nothing to return
  71. if not special_periods:
  72. return
  73. return min(special_periods,
  74. key=lambda p: datetime.strptime(p.date_start,
  75. DEFAULT_SERVER_DATE_FORMAT))
  76. def _get_period_range_from_start_period(self, start_period,
  77. include_opening=False,
  78. fiscalyear=False,
  79. stop_at_previous_opening=False):
  80. """We retrieve all periods before start period"""
  81. periods = super(CommonPartnersReportHeaderWebkit, self).\
  82. _get_period_range_from_start_period(
  83. start_period,
  84. include_opening=include_opening,
  85. fiscalyear=fiscalyear,
  86. stop_at_previous_opening=stop_at_previous_opening)
  87. first_special = self._get_first_special_period()
  88. if first_special and first_special.id not in periods:
  89. periods.append(first_special.id)
  90. return periods
  91. def _get_query_params_from_periods(self, period_start, period_stop,
  92. mode='exclude_opening'):
  93. """
  94. Build the part of the sql "where clause" which filters on the selected
  95. periods.
  96. :param browse_record period_start: first period of the report to print
  97. :param browse_record period_stop: last period of the report to print
  98. :param str mode: deprecated
  99. """
  100. # we do not want opening period so we exclude opening
  101. periods = self.pool.get('account.period').build_ctx_periods(
  102. self.cr, self.uid, period_start.id, period_stop.id)
  103. if not periods:
  104. return []
  105. if mode != 'include_opening':
  106. periods = self.exclude_opening_periods(periods)
  107. search_params = {'period_ids': tuple(periods),
  108. 'date_stop': period_stop.date_stop}
  109. sql_conditions = ""
  110. if periods:
  111. sql_conditions = " AND account_move_line.period_id in \
  112. %(period_ids)s"
  113. return sql_conditions, search_params
  114. def _get_query_params_from_dates(self, date_start, date_stop, **args):
  115. """
  116. Build the part of the sql where clause based on the dates to print.
  117. :param str date_start: start date of the report to print
  118. :param str date_stop: end date of the report to print
  119. """
  120. periods = self._get_opening_periods()
  121. if not periods:
  122. periods = (-1,)
  123. search_params = {'period_ids': tuple(periods),
  124. 'date_start': date_start,
  125. 'date_stop': date_stop}
  126. sql_conditions = " AND account_move_line.period_id not \
  127. in %(period_ids)s \
  128. AND account_move_line.date between \
  129. date(%(date_start)s) and date((%(date_stop)s))"
  130. return sql_conditions, search_params
  131. def _get_partners_move_line_ids(self, filter_from, account_id, start, stop,
  132. target_move,
  133. opening_mode='exclude_opening',
  134. exclude_reconcile=False,
  135. partner_filter=None):
  136. """
  137. :param str filter_from: "periods" or "dates"
  138. :param int account_id: id of the account where to search move lines
  139. :param str or browse_record start: start date or start period
  140. :param str or browse_record stop: stop date or stop period
  141. :param str target_move: 'posted' or 'all'
  142. :param opening_mode: deprecated
  143. :param boolean exclude_reconcile: wether the reconciled entries are
  144. filtred or not
  145. :param list partner_filter: list of partner ids, will filter on their
  146. move lines
  147. """
  148. final_res = defaultdict(list)
  149. sql_select = "SELECT account_move_line.id, \
  150. account_move_line.partner_id FROM account_move_line"
  151. sql_joins = ''
  152. sql_where = " WHERE account_move_line.account_id = %(account_ids)s " \
  153. " AND account_move_line.state = 'valid' "
  154. method = getattr(self, '_get_query_params_from_' + filter_from + 's')
  155. sql_conditions, search_params = method(start, stop)
  156. sql_where += sql_conditions
  157. if exclude_reconcile:
  158. sql_where += (" AND ((account_move_line.reconcile_id IS NULL)"
  159. " OR (account_move_line.reconcile_id IS NOT NULL \
  160. AND account_move_line.last_rec_date > \
  161. date(%(date_stop)s)))")
  162. if partner_filter:
  163. sql_where += " AND account_move_line.partner_id \
  164. in %(partner_ids)s"
  165. if target_move == 'posted':
  166. sql_joins += "INNER JOIN account_move \
  167. ON account_move_line.move_id = account_move.id"
  168. sql_where += " AND account_move.state = %(target_move)s"
  169. search_params.update({'target_move': target_move})
  170. search_params.update({
  171. 'account_ids': account_id,
  172. 'partner_ids': tuple(partner_filter),
  173. })
  174. sql = ' '.join((sql_select, sql_joins, sql_where))
  175. self.cursor.execute(sql, search_params)
  176. res = self.cursor.dictfetchall()
  177. if res:
  178. for row in res:
  179. final_res[row['partner_id']].append(row['id'])
  180. return final_res
  181. def _get_clearance_move_line_ids(self, move_line_ids, date_stop,
  182. date_until):
  183. if not move_line_ids:
  184. return []
  185. move_line_obj = self.pool.get('account.move.line')
  186. # we do not use orm in order to gain perfo
  187. # In this case I have to test the effective gain over an itteration
  188. # Actually ORM does not allows distinct behavior
  189. sql = "Select distinct reconcile_id from account_move_line \
  190. where id in %s"
  191. self.cursor.execute(sql, (tuple(move_line_ids),))
  192. rec_ids = self.cursor.fetchall()
  193. if rec_ids:
  194. rec_ids = [x[0] for x in rec_ids]
  195. l_ids = move_line_obj.search(self.cursor,
  196. self.uid,
  197. [('reconcile_id', 'in', rec_ids),
  198. ('date', '>=', date_stop),
  199. ('date', '<=', date_until)])
  200. return l_ids
  201. else:
  202. return []
  203. ##############################################
  204. # Initial Partner Balance helper #
  205. ##############################################
  206. def _tree_move_line_ids(self, move_lines_data, key=None):
  207. """
  208. move_lines_data must be a list of dict which contains at least keys :
  209. - account_id
  210. - partner_id
  211. - other keys with values of the line
  212. - if param key is defined, only this key will be inserted in the tree
  213. returns a tree like
  214. res[account_id.1][partner_id.1][move_line.1,
  215. move_line.2]
  216. [partner_id.2][move_line.3]
  217. res[account_id.2][partner_id.1][move_line.4]
  218. """
  219. res = defaultdict(dict)
  220. for row in move_lines_data[:]:
  221. account_id = row.pop('account_id')
  222. partner_id = row.pop('partner_id')
  223. if key:
  224. res[account_id].setdefault(partner_id, []).append(row[key])
  225. else:
  226. res[account_id][partner_id] = row
  227. return res
  228. def _partners_initial_balance_line_ids(self, account_ids, start_period,
  229. partner_filter,
  230. exclude_reconcile=False,
  231. force_period_ids=False,
  232. date_stop=None):
  233. # take ALL previous periods
  234. period_ids = force_period_ids \
  235. if force_period_ids \
  236. else self._get_period_range_from_start_period(
  237. start_period, fiscalyear=False, include_opening=False)
  238. if not period_ids:
  239. period_ids = [-1]
  240. search_param = {
  241. 'date_start': start_period.date_start,
  242. 'period_ids': tuple(period_ids),
  243. 'account_ids': tuple(account_ids),
  244. }
  245. sql = ("SELECT ml.id, ml.account_id, ml.partner_id "
  246. "FROM account_move_line ml "
  247. "INNER JOIN account_account a "
  248. "ON a.id = ml.account_id "
  249. "WHERE ml.period_id in %(period_ids)s "
  250. "AND ml.account_id in %(account_ids)s ")
  251. if exclude_reconcile:
  252. if not date_stop:
  253. raise Exception(
  254. "Missing \"date_stop\" to compute the open invoices.")
  255. search_param.update({'date_stop': date_stop})
  256. sql += ("AND ((ml.reconcile_id IS NULL) "
  257. "OR (ml.reconcile_id IS NOT NULL \
  258. AND ml.last_rec_date > date(%(date_stop)s))) ")
  259. if partner_filter:
  260. sql += "AND ml.partner_id in %(partner_ids)s "
  261. search_param.update({'partner_ids': tuple(partner_filter)})
  262. self.cursor.execute(sql, search_param)
  263. return self.cursor.dictfetchall()
  264. def _compute_partners_initial_balances(self, account_ids, start_period,
  265. partner_filter=None,
  266. exclude_reconcile=False,
  267. force_period_ids=False):
  268. """We compute initial balance.
  269. If form is filtered by date all initial balance are equal to 0
  270. This function will sum pear and apple in currency amount if account
  271. as no secondary currency"""
  272. if isinstance(account_ids, (int, long)):
  273. account_ids = [account_ids]
  274. move_line_ids = self._partners_initial_balance_line_ids(
  275. account_ids, start_period, partner_filter,
  276. exclude_reconcile=exclude_reconcile,
  277. force_period_ids=force_period_ids)
  278. if not move_line_ids:
  279. move_line_ids = [{'id': -1}]
  280. sql = ("SELECT ml.account_id, ml.partner_id,"
  281. " sum(ml.debit) as debit, sum(ml.credit) as credit,"
  282. " sum(ml.debit-ml.credit) as init_balance,"
  283. " CASE WHEN a.currency_id ISNULL THEN 0.0\
  284. ELSE sum(ml.amount_currency) \
  285. END as init_balance_currency, "
  286. " c.name as currency_name "
  287. "FROM account_move_line ml "
  288. "INNER JOIN account_account a "
  289. "ON a.id = ml.account_id "
  290. "LEFT JOIN res_currency c "
  291. "ON c.id = a.currency_id "
  292. "WHERE ml.id in %(move_line_ids)s "
  293. "GROUP BY ml.account_id, ml.partner_id, a.currency_id, c.name")
  294. search_param = {
  295. 'move_line_ids': tuple([move_line['id'] for move_line in
  296. move_line_ids])}
  297. self.cursor.execute(sql, search_param)
  298. res = self.cursor.dictfetchall()
  299. return self._tree_move_line_ids(res)
  300. ############################################################
  301. # Partner specific helper #
  302. ############################################################
  303. def _order_partners(self, *args):
  304. """We get the partner linked to all current accounts that are used.
  305. We also use ensure that partner are ordered by name
  306. args must be list"""
  307. res = []
  308. partner_ids = []
  309. for arg in args:
  310. if arg:
  311. partner_ids += arg
  312. if not partner_ids:
  313. return []
  314. existing_partner_ids = [
  315. partner_id for partner_id in partner_ids if partner_id]
  316. if existing_partner_ids:
  317. # We may use orm here as the performance optimization is not that
  318. # big
  319. sql = ("SELECT name|| ' ' ||CASE WHEN ref IS NOT NULL \
  320. THEN '('||ref||')' \
  321. ELSE '' END, id, ref, name"
  322. " FROM res_partner \
  323. WHERE id IN %s ORDER BY LOWER(name), ref")
  324. self.cursor.execute(sql, (tuple(set(existing_partner_ids)),))
  325. res = self.cursor.fetchall()
  326. # move lines without partners, set None for empty partner
  327. if not all(partner_ids):
  328. res.append((None, None, None, None))
  329. if not res:
  330. return []
  331. return res