Commit 3b600793 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 2ed3f5fb
Pipeline #14510 failed with stage
<?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,141 @@ 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
report_configuration_script_id = 'Alarm_getTestReportList{}'.format(self.id())
report_after_generation_script_id = 'Alarm_afterReportGenerated{}'.format(self.id())
createZODBPythonScript(
self.portal.portal_skins.custom,
report_configuration_script_id,
'',
textwrap.dedent(
'''\
# coding: utf-8
portal = context.getPortalObject()
report_data_list = [
# For the first two reports, we us included script:
# Alarm_contributeAndShareReportDocument
# which creates a document in document module.
# First with person module view
{
'form_id': 'PersonModule_viewPersonList',
'context': portal.person_module,
'parameters': {
'validation_state': 'validated',
},
'skin_name': 'ODS',
'language': 'fr',
'format': 'txt',
'callback_script_id': 'Alarm_contributeAndShareReportDocument',
'callback_script_kwargs': {
'title': 'Persons {}'.format(DateTime()),
'reference': 'TEST-Persons.Report',
'language': 'fr',
},
},
# Then with an accounting report (which uses report_view and a different
# approach to generate 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',
'callback_script_id': 'Alarm_contributeAndShareReportDocument',
'callback_script_kwargs': {
'title': 'Trial Balance {}'.format(DateTime()),
'reference': 'TEST-Trial.Balance.Report',
'language': 'fr',
},
},
# then another report to verify the callback script protocol.
{
'form_id': 'Person_view',
'context': portal.person_module.contentValues()[0],
'skin_name': 'ODS',
'language': portal.Localizer.get_default_language(),
'parameters': {},
'callback_script_id': '%s',
'callback_script_kwargs': {
'foo': 'bar'
},
},
]
return report_data_list
''' % report_after_generation_script_id))
createZODBPythonScript(
self.portal.portal_skins.custom,
report_after_generation_script_id,
'subject, attachment_list, foo',
textwrap.dedent(
'''\
context.setTitle('after script called with foo=' + foo)
'''
))
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()
# the first two reports are created
person_report, = self.portal.portal_catalog.getDocumentValueList(
reference='TEST-Persons.Report',
language='fr',
)
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',
language='fr',
)
self.assertEqual(trial_balance_report.getPortalType(), 'Spreadsheet')
# the third report, used to check the callback script protocol, modified the alarm title
self.assertEqual(alarm.getTitle(), 'after script called with foo=bar')
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