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.

429 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 = [
  233. x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE and
  234. line['rec_id'] == x['rec_id']
  235. ]
  236. refund_lines = [
  237. x for x in ledger_lines if x['jtype'] in REFUND_TYPE and
  238. line['rec_id'] == x['rec_id']
  239. ]
  240. if len(sale_lines) == 1:
  241. reference_line = sale_lines[0]
  242. elif len(refund_lines) == 1:
  243. reference_line = refund_lines[0]
  244. else:
  245. reference_line = line
  246. key = 'date_maturity' if reference_line.get(
  247. 'date_maturity') else 'ldate'
  248. return self._compute_delay_from_key(key,
  249. reference_line,
  250. end_date)
  251. def get_compute_method(self, reconcile_lookup, partner_id, line):
  252. """Get the function that should compute the delay for a given line
  253. :param reconcile_lookup: dict of reconcile group by id and count
  254. {rec_id: count of line related to reconcile}
  255. :param partner_id: current partner_id
  256. :param line: current ledger line generated by parent
  257. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  258. :returns: function bounded to :class:`.AccountAgedTrialBalanceWebkit`
  259. """
  260. if reconcile_lookup.get(line['rec_id'], 0.0) > 1:
  261. return self.compute_delay_from_partial_rec
  262. elif line['jtype'] in INV_TYPE and line.get('date_maturity'):
  263. return self.compute_delay_from_maturity
  264. else:
  265. return self.compute_delay_from_date
  266. def line_is_valid(self, partner_id, line):
  267. """Predicate hook that allows to filter line to be treated
  268. :param partner_id: current partner_id
  269. :param line: current ledger line generated by parent
  270. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  271. :returns: boolean True if line is allowed
  272. """
  273. return True
  274. def filter_lines(self, partner_id, lines):
  275. """Filter ledger lines that have to be treated
  276. :param partner_id: current partner_id
  277. :param lines: ledger_lines related to current partner
  278. and generated by parent
  279. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  280. :returns: list of allowed lines
  281. """
  282. return [x for x in lines if self.line_is_valid(partner_id, x)]
  283. def classify_line(self, partner_id, overdue_days):
  284. """Return the overdue range for a given delay
  285. We loop from smaller range to higher
  286. This should be the most effective solution as generaly
  287. customer tend to have one or two month of delay
  288. :param overdue_days: delay in days
  289. :param partner_id: current partner_id
  290. :returns: the correct range in :const:`RANGES`
  291. """
  292. for drange in RANGES:
  293. if overdue_days <= drange[1]:
  294. return drange
  295. return drange
  296. def compute_balance(self, res, aged_lines):
  297. """Compute the total balance of aged line
  298. for given account"""
  299. res['balance'] = sum(aged_lines.values())
  300. def compute_totals(self, aged_lines):
  301. """Compute the totals for an account
  302. :param aged_lines: dict of aged line taken from the
  303. property added to account record
  304. :returns: dict of total {'balance':1000.00, (30, 60): 3000,...}
  305. """
  306. totals = {}
  307. totals['balance'] = sum(x.get('balance', 0.0) for
  308. x in aged_lines)
  309. aged_ranges = [x.get('aged_lines', {}) for x in aged_lines]
  310. for drange in RANGES:
  311. totals[drange] = sum(x.get(drange, 0.0) for x in aged_ranges)
  312. return totals
  313. def compute_percents(self, totals):
  314. percents = {}
  315. base = totals['balance'] or 1.0
  316. for drange in RANGES:
  317. percents[drange] = (totals[drange] / base) * 100.0
  318. return percents
  319. def get_reconcile_count_lookup(self, lines):
  320. """Compute an lookup dict
  321. It contains has partial reconcile id as key and the count of lines
  322. related to the reconcile id
  323. :param: a list of ledger lines generated by parent
  324. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  325. :retuns: lookup dict {ec_id: count}
  326. """
  327. # possible bang if l_ids is really long.
  328. # We have the same weakness in common_report ...
  329. # but it seems not really possible for a partner
  330. # So I'll keep that option.
  331. l_ids = tuple(x['id'] for x in lines)
  332. sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line"
  333. " WHERE reconcile_partial_id IS NOT NULL"
  334. " AND id in %s"
  335. " GROUP BY reconcile_partial_id")
  336. self.cr.execute(sql, (l_ids,))
  337. res = self.cr.fetchall()
  338. return dict((x[0], x[1]) for x in res)
  339. HeaderFooterTextWebKitParser(
  340. 'report.account.account_aged_trial_balance_webkit',
  341. 'account.account',
  342. 'addons/account_financial_report_webkit/report/templates/\
  343. aged_trial_webkit.mako',
  344. parser=AccountAgedTrialBalanceWebkit,
  345. )