Commit d35b95ef authored by Vincent Pelletier's avatar Vincent Pelletier

AccountingTransactionModule_get*AgedBalanceLineList: Rework.

Make it more easily extensible by exposing a callback.
Stop loading documents in the simple version that are only needed by the
detailed version. Also, stop checking for this inside the main loop, and
instead use the callback mechanism.
Use a single query to look for mirror section title instead of one per
mirror section.
Behaviour change: mirror section (ie, the entity) title is not
translated anymore. It used to be because there did not seem to be a
reasion not to. It is not anymore to avoid loading rach entity from ZODB
in the default report. This rework makes customisation easier, including
overwriting this untranslated title with any desired transformation.
parent 53108963
"""
at_date (DateTime)
See getMovementHistoryList.
section_category (str)
section_category_strict (bool)
See Base_getSectionUidListForSectionCategory.
Result passed to getMovementHistoryList.
simulation_state (str or list of str)
See getMovementHistoryList.
period_list (list of numbers)
List of operation age ranges, in days. Used to dispatch operations by age:
period_0: older than earliest period
period_1: younger than earliest period, older than next period
...
period_n: younger than period n-1, older than at_date
period_future: Posterior to at_date
account_type (str)
Must be one of:
'account_type/asset/receivable'
'account_type/liability/payable'
lineCallback ((brain, period_name, line_dict) -> dict)
Called for each line found by getMovementHistoryList.
brain
Current line.
period_name (string)
Name of the period this line belongs to.
line_dict (dict)
Dictionary containing properties of corresponding line in the report.
May be modified by callback.
Returned value is resulting line property dictionary, or None to skip this
row.
reportCallback ((line_list) -> list of dict)
Called once all lines from getMovementHistoryList have been processed.
line_list (list)
Each entry is the dict returned by lineCallback, in order.
May be modified by callback.
Returned value is final line list. Each line must be a dict. If unsure, return
line_list.
"""
from collections import defaultdict
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery, ComplexQuery
from Products.PythonScripts.standard import Object
if reportCallback is None:
reportCallback = lambda x: x
portal = context.getPortalObject()
portal_catalog = portal.portal_catalog
portal_categories = portal.portal_categories
# we set the precision in request, for formatting on editable fields
portal.REQUEST.set(
'precision',
context.account_module.getQuantityPrecisionFromResource(
context.Base_getCurrencyForSection(section_category),
),
)
line_list = []
assert account_type in ('account_type/asset/receivable', 'account_type/liability/payable')
reverse_price_sign = account_type == 'account_type/liability/payable'
by_mirror_section_list_dict = defaultdict(list)
node_uid_list = [
x.uid
for x in portal_catalog(
portal_type='Account',
strict_account_type_uid=portal_categories.restrictedTraverse(account_type).getUid(),
)
]
if not node_uid_list:
return []
extra_kw = {}
ledger_relative_url_list = kw.get('ledger', None)
if ledger_relative_url_list:
if not isinstance(ledger_relative_url_list, list):
ledger_relative_url_list = [ledger_relative_url_list]
traverse = portal.portal_categories.restrictedTraverse
extra_kw['ledger_uid'] = [traverse(x).getUid() for x in ledger_relative_url_list]
period_list = [
('period_%i' % x, y)
for x, y in enumerate(sorted(period_list))
]
last_period_id = 'period_%s' % len(period_list)
for brain in portal.portal_simulation.getMovementHistoryList(
at_date=at_date,
simulation_state=simulation_state,
node_uid=node_uid_list,
portal_type=portal.getPortalAccountingMovementTypeList(),
section_uid=portal.Base_getSectionUidListForSectionCategory(
section_category,
section_category_strict,
),
grouping_query=ComplexQuery(
SimpleQuery(grouping_reference=None),
SimpleQuery(grouping_date=at_date, comparison_operator=">="),
logical_operator="OR",
),
**extra_kw
):
total_price = brain.total_price or 0
if reverse_price_sign:
total_price = -total_price
# Note that we use date_utc because date would load the object and we are just
# interested in the difference of days.
age = int(at_date - brain.date_utc)
if age < 0:
period_name = 'period_future'
else:
for period_name, period in period_list:
if age <= period:
break
else:
period_name = last_period_id
line_dict = lineCallback(
brain=brain,
period_name=period_name,
line_dict={
'mirror_section_uid': brain.mirror_section_uid,
'total_price': total_price,
'age': age,
period_name: total_price,
},
)
if line_dict is not None:
by_mirror_section_list_dict[brain.mirror_section_uid].append(line_dict)
line_list.append(line_dict)
for row in portal_catalog(
select_list=['title'],
uid=by_mirror_section_list_dict.keys(),
  • @vpelletier I think here we can potentially have empty list. Would it be better to have:

    if by_mirror_section_list_dict:
      for row in portal_catalog(
        select_list=['title'],
        ...

    I feel that something like this may break later on report rendering so maybe we can do another fix there to support empty report. Yet this is edge case and the first importan thing it to avoid a case of too long query.

  • the first importan thing it to avoid a case of too long query.

    +1

  • Thanks, done in 406a0181

Please register or sign in to reply
):
title = row.title
for line in by_mirror_section_list_dict[row.uid]:
line['mirror_section_title'] = title
return [
Object(
uid='new_',
**x
)
for x in reportCallback(line_list)
]
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>at_date, section_category, section_category_strict, simulation_state, period_list, account_type, lineCallback, reportCallback=None, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>AccountingTransactionModule_getAgedBalanceLineList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery, ComplexQuery
from Products.PythonScripts.standard import Object
portal = context.getPortalObject()
assert account_type in ('account_type/asset/receivable', 'account_type/liability/payable')
currency = context.Base_getCurrencyForSection(section_category)
precision = context.account_module.getQuantityPrecisionFromResource(currency)
# we set the precision in request, for formatting on editable fields
portal.REQUEST.set('precision', precision)
section_uid = portal.Base_getSectionUidListForSectionCategory(
section_category, section_category_strict)
grouping_query = ComplexQuery(
SimpleQuery(grouping_reference=None),
SimpleQuery(grouping_date=at_date, comparison_operator=">="),
logical_operator="OR")
if lineCallback is None:
lineCallback = lambda brain, period_name, line_dict: line_dict
if reportCallback is None:
reportCallback = lambda x: x
traverse = context.getPortalObject().restrictedTraverse
account_number_memo = {}
def getAccountNumber(account_url):
try:
return account_number_memo[account_url]
except KeyError:
account_number_memo[account_url] =\
portal.restrictedTraverse(account_url).Account_getGapId()
account_number_memo[account_url] = traverse(account_url).Account_getGapId()
return account_number_memo[account_url]
section_title_memo = {None: ''}
def getSectionTitle(uid):
try:
return section_title_memo[uid]
except KeyError:
section_title = ''
brain_list = portal.portal_catalog(uid=uid, limit=2)
if brain_list:
brain, = brain_list
section_title = brain.getObject().getTranslatedTitle()
section_title_memo[uid] = section_title
return section_title_memo[uid]
last_period_id = 'period_%s' % len(period_list)
line_list = []
extra_kw = {}
ledger = kw.get('ledger', None)
if ledger:
if not isinstance(ledger, list):
# Allows the generation of reports on different ledgers as the same time
ledger = [ledger]
portal_categories = portal.portal_categories
ledger_value_list = [portal_categories.restrictedTraverse(ledger_category, None)
for ledger_category in ledger]
for ledger_value in ledger_value_list:
extra_kw.setdefault('ledger_uid', []).append(ledger_value.getUid())
for brain in portal.portal_simulation.getMovementHistoryList(
at_date=at_date,
simulation_state=simulation_state,
node_category_strict_membership=account_type,
portal_type=portal.getPortalAccountingMovementTypeList(),
section_uid=section_uid,
grouping_query=grouping_query,
sort_on=(('stock.mirror_section_uid', 'ASC'),
('stock.date', 'ASC'),
('stock.uid', 'ASC')),
**extra_kw):
def myLineCallback(brain, period_name, line_dict):
line_dict = lineCallback(brain=brain, period_name=period_name, line_dict=line_dict)
if line_dict is None:
return
movement = brain.getObject()
transaction = movement.getParentValue()
total_price = brain.total_price or 0
if account_type == 'account_type/liability/payable':
total_price = - total_price
line = Object(uid='new_',
mirror_section_title=getSectionTitle(brain.mirror_section_uid),
mirror_section_uid=brain.mirror_section_uid,
total_price=total_price,)
if detail:
# Detailed version of the aged balance report needs to get properties from
# the movement or transactions, but summary does not. This conditional is
# here so that we do not load objects when running in summary mode.
line['explanation_title'] = movement.hasTitle() and movement.getTitle() or transaction.getTitle()
line['reference'] = transaction.getReference()
line['portal_type'] = transaction.getTranslatedPortalType()
line['date'] = brain.date
if brain.mirror_section_uid == movement.getSourceSectionUid() and brain.mirror_node_uid == movement.getSourceUid():
line['specific_reference'] = transaction.getDestinationReference()
line['gap_id'] = getAccountNumber(movement.getDestination())
else:
line['specific_reference'] = transaction.getSourceReference()
line['gap_id'] = getAccountNumber(movement.getSource())
assert brain.mirror_section_uid == movement.getDestinationSectionUid()
# Note that we use date_utc because date would load the object and we are just
# interested in the difference of days.
age = int(at_date - brain.date_utc)
line['age'] = age
if age < 0:
line['period_future'] = total_price
elif age <= period_list[0]:
line['period_0'] = total_price
# Detailed version of the aged balance report needs to get properties from
# the movement or transactions, but summary does not. This conditional is
# here so that we do not load objects when running in summary mode.
line_dict['explanation_title'] = movement.hasTitle() and movement.getTitle() or transaction.getTitle()
line_dict['reference'] = transaction.getReference()
line_dict['portal_type'] = transaction.getTranslatedPortalType()
line_dict['date'] = brain.date
if brain.mirror_section_uid == movement.getSourceSectionUid() and brain.mirror_node_uid == movement.getSourceUid():
line_dict['specific_reference'] = transaction.getDestinationReference()
line_dict['gap_id'] = getAccountNumber(movement.getDestination())
else:
for idx, period in enumerate(period_list):
if age <= period:
line['period_%s' % idx] = total_price
break
else:
line[last_period_id] = total_price
line_list.append(line)
return sorted(
line_list,
key=lambda x:(x['mirror_section_title'],
x['mirror_section_uid'], # in case we have two mirror section with same title
# we need lines from same section to be grouped together
# for summary report.
x.get('date'),
x.get('explanation_title'),))
line_dict['specific_reference'] = transaction.getSourceReference()
line_dict['gap_id'] = getAccountNumber(movement.getSource())
assert brain.mirror_section_uid == movement.getDestinationSectionUid()
return line_dict
def myReportCallback(line_list):
return reportCallback(
sorted(
line_list,
key=lambda x: (x['mirror_section_title'], x['date'], x['explanation_title']),
),
)
return context.AccountingTransactionModule_getAgedBalanceLineList(
lineCallback=myLineCallback,
reportCallback=myReportCallback,
**kw
)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>at_date, section_category, section_category_strict, simulation_state, period_list, account_type, detail=True, **kw</string> </value>
<value> <string>lineCallback=None, reportCallback=None, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
from Products.PythonScripts.standard import Object
portal = context.getPortalObject()
line_list = []
detail_line_list = portal\
.AccountingTransactionModule_getDetailedAgedBalanceLineList(
at_date, section_category, section_category_strict,
simulation_state, period_list, account_type, detail=False, **kw)
period_id_list = ['period_future']
for idx, _ in enumerate(period_list):
period_id_list.append('period_%s' % idx)
period_id_list.append('period_%s' % (idx + 1))
# Initialize to something that will not be equals to
# detail_line.mirror_section_uid below.
# In case we have used an account with mirror section,
# then mirror_section_uid will be None
previous_mirror_section_uid = -1
for detail_line in detail_line_list:
if previous_mirror_section_uid != detail_line.mirror_section_uid:
line = Object(uid='new_',
mirror_section_title=detail_line.mirror_section_title,
total_price=0)
line_list.append(line)
previous_mirror_section_uid = detail_line.mirror_section_uid
line['total_price'] = detail_line.total_price + line['total_price']
for period_id in period_id_list:
previous_value = line.get(period_id, 0)
added_value = detail_line.get(period_id, 0)
new_value = previous_value + added_value
if previous_value or new_value:
line[period_id] = new_value
return line_list
if lineCallback is None:
lineCallback = lambda brain, period_name, line_dict: line_dict
if reportCallback is None:
reportCallback = lambda x: x
by_mirror_section_dict = {}
def myBrainCallback(brain, period_name, line_dict):
try:
mirror_section_line_dict = by_mirror_section_dict[line_dict['mirror_section_uid']]
except KeyError:
line_dict = lineCallback(brain=brain, period_name=period_name, line_dict=line_dict)
if line_dict is None:
return
by_mirror_section_dict[line_dict['mirror_section_uid']] = line_dict
return line_dict
else:
total_price = line_dict['total_price']
mirror_section_line_dict[period_name] = mirror_section_line_dict.get(period_name, 0) + total_price
mirror_section_line_dict['total_price'] = mirror_section_line_dict['total_price'] + total_price
def myReportCallback(line_list):
return reportCallback(
sorted(line_list, key=lambda x: x['mirror_section_title']),
)
return context.AccountingTransactionModule_getAgedBalanceLineList(
lineCallback=myBrainCallback,
reportCallback=myReportCallback,
**kw
)
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>at_date, section_category, section_category_strict, simulation_state, period_list, account_type, **kw</string> </value>
<value> <string>lineCallback=None, reportCallback=None, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment