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.

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