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.

273 lines
11 KiB

11 years ago
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 day 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 += [_('Overdue since %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 to accounts browse records contained in :attr:`objects`"""
  118. lines_to_age = self.filter_lines(partner_id, ledger_lines)
  119. res = {}
  120. end_date = self._get_end_date(data)
  121. aged_lines = dict.fromkeys(RANGES, 0.0)
  122. reconcile_lookup = self.get_reconcile_count_lookup(lines_to_age)
  123. res['aged_lines'] = aged_lines
  124. for line in lines_to_age:
  125. compute_method = self.get_compute_method(reconcile_lookup,
  126. partner_id,
  127. line)
  128. delay = compute_method(line, end_date, ledger_lines)
  129. classification = self.classify_line(partner_id, delay)
  130. aged_lines[classification] += line['debit'] - line['credit']
  131. self.compute_balance(res, aged_lines)
  132. return res
  133. def _get_end_date(self, data):
  134. end_date = None
  135. date_to = data['form']['date_to']
  136. period_to_id = data['form']['period_to']
  137. if date_to:
  138. end_date = date_to
  139. elif period_to_id:
  140. period_to = self.pool['account.period'].browse(self.cr,
  141. self.uid,
  142. period_to_id)
  143. end_date = period_to.date_stop
  144. else:
  145. raise ValueError('End date and end period not available')
  146. return end_date
  147. def _compute_delay_from_key(self, key, line, end_date):
  148. from_date = datetime.strptime(line[key], DEFAULT_SERVER_DATE_FORMAT)
  149. end_date = datetime.strptime(end_date, DEFAULT_SERVER_DATE_FORMAT)
  150. delta = end_date - from_date
  151. return delta.days
  152. def compute_delay_from_maturity(self, line, end_date, ledger_lines):
  153. return self._compute_delay_from_key('date_maturity',
  154. line,
  155. end_date)
  156. def compute_delay_from_date(self, line, end_date, ledger_lines):
  157. return self._compute_delay_from_key('ldate',
  158. line,
  159. end_date)
  160. def compute_delay_from_partial_rec(self, line, end_date, ledger_lines):
  161. sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE]
  162. refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE]
  163. reference_line = line
  164. if len(sale_lines) == 1:
  165. reference_line = sale_lines[0]
  166. elif len(refund_lines) == 1:
  167. reference_line = refund_lines[0]
  168. key = line.get('date_maturity', ldate')
  169. return self._compute_delay_from_key(key,
  170. reference_line,
  171. end_date)
  172. def get_compute_method(self, reconcile_lookup, partner_id, line):
  173. if reconcile_lookup.get(line['rec_id'], 0.0) > 1:
  174. return self.compute_delay_from_partial_rec
  175. if line['jtype'] in INV_TYPE:
  176. if line.get('date_maturity'):
  177. return self.compute_delay_from_maturity
  178. return self.compute_delay_from_date
  179. else:
  180. return self.compute_delay_from_date
  181. def line_is_valid(self, partner_id, line):
  182. """Predicate that tells if line has to be treated"""
  183. # waiting some spec here maybe dead code
  184. return True
  185. def filter_lines(self, partner_id, lines):
  186. # vaiting specs
  187. return [x for x in lines if self.line_is_valid(partner_id, x)]
  188. def classify_line(self, partner_id, overdue_days):
  189. """Return the range index for a number of overdue days
  190. We loop from smaller range to higher
  191. This should be the most effective solution as generaly
  192. customer tend to have one or two month of delay
  193. :param overdue_days: int representing the lenght in days of delay
  194. :returns: the index of the correct range in ´´RANGES´´
  195. """
  196. for drange in RANGES:
  197. if overdue_days <= drange[1]:
  198. return drange
  199. return drange
  200. def compute_balance(self, res, aged_lines):
  201. res['balance'] = sum(aged_lines.values())
  202. def compute_totals(self, aged_lines):
  203. totals = {}
  204. totals['balance'] = sum(x.get('balance', 0.0) for
  205. x in aged_lines)
  206. aged_ranges = [x.get('aged_lines', {}) for x in aged_lines]
  207. for drange in RANGES:
  208. totals[drange] = sum(x.get(drange, 0.0) for x in aged_ranges)
  209. return totals
  210. def compute_percents(self, totals):
  211. percents = {}
  212. base = float(totals['balance']) or 1.0
  213. for drange in RANGES:
  214. percents[drange] = (float(totals[drange]) / base) * 100.0
  215. return percents
  216. def get_reconcile_count_lookup(self, lines):
  217. # possible bang if l_ids is really long.
  218. # We have the same weakness in common_report ...
  219. # but it seems not really possible for a partner
  220. # So I'll keep that option.
  221. l_ids = tuple(x['id'] for x in lines)
  222. sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line \n"
  223. " WHERE reconcile_partial_id IS NOT NULL \n"
  224. " AND id in %s \n"
  225. " GROUP BY reconcile_partial_id")
  226. self.cr.execute(sql, (l_ids,))
  227. res = self.cr.fetchall()
  228. if res:
  229. return dict((x[0], x[1]) for x in res)
  230. return {}
  231. HeaderFooterTextWebKitParser(
  232. 'report.account.account_aged_trial_balance_webkit',
  233. 'account.account',
  234. 'addons/account_financial_report_webkit/report/templates/aged_trial_webkit.mako',
  235. parser=AccountAgedTrialBalanceWebkit,
  236. )