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.

410 lines
15 KiB

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