Commit 902b0ce5 authored by Jérome Perrin's avatar Jérome Perrin

deferred_style: alarm to automate report production

See Alarm_generateReportDocumentList for the full API and the test for example
usage.
parent 2b31de5a
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Alarm" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>active_sense_method_id</string> </key>
<value> <string>Alarm_generateReportDocumentList</string> </value>
</item>
<item>
<key> <string>automatic_solve</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>configuration_form_id</string> </key>
<value> <string>Alarm_viewGenerateReportDocumentConfiguration</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>template_report_alarm</string> </value>
</item>
<item>
<key> <string>periodicity_day_frequency</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>periodicity_hour</string> </key>
<value>
<tuple>
<int>0</int>
</tuple>
</value>
</item>
<item>
<key> <string>periodicity_minute</string> </key>
<value>
<tuple>
<int>0</int>
</tuple>
</value>
</item>
<item>
<key> <string>periodicity_month</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_month_day</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_start_date</string> </key>
<value>
<object>
<klass>
<global name="DateTime" module="DateTime.DateTime"/>
</klass>
<tuple>
<none/>
</tuple>
<state>
<tuple>
<float>1609459200.0</float>
<string>GMT</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>periodicity_week</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Alarm</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Report Alarm</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# type: (str, list[dict], str, str, str, str, str, DateTime.DateTime, Any)
portal = context.getPortalObject()
for attachment in attachment_list:
document = portal.portal_contributions.newContent(
data=attachment['content'],
filename=attachment['name'],
title=title,
reference=reference,
version=version,
publication_section=publication_section,
language=language,
effective_date=effective_date,
)
document.share()
<?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>subject, attachment_list, title, reference, version, publication_section=\'\', language=None, effective_date=None, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Alarm_contributeAndShareReportDocument</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# coding: utf-8
"""
Alarm must define a script that returns a list of dictionnaries with the following keys:
- form_id str: id of an ERP5 Form or ERP5 Report. Required.
- context erp5.portal_type.Base: the context to render the report. Required.
- parameters dict: request parameters to render the report. Required.
Must be serializable for CMFActivity
- skin_name str: skin selection to use for this report ('ODS' | 'ODT'). Required.
- format Optional[str]: convert the document to this format. Note that in scenarios like
storing the result report in document module, it's better to keep the default format (None)
and convert on demand the stored document.
- language str: Localizer language to use. Required.
- callback_script_id str: id of a script to call at the end of report generation. Required
The script will be called on the context of the alarm, with the following arguments:
- subject str: the name of the report
- attachment_list dict: files produced by the report, dicts with following keys:
- name str: file name
- mime str: file mime type
- content bytes: file body
- **callback_script_kwargs
- callback_script_kwargs dict: of arguemnts that will be passed to callback script id.
- setup Callable[[dict], dict]: a function to call at setup before rendering the report.
This function receive this dict as argument and must return a dict of the same type.
"""
priority = 3
portal = context.getPortalObject()
report_configuration_script_id = context.getProperty('report_configuration_script_id')
assert report_configuration_script_id
for report_data in getattr(context, report_configuration_script_id)():
if report_data.get('setup'):
report_data = report_data['setup'](report_data)
notify_report_complete_kwargs = {
'alarm_relative_url': context.getRelativeUrl(),
'callback_script_id': report_data['callback_script_id'],
'callback_script_kwargs': report_data.get('callback_script_kwargs', {}),
}
report_context = report_data.get('context', context)
report_active_context = report_context.activate(
activity='SQLQueue',
node=portal.portal_preferences.getPreferredDeferredReportActivityFamily(),
tag=tag,
priority=priority,
)
if getattr(getattr(report_context, report_data['form_id']), 'pt', 'form_list') == 'report_view':
# erp5 report
report_active_context.Base_computeReportSection(
form=report_data['form_id'],
request_other=report_data['parameters'],
user_name=None,
tag=tag,
skin_name=report_data['skin_name'],
format=report_data.get('format', None),
priority=priority,
localizer_language=report_data['language'],
notify_report_complete_script_id='ERP5Site_finalizeAlarmReportDocumentGeneration',
notify_report_complete_kwargs=notify_report_complete_kwargs,
)
else:
# simple view
params = {}
if 'format' in report_data:
params['format'] = report_data['format']
report_active_context.Base_renderSimpleView(
localizer_language=report_data['language'],
skin_name=report_data['skin_name'],
request_form=report_data['parameters'],
deferred_style_dialog_method=report_data['form_id'],
user_name=None,
params=params,
notify_report_complete_script_id='ERP5Site_finalizeAlarmReportDocumentGeneration',
notify_report_complete_kwargs=notify_report_complete_kwargs,
)
<?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>tag, fixit=False, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Alarm_generateReportDocumentList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ERP5 Form" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Base_edit</string> </value>
</item>
<item>
<key> <string>action_title</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>edit_order</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>enctype</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<list>
<string>left</string>
<string>right</string>
<string>center</string>
<string>bottom</string>
<string>hidden</string>
</list>
</value>
</item>
<item>
<key> <string>groups</string> </key>
<value>
<dictionary>
<item>
<key> <string>bottom</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>center</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>hidden</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>left</string> </key>
<value>
<list>
<string>my_report_configuration_script_id</string>
</list>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Alarm_viewGenerateReportDocumentConfiguration</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Alarm_viewGenerateReportDocumenConfiguration</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>ERP5 Form</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_view</string> </value>
</item>
<item>
<key> <string>row_length</string> </key>
<value> <int>4</int> </value>
</item>
<item>
<key> <string>stored_encoding</string> </key>
<value> <string>UTF-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Configuration</string> </value>
</item>
<item>
<key> <string>unicode_mode</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>update_action</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>update_action_title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>description</string>
<string>display_width</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_report_configuration_script_id</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>description</string> </key>
<value> <string>ID of a script returning the report configuration. See Alarm_generateReportDocumentList for the exact API</string> </value>
</item>
<item>
<key> <string>display_width</string> </key>
<value> <int>20</int> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_string_field</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Report Configuration Script ID</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# pylint:disable=redefined-builtin
portal = context.getPortalObject()
assert alarm_relative_url
alarm = portal.restrictedTraverse(alarm_relative_url)
assert callback_script_id
callback = getattr(alarm, callback_script_id)
callback(
subject=subject,
attachment_list=attachment_list,
**callback_script_kwargs
)
<?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>user_name, subject, message, attachment_list, format, alarm_relative_url, callback_script_id, callback_script_kwargs</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_finalizeAlarmReportDocumentGeneration</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
2021-02-04 Jérome
* Alarm to automate report creation
2009-09-12 Jérome
* Allow rendering of any form / printout in deferred mode
\ No newline at end of file
portal_alarms/template_report_alarm
\ No newline at end of file
......@@ -27,6 +27,7 @@
##############################################################################
import unittest
import textwrap
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import createZODBPythonScript
from Testing import ZopeTestCase
......@@ -398,8 +399,101 @@ class TestODTDeferredStyle(TestDeferredStyleBase):
portal_type = "Text"
class TestDeferredReportAlarm(DeferredStyleTestCase):
def getBusinessTemplateList(self):
return super(TestDeferredReportAlarm, self).getBusinessTemplateList() + (
'erp5_pdm',
'erp5_simulation',
'erp5_trade',
'erp5_accounting',
'erp5_knowledge_pad',
'erp5_web',
'erp5_ingestion',
'erp5_ingestion_mysql_innodb_catalog',
'erp5_dms',
)
def test_alarm(self):
# create some data for reports
self.portal.person_module.newContent(portal_type='Person', first_name="not_included")
self.portal.person_module.newContent(portal_type='Person', first_name="yes_included").validate()
# make a script to configure the reports. Once reports are finished, this script
# will save in document module.
report_configuration_script_id = 'Alarm_getTestReportList{}'.format(self.id())
createZODBPythonScript(
self.portal.portal_skins.custom,
report_configuration_script_id,
'',
textwrap.dedent(
'''\
# coding: utf-8
portal = context.getPortalObject()
def makeCallbackFunction(report_title, report_reference):
def done(subject, attachment_list):
for attachment in attachment_list:
document = portal.portal_contributions.newContent(
data=attachment['content'],
filename=attachment['name'],
title=report_title,
reference=report_reference
)
document.share()
return done
report_data_list = [
{
'form_id': 'PersonModule_viewPersonList',
'context': portal.person_module,
'parameters': {
'validation_state': 'validated',
},
'skin_name': 'ODS',
'language': 'fr',
'format': 'txt',
'done': makeCallbackFunction('Persons %s' % DateTime(), 'TEST-Persons.Report')
},
{
'form_id': 'AccountModule_viewTrialBalanceReport',
'context': portal.accounting_module,
'parameters': {
'from_date': DateTime(2021, 1, 1),
'at_date': DateTime(2021, 12, 31),
'section_category': 'group',
'section_category_strict': False,
'simulation_state': ['delivered'],
'show_empty_accounts': True,
'expand_accounts': False,
'per_account_class_summary': False,
'show_detailed_balance_columns': False,
},
'skin_name': 'ODS',
'language': 'fr',
'done': makeCallbackFunction('Trial Balance %s' % DateTime(), 'TEST-Trial.Balance.Report')
}
]
return report_data_list
'''))
alarm = self.portal.portal_alarms.template_report_alarm.Base_createCloneDocument(batch_mode=True)
alarm.edit(
report_configuration_script_id=report_configuration_script_id
)
alarm.activeSense()
self.tic()
person_report, = self.portal.portal_catalog.getDocumentValueList(reference='TEST-Persons.Report')
self.assertIn('yes_included', person_report.getTextContent())
self.assertNotIn('not_included', person_report.getTextContent())
trial_balance_report, = self.portal.portal_catalog.getDocumentValueList(reference='TEST-Trial.Balance.Report')
self.assertEqual(trial_balance_report.getPortalType(), 'Spreadsheet')
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestODSDeferredStyle))
suite.addTest(unittest.makeSuite(TestODTDeferredStyle))
suite.addTest(unittest.makeSuite(TestDeferredReportAlarm))
return suite
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