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.

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