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.

420 lines
16 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Author: Alexis de Lattre
  5. # Author: Nicolas Bessi
  6. # Copyright 2015 Akretion (www.akretion.com)
  7. # Copyright 2014 Camptocamp SA
  8. #
  9. # This program is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU Affero General Public License as
  11. # published by the Free Software Foundation, either version 3 of the
  12. # License, or (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU Affero General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Affero General Public License
  20. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. ##############################################################################
  23. from __future__ import division
  24. from datetime import datetime
  25. from openerp import pooler
  26. from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
  27. from openerp.tools.translate import _
  28. from .open_invoices import PartnersOpenInvoicesWebkit
  29. from .webkit_parser_header_fix import HeaderFooterTextWebKitParser
  30. def make_ranges(top, offset):
  31. """Return sorted days ranges
  32. :param top: maximum overdue day
  33. :param offset: offset for ranges
  34. :returns: list of sorted ranges tuples in days
  35. eg. [(-100000, 0), (0, offset),
  36. (offset, n*offset), ... (top, 100000)]
  37. """
  38. ranges = [(n, min(n + offset, top)) for n in xrange(0, top, offset)]
  39. ranges.insert(0, (-100000000000, 0))
  40. ranges.append((top, 100000000000))
  41. return ranges
  42. # list of overdue ranges
  43. RANGES = make_ranges(120, 30)
  44. def make_ranges_titles():
  45. """Generates title to be used by mako"""
  46. titles = [_('Not Due')]
  47. titles += [_(u'Overdue ≤ %s d.') % x[1] for x in RANGES[1:-1]]
  48. titles.append(_('Overdue > %s d.') % RANGES[-1][0])
  49. return titles
  50. # list of overdue ranges title
  51. RANGES_TITLES = make_ranges_titles()
  52. # list of payable journal types
  53. REC_PAY_TYPE = ('purchase', 'sale')
  54. # list of refund payable type
  55. REFUND_TYPE = ('purchase_refund', 'sale_refund')
  56. INV_TYPE = REC_PAY_TYPE + REFUND_TYPE
  57. class AccountAgedOpenInvoicesWebkit(PartnersOpenInvoicesWebkit):
  58. """Compute Aged Open Invoices based on result of Open Invoices"""
  59. def __init__(self, cursor, uid, name, context=None):
  60. """Constructor,
  61. refer to :class:`openerp.report.report_sxw.rml_parse`"""
  62. super(AccountAgedOpenInvoicesWebkit, self).__init__(cursor, uid, name,
  63. context=context)
  64. self.pool = pooler.get_pool(self.cr.dbname)
  65. self.cursor = self.cr
  66. company = self.pool.get('res.users').browse(self.cr, uid, uid,
  67. context=context).company_id
  68. header_report_name = ' - '.join((_('Aged Open Invoices'),
  69. company.currency_id.name))
  70. footer_date_time = self.formatLang(str(datetime.today()),
  71. date_time=True)
  72. self.localcontext.update({
  73. 'cr': cursor,
  74. 'uid': uid,
  75. 'company': company,
  76. 'ranges': self._get_ranges(),
  77. 'ranges_titles': self._get_ranges_titles(),
  78. 'report_name': _('Aged Open Invoices'),
  79. 'additional_args': [
  80. ('--header-font-name', 'Helvetica'),
  81. ('--footer-font-name', 'Helvetica'),
  82. ('--header-font-size', '10'),
  83. ('--footer-font-size', '6'),
  84. ('--header-left', header_report_name),
  85. ('--header-spacing', '2'),
  86. ('--footer-left', footer_date_time),
  87. ('--footer-right',
  88. ' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
  89. ('--footer-line',),
  90. ],
  91. })
  92. def _get_ranges(self):
  93. """:returns: :cons:`RANGES`"""
  94. return RANGES
  95. def _get_ranges_titles(self):
  96. """:returns: :cons: `RANGES_TITLES`"""
  97. return RANGES_TITLES
  98. def set_context(self, objects, data, ids, report_type=None):
  99. """Populate aged_lines, aged_balance, aged_percents attributes
  100. on each account browse record that will be used by mako template
  101. The browse record are store in :attr:`objects`
  102. The computation are based on the ledger_lines attribute set on account
  103. contained by :attr:`objects`
  104. :attr:`objects` values were previously set by parent class
  105. :class: `.open_invoices.PartnersOpenInvoicesWebkit`
  106. :returns: parent :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  107. call to set_context
  108. """
  109. res = super(AccountAgedOpenInvoicesWebkit, self).set_context(
  110. objects,
  111. data,
  112. ids,
  113. report_type=report_type
  114. )
  115. aged_open_inv = {}
  116. # Stupid dict that we copy in parts of the main aged_open_inv dict
  117. aged_dict = {}
  118. for classif in self.localcontext['ranges']:
  119. aged_dict[classif] = 0.0
  120. for acc in self.objects:
  121. aged_open_inv[acc.id] = aged_dict.copy()
  122. aged_open_inv[acc.id]['balance'] = 0.0
  123. for part_id, partner_lines in\
  124. self.localcontext['ledger_lines'][acc.id].items():
  125. aged_open_inv[acc.id][part_id] = aged_dict.copy()
  126. aged_open_inv[acc.id][part_id]['balance'] = 0.0
  127. aged_open_inv[acc.id][part_id]['lines'] = list(partner_lines)
  128. for line in aged_open_inv[acc.id][part_id]['lines']:
  129. line.update(aged_dict)
  130. self.compute_aged_line(part_id, line, data)
  131. aged_open_inv[acc.id][part_id]['balance'] +=\
  132. line['balance']
  133. aged_open_inv[acc.id]['balance'] += line['balance']
  134. for classif in self.localcontext['ranges']:
  135. aged_open_inv[acc.id][part_id][classif] +=\
  136. line[classif]
  137. aged_open_inv[acc.id][classif] +=\
  138. line[classif]
  139. self.localcontext.update({
  140. 'aged_open_inv': aged_open_inv,
  141. })
  142. return res
  143. def compute_aged_line(self, partner_id, ledger_line, data):
  144. """Add classification to accounts browse records
  145. contained in :attr:`objects` for a given partner
  146. :param: partner_id: current partner
  147. :param ledger_line: generated by parent
  148. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  149. :returns: dict of computed aged lines
  150. eg {'balance': 1000.0,
  151. 'aged_lines': {(90, 120): 0.0, ...}
  152. """
  153. end_date = self._get_end_date(data)
  154. reconcile_lookup = self.get_reconcile_count_lookup([ledger_line])
  155. compute_method = self.get_compute_method(reconcile_lookup,
  156. partner_id,
  157. ledger_line)
  158. delay = compute_method(ledger_line, end_date, [ledger_line])
  159. classification = self.classify_line(partner_id, delay)
  160. ledger_line[classification] += ledger_line['balance']
  161. def _get_end_date(self, data):
  162. """Retrieve end date to be used to compute delay.
  163. :param data: data dict send to report contains form dict
  164. :returns: end date to be used to compute overdue delay
  165. """
  166. end_date = None
  167. date_to = data['form']['date_to']
  168. period_to_id = data['form']['period_to']
  169. fiscal_to_id = data['form']['fiscalyear_id']
  170. if date_to:
  171. end_date = date_to
  172. elif period_to_id:
  173. period_to = self.pool['account.period'].browse(self.cr,
  174. self.uid,
  175. period_to_id)
  176. end_date = period_to.date_stop
  177. elif fiscal_to_id:
  178. fiscal_to = self.pool['account.fiscalyear'].browse(self.cr,
  179. self.uid,
  180. fiscal_to_id)
  181. end_date = fiscal_to.date_stop
  182. else:
  183. raise ValueError('End date and end period not available')
  184. return end_date
  185. def _compute_delay_from_key(self, key, line, end_date):
  186. """Compute overdue delay delta in days for line using attribute in key
  187. delta = end_date - date of key
  188. :param line: current ledger line
  189. :param key: date key to be used to compute delta
  190. :param end_date: end_date computed for wizard data
  191. :returns: delta in days
  192. """
  193. from_date = datetime.strptime(line[key], DEFAULT_SERVER_DATE_FORMAT)
  194. end_date = datetime.strptime(end_date, DEFAULT_SERVER_DATE_FORMAT)
  195. delta = end_date - from_date
  196. return delta.days
  197. def compute_delay_from_maturity(self, line, end_date, ledger_lines):
  198. """Compute overdue delay delta in days for line using attribute in key
  199. delta = end_date - maturity date
  200. :param line: current ledger line
  201. :param end_date: end_date computed for wizard data
  202. :param ledger_lines: generated by parent
  203. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  204. :returns: delta in days
  205. """
  206. return self._compute_delay_from_key('date_maturity',
  207. line,
  208. end_date)
  209. def compute_delay_from_date(self, line, end_date, ledger_lines):
  210. """Compute overdue delay delta in days for line using attribute in key
  211. delta = end_date - date
  212. :param line: current ledger line
  213. :param end_date: end_date computed for wizard data
  214. :param ledger_lines: generated by parent
  215. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  216. :returns: delta in days
  217. """
  218. return self._compute_delay_from_key('ldate',
  219. line,
  220. end_date)
  221. def compute_delay_from_partial_rec(self, line, end_date, ledger_lines):
  222. """Compute overdue delay delta in days for the case where move line
  223. is related to a partial reconcile with more than one reconcile line
  224. :param line: current ledger line
  225. :param end_date: end_date computed for wizard data
  226. :param ledger_lines: generated by parent
  227. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  228. :returns: delta in days
  229. """
  230. sale_lines = [
  231. x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE and
  232. line['rec_id'] == x['rec_id']]
  233. refund_lines = [
  234. x for x in ledger_lines if x['jtype'] in REFUND_TYPE and
  235. line['rec_id'] == x['rec_id']]
  236. if len(sale_lines) == 1:
  237. reference_line = sale_lines[0]
  238. elif len(refund_lines) == 1:
  239. reference_line = refund_lines[0]
  240. else:
  241. reference_line = line
  242. key = 'date_maturity' if reference_line.get(
  243. 'date_maturity') else 'ldate'
  244. return self._compute_delay_from_key(key,
  245. reference_line,
  246. end_date)
  247. def get_compute_method(self, reconcile_lookup, partner_id, line):
  248. """Get the function that should compute the delay for a given line
  249. :param reconcile_lookup: dict of reconcile group by id and count
  250. {rec_id: count of line related to reconcile}
  251. :param partner_id: current partner_id
  252. :param line: current ledger line generated by parent
  253. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  254. :returns: function bounded to :class:`.AccountAgedOpenInvoicesWebkit`
  255. """
  256. if reconcile_lookup.get(line['rec_id'], 0.0) > 1:
  257. return self.compute_delay_from_partial_rec
  258. elif line['jtype'] in INV_TYPE and line.get('date_maturity'):
  259. return self.compute_delay_from_maturity
  260. else:
  261. return self.compute_delay_from_date
  262. def line_is_valid(self, partner_id, line):
  263. """Predicate hook that allows to filter line to be treated
  264. :param partner_id: current partner_id
  265. :param line: current ledger line generated by parent
  266. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  267. :returns: boolean True if line is allowed
  268. """
  269. return True
  270. def filter_lines(self, partner_id, lines):
  271. """Filter ledger lines that have to be treated
  272. :param partner_id: current partner_id
  273. :param lines: ledger_lines related to current partner
  274. and generated by parent
  275. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  276. :returns: list of allowed lines
  277. """
  278. return [x for x in lines if self.line_is_valid(partner_id, x)]
  279. def classify_line(self, partner_id, overdue_days):
  280. """Return the overdue range for a given delay
  281. We loop from smaller range to higher
  282. This should be the most effective solution as generaly
  283. customer tend to have one or two month of delay
  284. :param overdue_days: delay in days
  285. :param partner_id: current partner_id
  286. :returns: the correct range in :const:`RANGES`
  287. """
  288. for drange in RANGES:
  289. if overdue_days <= drange[1]:
  290. return drange
  291. return drange
  292. def compute_balance(self, res, aged_lines):
  293. """Compute the total balance of aged line
  294. for given account"""
  295. res['balance'] = sum(aged_lines.values())
  296. def compute_totals(self, aged_lines):
  297. """Compute the totals for an account
  298. :param aged_lines: dict of aged line taken from the
  299. property added to account record
  300. :returns: dict of total {'balance':1000.00, (30, 60): 3000,...}
  301. """
  302. totals = {}
  303. totals['balance'] = sum(x.get('balance', 0.0) for
  304. x in aged_lines)
  305. aged_ranges = [x.get('aged_lines', {}) for x in aged_lines]
  306. for drange in RANGES:
  307. totals[drange] = sum(x.get(drange, 0.0) for x in aged_ranges)
  308. return totals
  309. def get_reconcile_count_lookup(self, lines):
  310. """Compute an lookup dict
  311. It contains has partial reconcile id as key and the count of lines
  312. related to the reconcile id
  313. :param: a list of ledger lines generated by parent
  314. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  315. :retuns: lookup dict {ec_id: count}
  316. """
  317. # possible bang if l_ids is really long.
  318. # We have the same weakness in common_report ...
  319. # but it seems not really possible for a partner
  320. # So I'll keep that option.
  321. l_ids = tuple(x['id'] for x in lines)
  322. sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line"
  323. " WHERE reconcile_partial_id IS NOT NULL"
  324. " AND id in %s"
  325. " GROUP BY reconcile_partial_id")
  326. self.cr.execute(sql, (l_ids,))
  327. res = self.cr.fetchall()
  328. return dict((x[0], x[1]) for x in res)
  329. HeaderFooterTextWebKitParser(
  330. 'report.account.account_aged_open_invoices_webkit',
  331. 'account.account',
  332. 'addons/account_financial_report_webkit/report/templates/\
  333. aged_open_invoices_webkit.mako',
  334. parser=AccountAgedOpenInvoicesWebkit,
  335. )