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.

425 lines
16 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. agged_lines_accounts = {}
  114. agged_totals_accounts = {}
  115. agged_percents_accounts = {}
  116. for acc in self.objects:
  117. agged_lines_accounts[acc.id] = {}
  118. agged_totals_accounts[acc.id] = {}
  119. agged_percents_accounts[acc.id] = {}
  120. for part_id, partner_lines in\
  121. self.localcontext['ledger_lines'][acc.id].items():
  122. aged_lines = self.compute_aged_lines(part_id,
  123. partner_lines,
  124. data)
  125. if aged_lines:
  126. agged_lines_accounts[acc.id][part_id] = aged_lines
  127. agged_totals_accounts[acc.id] = totals = self.compute_totals(
  128. agged_lines_accounts[acc.id].values())
  129. agged_percents_accounts[acc.id] = self.compute_percents(totals)
  130. self.localcontext.update({
  131. 'agged_lines_accounts': agged_lines_accounts,
  132. 'agged_totals_accounts': agged_totals_accounts,
  133. 'agged_percents_accounts': agged_percents_accounts,
  134. })
  135. # Free some memory
  136. del(self.localcontext['ledger_lines'])
  137. return res
  138. def compute_aged_lines(self, partner_id, ledger_lines, data):
  139. """Add property aged_lines to accounts browse records
  140. contained in :attr:`objects` for a given partner
  141. :param: partner_id: current partner
  142. :param ledger_lines: generated by parent
  143. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  144. :returns: dict of computed aged lines
  145. eg {'balance': 1000.0,
  146. 'aged_lines': {(90, 120): 0.0, ...}
  147. """
  148. lines_to_age = self.filter_lines(partner_id, ledger_lines)
  149. res = {}
  150. end_date = self._get_end_date(data)
  151. aged_lines = dict.fromkeys(RANGES, 0.0)
  152. reconcile_lookup = self.get_reconcile_count_lookup(lines_to_age)
  153. res['aged_lines'] = aged_lines
  154. for line in lines_to_age:
  155. compute_method = self.get_compute_method(reconcile_lookup,
  156. partner_id,
  157. line)
  158. delay = compute_method(line, end_date, ledger_lines)
  159. classification = self.classify_line(partner_id, delay)
  160. aged_lines[classification] += line['debit'] - line['credit']
  161. self.compute_balance(res, aged_lines)
  162. return res
  163. def _get_end_date(self, data):
  164. """Retrieve end date to be used to compute delay.
  165. :param data: data dict send to report contains form dict
  166. :returns: end date to be used to compute overdue delay
  167. """
  168. end_date = None
  169. date_to = data['form']['date_to']
  170. period_to_id = data['form']['period_to']
  171. fiscal_to_id = data['form']['fiscalyear_id']
  172. if date_to:
  173. end_date = date_to
  174. elif period_to_id:
  175. period_to = self.pool['account.period'].browse(self.cr,
  176. self.uid,
  177. period_to_id)
  178. end_date = period_to.date_stop
  179. elif fiscal_to_id:
  180. fiscal_to = self.pool['account.fiscalyear'].browse(self.cr,
  181. self.uid,
  182. fiscal_to_id)
  183. end_date = fiscal_to.date_stop
  184. else:
  185. raise ValueError('End date and end period not available')
  186. return end_date
  187. def _compute_delay_from_key(self, key, line, end_date):
  188. """Compute overdue delay delta in days for line using attribute in key
  189. delta = end_date - date of key
  190. :param line: current ledger line
  191. :param key: date key to be used to compute delta
  192. :param end_date: end_date computed for wizard data
  193. :returns: delta in days
  194. """
  195. from_date = datetime.strptime(line[key], DEFAULT_SERVER_DATE_FORMAT)
  196. end_date = datetime.strptime(end_date, DEFAULT_SERVER_DATE_FORMAT)
  197. delta = end_date - from_date
  198. return delta.days
  199. def compute_delay_from_maturity(self, line, end_date, ledger_lines):
  200. """Compute overdue delay delta in days for line using attribute in key
  201. delta = end_date - maturity date
  202. :param line: current ledger line
  203. :param end_date: end_date computed for wizard data
  204. :param ledger_lines: generated by parent
  205. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  206. :returns: delta in days
  207. """
  208. return self._compute_delay_from_key('date_maturity',
  209. line,
  210. end_date)
  211. def compute_delay_from_date(self, line, end_date, ledger_lines):
  212. """Compute overdue delay delta in days for line using attribute in key
  213. delta = end_date - date
  214. :param line: current ledger line
  215. :param end_date: end_date computed for wizard data
  216. :param ledger_lines: generated by parent
  217. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  218. :returns: delta in days
  219. """
  220. return self._compute_delay_from_key('ldate',
  221. line,
  222. end_date)
  223. def compute_delay_from_partial_rec(self, line, end_date, ledger_lines):
  224. """Compute overdue delay delta in days for the case where move line
  225. is related to a partial reconcile with more than one reconcile line
  226. :param line: current ledger line
  227. :param end_date: end_date computed for wizard data
  228. :param ledger_lines: generated by parent
  229. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  230. :returns: delta in days
  231. """
  232. sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE
  233. and line['rec_id'] == x['rec_id']]
  234. refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE
  235. and 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:`.AccountAgedTrialBalanceWebkit`
  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 compute_percents(self, totals):
  310. percents = {}
  311. base = totals['balance'] or 1.0
  312. for drange in RANGES:
  313. percents[drange] = (totals[drange] / base) * 100.0
  314. return percents
  315. def get_reconcile_count_lookup(self, lines):
  316. """Compute an lookup dict
  317. It contains has partial reconcile id as key and the count of lines
  318. related to the reconcile id
  319. :param: a list of ledger lines generated by parent
  320. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  321. :retuns: lookup dict {ec_id: count}
  322. """
  323. # possible bang if l_ids is really long.
  324. # We have the same weakness in common_report ...
  325. # but it seems not really possible for a partner
  326. # So I'll keep that option.
  327. l_ids = tuple(x['id'] for x in lines)
  328. sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line"
  329. " WHERE reconcile_partial_id IS NOT NULL"
  330. " AND id in %s"
  331. " GROUP BY reconcile_partial_id")
  332. self.cr.execute(sql, (l_ids,))
  333. res = self.cr.fetchall()
  334. return dict((x[0], x[1]) for x in res)
  335. HeaderFooterTextWebKitParser(
  336. 'report.account.account_aged_trial_balance_webkit',
  337. 'account.account',
  338. 'addons/account_financial_report_webkit/report/templates/\
  339. aged_trial_webkit.mako',
  340. parser=AccountAgedTrialBalanceWebkit,
  341. )