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.

297 lines
12 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 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 datetime import datetime
  22. from openerp import pooler
  23. from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
  24. from openerp.tools.translate import _
  25. from .open_invoices import PartnersOpenInvoicesWebkit
  26. from .webkit_parser_header_fix import HeaderFooterTextWebKitParser
  27. def make_ranges(top, offset):
  28. """Return sorted days ranges"""
  29. ranges = [(n, min(n + offset, top)) for n in xrange(0, top, offset)]
  30. ranges.insert(0, (-100000000000, 0))
  31. ranges.append((top, 100000000000))
  32. return ranges
  33. #list of overdue ranges
  34. RANGES = make_ranges(120, 30)
  35. def make_ranges_titles():
  36. """Generates title to be used by Mako"""
  37. titles = [_('Due')]
  38. titles += [_(u'Overdue ≤ %s d.') % x[1] for x in RANGES[1:-1]]
  39. titles.append(_('Older'))
  40. return titles
  41. #list of overdue ranges title
  42. RANGES_TITLES = make_ranges_titles()
  43. #list of payable journal types
  44. REC_PAY_TYPE = ('purchase', 'sale')
  45. #list of refund payable type
  46. REFUND_TYPE = ('purchase_refund', 'sale_refund')
  47. INV_TYPE = REC_PAY_TYPE + REFUND_TYPE
  48. class AccountAgedTrialBalanceWebkit(PartnersOpenInvoicesWebkit):
  49. def __init__(self, cursor, uid, name, context=None):
  50. super(AccountAgedTrialBalanceWebkit, self).__init__(cursor, uid, name,
  51. context=context)
  52. self.pool = pooler.get_pool(self.cr.dbname)
  53. self.cursor = self.cr
  54. company = self.pool.get('res.users').browse(self.cr, uid, uid,
  55. context=context).company_id
  56. header_report_name = ' - '.join((_('Aged Partner Balance'),
  57. company.currency_id.name))
  58. footer_date_time = self.formatLang(str(datetime.today()),
  59. date_time=True)
  60. self.localcontext.update({
  61. 'cr': cursor,
  62. 'uid': uid,
  63. 'company': company,
  64. 'ranges': self._get_ranges(),
  65. 'ranges_titles': self._get_ranges_titles(),
  66. 'report_name': _('Aged Partner Balance'),
  67. 'additional_args': [
  68. ('--header-font-name', 'Helvetica'),
  69. ('--footer-font-name', 'Helvetica'),
  70. ('--header-font-size', '10'),
  71. ('--footer-font-size', '6'),
  72. ('--header-left', header_report_name),
  73. ('--header-spacing', '2'),
  74. ('--footer-left', footer_date_time),
  75. ('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
  76. ('--footer-line',),
  77. ],
  78. })
  79. def _get_ranges(self):
  80. """:returns: :cons:`RANGES`"""
  81. return RANGES
  82. def _get_ranges_titles(self):
  83. """:returns: :cons: `RANGES_TITLES`"""
  84. return RANGES_TITLES
  85. def set_context(self, objects, data, ids, report_type=None):
  86. """Populate aged_lines, aged_balance, aged_percents attributes
  87. on each browse record that will be used by mako template
  88. The computation are based on the ledger_lines attribute set on account
  89. contained by :attr:`objects`
  90. self.object were previously set by parent class
  91. :class: `.open_invoices.PartnersOpenInvoicesWebkit`
  92. :returns: parent :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  93. call to set_context
  94. """
  95. res = super(AccountAgedTrialBalanceWebkit, self).set_context(
  96. objects,
  97. data,
  98. ids,
  99. report_type=report_type
  100. )
  101. for acc in self.objects:
  102. acc.aged_lines = {}
  103. acc.agged_totals = {}
  104. acc.agged_percents = {}
  105. for part_id, partner_lines in acc.ledger_lines.items():
  106. aged_lines = self.compute_aged_lines(part_id,
  107. partner_lines,
  108. data)
  109. if aged_lines:
  110. acc.aged_lines[part_id] = aged_lines
  111. acc.aged_totals = totals = self.compute_totals(acc.aged_lines.values())
  112. acc.aged_percents = self.compute_percents(totals)
  113. #Free some memory
  114. del(acc.ledger_lines)
  115. return res
  116. def compute_aged_lines(self, partner_id, ledger_lines, data):
  117. """Add property aged_lines to accounts browse records
  118. contained in :attr:`objects` for a given partner
  119. :params: partner_id current partner
  120. :params: ledger_lines generated by parent
  121. :class:`.open_invoices.PartnersOpenInvoicesWebkit`
  122. :return computed ledger lines
  123. """
  124. lines_to_age = self.filter_lines(partner_id, ledger_lines)
  125. res = {}
  126. end_date = self._get_end_date(data)
  127. aged_lines = dict.fromkeys(RANGES, 0.0)
  128. reconcile_lookup = self.get_reconcile_count_lookup(lines_to_age)
  129. res['aged_lines'] = aged_lines
  130. for line in lines_to_age:
  131. compute_method = self.get_compute_method(reconcile_lookup,
  132. partner_id,
  133. line)
  134. delay = compute_method(line, end_date, ledger_lines)
  135. classification = self.classify_line(partner_id, delay)
  136. aged_lines[classification] += line['debit'] - line['credit']
  137. self.compute_balance(res, aged_lines)
  138. return res
  139. def _get_end_date(self, data):
  140. """Retrieve end date to be used to compute delay.
  141. :param data: data dict send to report contains form dict
  142. :returns: end date to be used to compute overdur delay
  143. """
  144. end_date = None
  145. date_to = data['form']['date_to']
  146. period_to_id = data['form']['period_to']
  147. fiscal_to_id = data['form']['fiscalyear_id']
  148. if date_to:
  149. end_date = date_to
  150. elif period_to_id:
  151. period_to = self.pool['account.period'].browse(self.cr,
  152. self.uid,
  153. period_to_id)
  154. end_date = period_to.date_stop
  155. elif fiscal_to_id:
  156. fiscal_to = self.pool['account.fiscalyear'].browse(self.cr,
  157. self.uid,
  158. fiscal_to_id)
  159. end_date = fiscal_to.date_stop
  160. else:
  161. raise ValueError('End date and end period not available')
  162. return end_date
  163. def _compute_delay_from_key(self, key, line, end_date):
  164. from_date = datetime.strptime(line[key], DEFAULT_SERVER_DATE_FORMAT)
  165. end_date = datetime.strptime(end_date, DEFAULT_SERVER_DATE_FORMAT)
  166. delta = end_date - from_date
  167. return delta.days
  168. def compute_delay_from_maturity(self, line, end_date, ledger_lines):
  169. return self._compute_delay_from_key('date_maturity',
  170. line,
  171. end_date)
  172. def compute_delay_from_date(self, line, end_date, ledger_lines):
  173. return self._compute_delay_from_key('ldate',
  174. line,
  175. end_date)
  176. def compute_delay_from_partial_rec(self, line, end_date, ledger_lines):
  177. sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE]
  178. refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE]
  179. reference_line = line
  180. if len(sale_lines) == 1:
  181. reference_line = sale_lines[0]
  182. elif len(refund_lines) == 1:
  183. reference_line = refund_lines[0]
  184. key = 'date_maturity' if line.get('date_maturity') else 'ldate'
  185. return self._compute_delay_from_key(key,
  186. reference_line,
  187. end_date)
  188. def get_compute_method(self, reconcile_lookup, partner_id, line):
  189. if reconcile_lookup.get(line['rec_id'], 0.0) > 1:
  190. return self.compute_delay_from_partial_rec
  191. if line['jtype'] in INV_TYPE:
  192. if line.get('date_maturity'):
  193. return self.compute_delay_from_maturity
  194. return self.compute_delay_from_date
  195. else:
  196. return self.compute_delay_from_date
  197. def line_is_valid(self, partner_id, line):
  198. """Predicate that tells if line has to be treated"""
  199. # waiting some spec here maybe dead code
  200. return True
  201. def filter_lines(self, partner_id, lines):
  202. # vaiting specs
  203. return [x for x in lines if self.line_is_valid(partner_id, x)]
  204. def classify_line(self, partner_id, overdue_days):
  205. """Return the range index for a number of overdue days
  206. We loop from smaller range to higher
  207. This should be the most effective solution as generaly
  208. customer tend to have one or two month of delay
  209. :param overdue_days: int representing the lenght in days of delay
  210. :returns: the index of the correct range in ´´RANGES´´
  211. """
  212. for drange in RANGES:
  213. if overdue_days <= drange[1]:
  214. return drange
  215. return drange
  216. def compute_balance(self, res, aged_lines):
  217. res['balance'] = sum(aged_lines.values())
  218. def compute_totals(self, aged_lines):
  219. totals = {}
  220. totals['balance'] = sum(x.get('balance', 0.0) for
  221. x in aged_lines)
  222. aged_ranges = [x.get('aged_lines', {}) for x in aged_lines]
  223. for drange in RANGES:
  224. totals[drange] = sum(x.get(drange, 0.0) for x in aged_ranges)
  225. return totals
  226. def compute_percents(self, totals):
  227. percents = {}
  228. base = float(totals['balance']) or 1.0
  229. for drange in RANGES:
  230. percents[drange] = (float(totals[drange]) / base) * 100.0
  231. return percents
  232. def get_reconcile_count_lookup(self, lines):
  233. # possible bang if l_ids is really long.
  234. # We have the same weakness in common_report ...
  235. # but it seems not really possible for a partner
  236. # So I'll keep that option.
  237. l_ids = tuple(x['id'] for x in lines)
  238. sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line \n"
  239. " WHERE reconcile_partial_id IS NOT NULL \n"
  240. " AND id in %s \n"
  241. " GROUP BY reconcile_partial_id")
  242. self.cr.execute(sql, (l_ids,))
  243. res = self.cr.fetchall()
  244. if res:
  245. return dict((x[0], x[1]) for x in res)
  246. return {}
  247. HeaderFooterTextWebKitParser(
  248. 'report.account.account_aged_trial_balance_webkit',
  249. 'account.account',
  250. 'addons/account_financial_report_webkit/report/templates/aged_trial_webkit.mako',
  251. parser=AccountAgedTrialBalanceWebkit,
  252. )