Commit 424cd467 authored by Romain Courteaud's avatar Romain Courteaud

slapos_*: new portal type Subscription Change Request

Like a Subscription Request, a Subscription Change Request is used to create a new Open Sale Order,
but, it also archive a previous one at the same time.

The purpose is to change some parameters of an Open Sale Order,
and correctly created compensation movement, to ensure the resource
stock is correct.

Stop using discount resource, and reduce the quantity of the consumed software product / service

Only Sale people can create Subscription Change Request.

TODO: check how quantity precision can be defined

The only use case supported for now is changing an Instance Tree's user.
parent 239f684f
......@@ -19,7 +19,7 @@ for open_order_line in open_sale_order.contentValues(
if item is None:
raise AssertionError('No matching item on: %s' % open_order_cell.getRelativeUrl())
if item.getValidationState() not in ['invalidated', 'archived']:
if check_unused_item and (item.getValidationState() not in ['invalidated', 'archived']):
# Do not touch if the item is not clean yet
return
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<value> <string>check_unused_item=True</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
......@@ -22,7 +22,8 @@ portal.portal_catalog.searchAndActivate(
# Only Packing List created to generate deposit Invoice need to be expanded
'Payment Transaction',
# Discount Sale Packing List
'Subscription Request'
'Subscription Request',
'Subscription Change Request'
],
**kw
)
......
......@@ -47,7 +47,7 @@ class TestSlapOSAccountingScenario(TestSlapOSVirtualMasterScenarioMixin):
self.assertTrue(owner_person.Entity_hasOutstandingAmount(include_planned=True))
amount_list = owner_person.Entity_getOutstandingAmountList(include_planned=True)
self.assertEqual(len(amount_list), 1)
self.assertEqual(amount_list[0].total_price, 24.384)
self.assertAlmostEqual(amount_list[0].total_price, 24.192)
self.assertFalse(owner_person.Entity_hasOutstandingAmount())
self.assertEqual(subscription_request.getSimulationState(), "invalidated")
open_sale_order = self.portal.portal_catalog.getResultValue(
......@@ -69,7 +69,7 @@ class TestSlapOSAccountingScenario(TestSlapOSVirtualMasterScenarioMixin):
self.assertEqual(first_invoice.getStartDate(), DateTime('2021/03/19'))
self.assertEqual(first_invoice.getStopDate(), DateTime('2021/04/19'))
# Discount and first subscription
self.assertEqual(first_invoice.getTotalPrice(), 24.384)
self.assertAlmostEqual(first_invoice.getTotalPrice(), 24.192)
# Ensure no unexpected object has been created
# 1 accounting transaction
# 1 open order
......@@ -87,11 +87,11 @@ class TestSlapOSAccountingScenario(TestSlapOSVirtualMasterScenarioMixin):
self.assertTrue(owner_person.Entity_hasOutstandingAmount(include_planned=True))
amount_list = owner_person.Entity_getOutstandingAmountList(include_planned=True)
self.assertEqual(len(amount_list), 1)
self.assertEqual(amount_list[0].total_price, 175.584)
self.assertAlmostEqual(amount_list[0].total_price, 175.392)
self.assertTrue(owner_person.Entity_hasOutstandingAmount())
amount_list = owner_person.Entity_getOutstandingAmountList()
self.assertEqual(len(amount_list), 1)
self.assertEqual(amount_list[0].total_price, 125.184)
self.assertAlmostEqual(amount_list[0].total_price, 124.992)
self.assertEqual(first_invoice.getSimulationState(), "stopped")
# Ensure no unexpected object has been created
# 4 accounting transactions
......@@ -116,7 +116,7 @@ class TestSlapOSAccountingScenario(TestSlapOSVirtualMasterScenarioMixin):
)
)
payment_transaction.stop()
self.assertEqual(payment_transaction.AccountingTransaction_getTotalCredit(), 74.78399999999999)
self.assertAlmostEqual(payment_transaction.AccountingTransaction_getTotalCredit(), 74.592)
self.tic()
self.assertTrue(owner_person.Entity_hasOutstandingAmount(include_planned=True))
amount_list = owner_person.Entity_getOutstandingAmountList(include_planned=True)
......
......@@ -231,6 +231,7 @@ class TestSlapOSConfigurator(SlapOSTestCaseMixin):
'software_product_module',
'software_publication_module',
'software_release_module',
'subscription_change_request_module',
'subscription_request_module',
'support_request_module',
'system_event_module',
......
<local_roles_item>
<local_roles>
<role id='F-SALE*'>
<item>Auditor</item>
<item>Author</item>
</role>
</local_roles>
<local_role_group_ids>
<local_role_group_id id='function'>
<principal id='F-SALE*'>Auditor</principal>
<principal id='F-SALE*'>Author</principal>
</local_role_group_id>
</local_role_group_ids>
</local_roles_item>
\ No newline at end of file
<type_roles>
<role id='Author; Auditor'>
<property id='title'>Sale</property>
<multi_property id='categories'>local_role_group/function</multi_property>
<multi_property id='category'>function/sale*</multi_property>
<multi_property id='base_category'>function</multi_property>
</role>
</type_roles>
\ No newline at end of file
<type_roles>
<role id='Auditor'>
<property id='title'>Sale Agent</property>
<multi_property id='categories'>local_role_group/function</multi_property>
<multi_property id='category'>function/sale/agent</multi_property>
<multi_property id='base_category'>function</multi_property>
</role>
<role id='Auditor'>
<property id='title'>Sale Manager</property>
<multi_property id='categories'>local_role_group/function</multi_property>
<multi_property id='category'>function/sale/manager</multi_property>
<multi_property id='base_category'>function</multi_property>
</role>
</type_roles>
\ No newline at end of file
......@@ -40,6 +40,7 @@ for business_application_id, module_name_list in [
"software_installation_module",
"software_instance_module",
"subscription_request_module",
"subscription_change_request_module",
]],
["crm", [ # Customer Relation Management
"campaign_module",
......
......@@ -1642,6 +1642,26 @@ class TestSubscriptionRequest(TestSlapOSGroupRoleSecurityMixin):
self.assertRoles(delivery, person.getUserId(), ['Associate'])
self.assertRoles(delivery, 'R-SHADOW-PERSON', ['Auditor'])
class TestSubscriptionChangeRequestModule(TestSlapOSGroupRoleSecurityMixin):
def test_SubscriptionChangeRequestModule(self):
module = self.portal.subscription_change_request_module
self.assertSecurityGroup(module,
['F-SALE*', module.Base_getOwnerId()], False)
self.assertRoles(module, 'F-SALE*', ['Auditor', 'Author'])
self.assertRoles(module, module.Base_getOwnerId(), ['Owner'])
class TestSubscriptionChangeRequest(TestSlapOSGroupRoleSecurityMixin):
def test_SubscriptionChangeRequest_default(self):
delivery = self.portal.subscription_change_request_module.newContent(
portal_type='Subscription Change Request')
self.assertSecurityGroup(delivery,
[self.user_id, 'F-SALEAGT', 'F-SALEMAN'], False)
self.assertRoles(delivery, self.user_id, ['Owner'])
self.assertRoles(delivery, 'F-SALEAGT', ['Auditor'])
self.assertRoles(delivery, 'F-SALEMAN', ['Auditor'])
class TestOrganisationModule(TestSlapOSGroupRoleSecurityMixin):
def test_OrganisationModule(self):
module = self.portal.organisation_module
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2022 Nexedi SA and Contributors. All Rights Reserved.
#
##############################################################################
from erp5.component.test.testSlapOSERP5VirtualMasterScenario import TestSlapOSVirtualMasterScenarioMixin
from erp5.component.test.SlapOSTestCaseMixin import PinnedDateTime
from DateTime import DateTime
class TestSlapOSSubscriptionChangeRequestScenarioMixin(TestSlapOSVirtualMasterScenarioMixin):
pass
class TestSlapOSSubscriptionChangeRequestScenario(TestSlapOSSubscriptionChangeRequestScenarioMixin):
def test_subscription_change_request_change_instance_destination_without_accounting_scenario(self):
currency, _, _, sale_person = self.bootstrapVirtualMasterTest(is_virtual_master_accountable=False)
self.tic()
self.logout()
with PinnedDateTime(self, DateTime('2024/01/25')):
# lets join as slapos administrator, which will own few compute_nodes
owner_reference = 'owner-%s' % self.generateNewId()
self.joinSlapOS(self.web_site, owner_reference)
self.login()
owner_person = self.portal.portal_catalog.getResultValue(
portal_type="ERP5 Login",
reference=owner_reference).getParentValue()
#owner_person.setCareerSubordinationValue(seller_organisation)
self.tic()
# hooray, now it is time to create compute_nodes
self.logout()
self.login(sale_person.getUserId())
# create a default project
project_relative_url = self.addProject(person=owner_person, currency=currency)
self.logout()
self.login()
project = self.portal.restrictedTraverse(project_relative_url)
preference = self.portal.portal_preferences.slapos_default_system_preference
preference.edit(
preferred_subscription_assignment_category_list=[
'function/customer',
'role/client',
'destination_project/%s' % project.getRelativeUrl()
]
)
self.tic()
self.logout()
self.login(owner_person.getUserId())
# and install some software on them
public_server_software = self.generateNewSoftwareReleaseUrl()
public_instance_type = 'public type'
self.addSoftwareProduct(
"instance product", project, public_server_software, public_instance_type
)
# join as the another visitor and request software instance on public
# compute_node
self.logout()
with PinnedDateTime(self, DateTime('2024/01/29')):
public_reference = 'public-%s' % self.generateNewId()
self.joinSlapOS(self.web_site, public_reference)
with PinnedDateTime(self, DateTime('2024/02/01')):
public_reference2 = 'public2-%s' % self.generateNewId()
self.joinSlapOS(self.web_site, public_reference2)
self.login()
public_person = self.portal.portal_catalog.getResultValue(
portal_type="ERP5 Login",
reference=public_reference).getParentValue()
public_person2 = self.portal.portal_catalog.getResultValue(
portal_type="ERP5 Login",
reference=public_reference2).getParentValue()
person_user_id = public_person.getUserId()
software_release = public_server_software
software_type = public_instance_type
project_reference = project.getReference()
public_instance_title = 'Public title %s' % self.generateNewId()
self.login(person_user_id)
with PinnedDateTime(self, DateTime('2024/02/10')):
self.personRequestInstanceNotReady(
software_release=software_release,
software_type=software_type,
partition_reference=public_instance_title,
project_reference=project_reference
)
self.tic()
# XXX search only for this user
instance_tree = self.portal.portal_catalog.getResultValue(
portal_type="Instance Tree",
title=public_instance_title,
follow_up__reference=project_reference
)
self.checkServiceSubscriptionRequest(instance_tree)
self.tic()
with PinnedDateTime(self, DateTime('2024/02/25')):
self.login(sale_person.getUserId())
subscription_change_request = public_person2.Person_claimSlaposItemSubscription(
instance_tree.getReference(),
None
)
self.tic()
self.logout()
self.login()
self.assertEquals(instance_tree.getDestinationSection(),
public_person2.getRelativeUrl())
# Total of quantity should be zero
inventory_list_kw = {
'group_by_section': False,
'group_by_node': False,
'group_by_variation': False,
'group_by_resource': True,
'resource_uid': subscription_change_request.getResourceUid(),
}
inventory_list = self.portal.portal_simulation.getCurrentInventoryList(**inventory_list_kw)
self.assertEquals(1, len(inventory_list))
self.assertEquals(0, inventory_list[0].total_quantity)
# Seller only sold 1 month
inventory_list_kw = {
'node_uid': subscription_change_request.getSourceUid(),
'group_by_section': False,
'group_by_node': False,
'group_by_variation': False,
'group_by_resource': True,
'resource_uid': subscription_change_request.getResourceUid(),
}
inventory_list = self.portal.portal_simulation.getCurrentInventoryList(**inventory_list_kw)
self.assertEquals(1, len(inventory_list))
# 1 - 0.42 (13 days of 31) - 0.1 (3 days of 31) + 1 - 0.83 (24 days of 29)
self.assertAlmostEquals(-0.65, inventory_list[0].total_quantity)
inventory_list_kw = {
'group_by_section': False,
'group_by_node': True,
'group_by_variation': True,
'resource_uid': subscription_change_request.getResourceUid(),
}
inventory_list = self.portal.portal_simulation.getCurrentInventoryList(**inventory_list_kw)
self.assertEquals(3, len(inventory_list))
# tracking_list = instance_tree.Item_getTrackingList()
# self.assertEquals(2, len(tracking_list))
# XXX TODO self.assertEquals(None, self.portal.portal_simulation.getInventoryList())
# Ensure no unexpected object has been created
# 2 credential request
# 1 instance tree
# 7 open sale order
# 4 assignment
# 3 simulation movement
# 6 sale packing list / line
# 2 sale trade condition ( a 3rd trade condition is not linked to the project)
# 1 software instance
# 1 software product
# 1 subscription change request
# 2 subscription request
self.assertRelatedObjectCount(project, 30)
with PinnedDateTime(self, DateTime('2024/02/15')):
self.checkERP5StateBeforeExit()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testSlapOSERP5SubscriptionChangeRequestScenario</string> </value>
</item>
<item>
<key> <string>default_source_reference</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testSlapOSERP5SubscriptionChangeRequestScenario</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -75,6 +75,7 @@ service_module/zero_emission_ratio
software_installation_module
software_instance_module
software_product_module
subscription_change_request_module
subscription_request_module
support_request_module
system_event_module
......
......@@ -87,6 +87,8 @@ Software Instance
Software Instance Module
Software Product
Software Product Module
Subscription Change Request
Subscription Change Request Module
Subscription Request
Subscription Request Module
Support Request
......
......@@ -7,6 +7,7 @@ test.erp5.testSlapOSERP5InteractionWorkflow
test.erp5.testSlapOSERP5LocalPermissionSlapOSInteractionWorkflow
test.erp5.testSlapOSERP5SiteDump
test.erp5.testSlapOSERP5SkinSelection
test.erp5.testSlapOSERP5SubscriptionChangeRequestScenario
test.erp5.testSlapOSERP5VirtualMasterScenario
test.erp5.testSlapOSPrecacheManifest
test.erp5.testSlapOSPrecacheManifest
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_jio_action</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_jio_action</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>claim_slapos_item_subscription</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>60.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Claim Service Subscription</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/Person_viewClaimSlaposItemSubscriptionDialog</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>python: portal.Base_checkPermission(\'subscription_change_request_module\', \'Add portal content\') and (context.getValidationState() == \'validated\')</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
portal = context.getPortalObject()
Base_translateString = portal.Base_translateString
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
batch = (dialog_id is None)
# Search for the matching item
sql_instance_list = portal.portal_catalog(
reference=reference,
# Project are not handled yet, as it must also move all compute node subscription at the same time
portal_type=['Instance Tree'],
validation_state='validated',
limit=2
)
if len(sql_instance_list) != 1:
keep_items = {
'your_reference': reference,
'portal_status_level': 'warning',
'portal_status_message': Base_translateString('Unknown reference')
}
if batch:
raise ValueError(keep_items['portal_status_message'] + str(len(sql_instance_list)))
return context.Base_renderForm(dialog_id, keep_items=keep_items)
# Search for the existing Open Sale Order
item = sql_instance_list[0].getObject()
open_sale_order_cell = portal.portal_catalog.getResultValue(
portal_type=['Open Sale Order Line', 'Open Sale Order Cell'],
aggregate__uid=item.getUid(),
validation_state='validated'
)
if open_sale_order_cell is None:
keep_items = {
'your_reference': reference,
'portal_status_level': 'warning',
'portal_status_message': Base_translateString('No Open Sale Order found')
}
if batch:
raise ValueError(keep_items['portal_status_message'])
return context.Base_renderForm(dialog_id, keep_items=keep_items)
open_sale_order = open_sale_order_cell.getParentValue()
while open_sale_order.getPortalType() != 'Open Sale Order':
open_sale_order = open_sale_order.getParentValue()
# Create the Subscription Change Request
subscription_change_request = open_sale_order_cell.getResourceValue().Resource_createSubscriptionRequest(
context,
# [software_type, software_release],
open_sale_order_cell.getVariationCategoryList(),
open_sale_order.getSourceProjectValue(),
currency_value=open_sale_order.getPriceCurrencyValue(),
portal_type='Subscription Change Request',
item_value=item,
causality_value=open_sale_order
)
if batch:
return subscription_change_request
return subscription_change_request.Base_redirect()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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>reference, dialog_id, activate_kw=None, REQUEST=None, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Person_claimSlaposItemSubscription</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>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>action</string> </key>
<value> <string>Person_claimSlaposItemSubscription</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>your_reference</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>Person_viewClaimSlaposItemSubscriptionDialog</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>Person_viewClaimSlaposItemSubscriptionDialog</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_dialog</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>Claim Subscription</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>required</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>your_reference</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>field_id</string> </key>
<value> <string>my_dialog_mode_reference</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>required</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -34,6 +34,7 @@ Instance Tree | slapos_request_hs_stop
Instance Tree | slapos_view_request_destroy
Instance Tree | update_slapos_parameter
Mail Message | slapos_panel_view
Person | claim_slapos_item_subscription
Person | create_slapos_project
Person | slapos_panel_view
Preference | slapos_panel_view
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_list</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_list</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/SubscriptionChangeRequestModule_viewSubscriptionChangeRequestList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ActionInformation" module="Products.CMFCore.ActionInformation"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>action</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>action_type/object_view</string>
</tuple>
</value>
</item>
<item>
<key> <string>category</string> </key>
<value> <string>object_view</string> </value>
</item>
<item>
<key> <string>condition</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>icon</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>view</string> </value>
</item>
<item>
<key> <string>permissions</string> </key>
<value>
<tuple>
<string>View</string>
</tuple>
</value>
</item>
<item>
<key> <string>priority</string> </key>
<value> <float>1.0</float> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>View</string> </value>
</item>
<item>
<key> <string>visible</string> </key>
<value> <int>1</int> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Expression" module="Products.CMFCore.Expression"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>text</string> </key>
<value> <string>string:${object_url}/SubscriptionChangeRequest_view</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<module>
<id>subscription_change_request_module</id>
<permission_list>
<permission type='tuple'>
<name>Access Transient Objects</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Access contents information</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Access session data</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Add portal content</name>
<role>Assignor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Add portal folders</name>
<role>Assignor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Change local roles</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Copy or Move</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Delete objects</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>List folder contents</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>Modify portal content</name>
<role>Assignor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>View</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Manager</role>
</permission>
<permission type='tuple'>
<name>View History</name>
<role>Assignee</role>
<role>Assignor</role>
<role>Associate</role>
<role>Auditor</role>
<role>Author</role>
<role>Manager</role>
</permission>
</permission_list>
<portal_type>Subscription Change Request Module</portal_type>
<title>Subscription Change Requests</title>
</module>
\ No newline at end of file
<?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_validateSubmittedSubscriptionChangeRequest</string> </value>
</item>
<item>
<key> <string>automatic_solve</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>slapos_subscription_change_request_validate_submitted</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/>
</value>
</item>
<item>
<key> <string>periodicity_minute</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_minute_frequency</string> </key>
<value>
<none/>
</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="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="DateTime" module="DateTime.DateTime"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<tuple>
<float>1677632460.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>Validate submitted Subscription Change Request</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<allowed_content_type_list>
<portal_type id="Subscription Change Request Module">
<item>Subscription Change Request</item>
</portal_type>
<portal_type id="Subscription Request">
<item>Email</item>
</portal_type>
......
<base_category_list>
<portal_type id="Subscription Change Request Module">
<item>business_application</item>
</portal_type>
<portal_type id="Subscription Request">
<item>aggregate</item>
<item>causality</item>
......
<property_sheet_list>
<portal_type id="Subscription Change Request">
<item>Amount</item>
<item>CredentialRequest</item>
<item>DublinCore</item>
<item>InstanceTree</item>
<item>Person</item>
<item>Price</item>
<item>Reference</item>
<item>SlapOSSaleProfileConstraint</item>
<item>SlapOSSubscriptionRequestConstraint</item>
<item>Task</item>
<item>Url</item>
<item>VariationRange</item>
</portal_type>
<portal_type id="Subscription Request">
<item>Amount</item>
<item>CredentialRequest</item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_property_domain_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>content_icon</string> </key>
<value> <string>folder_icon.gif</string> </value>
</item>
<item>
<key> <string>factory</string> </key>
<value> <string>addFolder</string> </value>
</item>
<item>
<key> <string>group_list</string> </key>
<value>
<tuple>
<string>module</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Subscription Change Request Module</string> </value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>Folder</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>short_title</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="TranslationInformation" module="Products.ERP5Type.TranslationProviderBase"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>domain_name</string> </key>
<value> <string>erp5_ui</string> </value>
</item>
<item>
<key> <string>property_name</string> </key>
<value> <string>short_title</string> </value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="TranslationInformation" module="Products.ERP5Type.TranslationProviderBase"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>domain_name</string> </key>
<value> <string>erp5_ui</string> </value>
</item>
<item>
<key> <string>property_name</string> </key>
<value> <string>title</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>factory</string> </key>
<value> <string>addXMLObject</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Subscription Change Request</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value> <string>SubscriptionChangeRequest_init</string> </value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>short_title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>CredentialRequest</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -27,6 +27,10 @@
<type>Software Product</type>
<workflow>slapos_subscription_request_interaction_workflow</workflow>
</chain>
<chain>
<type>Subscription Change Request</type>
<workflow>edit_workflow, slapos_subscription_request_interaction_workflow, subscription_request_workflow</workflow>
</chain>
<chain>
<type>Subscription Request</type>
<workflow>edit_workflow, slapos_subscription_request_interaction_workflow, subscription_request_workflow</workflow>
......
portal = context.getPortalObject()
portal.portal_catalog.searchAndActivate(
method_id='SubscriptionChangeRequest_validateIfSubmitted',
# Project are created only from UI for now
portal_type=["Subscription Change Request"],
simulation_state='submitted',
packet_size=1, # Separate calls to many transactions
activate_kw={'tag': tag}
)
context.activate(after_tag=tag).getId()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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, params</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Alarm_validateSubmittedSubscriptionChangeRequest</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -57,12 +57,11 @@ if destination_decision_value is None:
return
try:
subscription_request = service.Resource_createSubscriptionRequest(destination_decision_value, resource_vcl, project_value, currency_value=currency_value, default_price=default_price)
subscription_request = service.Resource_createSubscriptionRequest(destination_decision_value, resource_vcl, project_value, currency_value=currency_value, default_price=default_price,
item_value=item, causality_value=item)
except AssertionError as error:
storeWorkflowComment(item, str(error))
return
subscription_request.setAggregateValue(item)
subscription_request.setCausalityValue(item)
subscription_request.reindexObject(activate_kw=activate_kw)
item.reindexObject(activate_kw=activate_kw)
......
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
portal = context.getPortalObject()
open_sale_order_cell = context
hosting_subscription = open_sale_order_cell.getAggregateValue(portal_type='Hosting Subscription')
open_sale_order = open_sale_order_cell.getParentValue()
if open_sale_order_cell.getPortalType() == 'Open Sale Order Cell':
open_sale_order = open_sale_order.getParentValue()
start_date = open_sale_order.getStartDate()
next_period_date = hosting_subscription.getNextPeriodicalDate(current_date)
if open_sale_order.getValidationState() == 'validated':
unused_day_count = current_date - start_date
elif open_sale_order.getValidationState() == 'archived':
unused_day_count = next_period_date - current_date
else:
raise NotImplementedError('Unhandled open order state: %s' % open_sale_order.getValidationState())
sale_packing_list_edit_kw = dict(
title=title,
start_date=start_date,
# It should match the first open order invoice
stop_date=next_period_date,
specialise_value=open_sale_order.getSpecialiseValue(),
source_value=open_sale_order.getSourceValue(),
source_section_value=open_sale_order.getSourceSectionValue(),
source_decision_value=open_sale_order.getSourceDecisionValue(),
source_project_value=open_sale_order.getSourceProjectValue(),
destination_value=open_sale_order.getDestinationValue(),
destination_section_value=open_sale_order.getDestinationSectionValue(),
destination_decision_value=open_sale_order.getDestinationDecisionValue(),
destination_project_value=open_sale_order.getDestinationProjectValue(),
ledger_value=open_sale_order.getLedgerValue(),
causality_value=causality_value,
price_currency_value=open_sale_order.getPriceCurrencyValue(),
activate_kw=activate_kw
)
if (0 < unused_day_count):
# If the open order starts before today,
# generate a discount to the user on his next invoice
# and reduce the stock consumption
sale_packing_list = portal.sale_packing_list_module.newContent(
portal_type="Sale Packing List",
comment="%s unused days of %s" % (unused_day_count, next_period_date-start_date),
**sale_packing_list_edit_kw
)
variation_category_list = open_sale_order_cell.getVariationCategoryList()
sale_packing_list_line = sale_packing_list.newContent(
portal_type="Sale Packing List Line",
resource_value=open_sale_order_cell.getResourceValue(),
variation_category_list=variation_category_list,
quantity_unit_value=open_sale_order_cell.getQuantityUnitValue(),
base_contribution_list=open_sale_order_cell.getResourceValue().getBaseContributionList(),
use=open_sale_order_cell.getResourceValue().getUse(),
activate_kw=activate_kw
)
if variation_category_list:
base_id = 'movement'
cell_key = list(sale_packing_list_line.getCellKeyList(base_id=base_id))[0]
sale_packing_list_cell = sale_packing_list_line.newCell(
base_id=base_id,
portal_type="Sale Packing List Cell",
*cell_key
)
sale_packing_list_cell.edit(
mapped_value_property_list=['price','quantity'],
predicate_category_list=cell_key,
variation_category_list=cell_key,
activate_kw=activate_kw
)
else:
sale_packing_list_cell = sale_packing_list_line
quantity = open_sale_order_cell.getQuantity() * (unused_day_count / (next_period_date - start_date))
# precision = context.getQuantityPrecisionFromResource(open_sale_order_cell.getResourceValue())
# XXX Stock does not seem to use quantity unit precision...
precision = 2
# Use currency precision to reduce the float length
quantity = float(('%%0.%sf' % precision) % quantity)
aggregate_value_list = [x for x in open_sale_order_cell.getAggregateValue() if (x.getPortalType() != 'Hosting Subscription')]
sale_packing_list_cell.edit(
# Quantity is negative, to reduce the stock of the consumed product
quantity=-quantity,
price=open_sale_order_cell.getPrice(),
aggregate_value_list=aggregate_value_list,
activate_kw=activate_kw
)
sale_packing_list.Delivery_fixBaseContributionTaxableRate()
sale_packing_list.Base_checkConsistency()
sale_packing_list.confirm()
sale_packing_list.stop()
sale_packing_list.deliver()
return sale_packing_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>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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>current_date, title, causality_value, activate_kw=None, REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>OpenSaleOrderCell_createDiscountSalePackingList</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -7,12 +7,14 @@ if subscriber_person_value is None:
source_project_value = None
destination_project_value = None
trade_condition_type = None
item = None
if resource.getPortalType() == "Software Product":
source_project_value = project_value
trade_condition_type = "instance_tree"
assert item_value is not None
assert item_value.getPortalType() == 'Instance Tree'
elif resource.getPortalType() == "Service":
if resource.getRelativeUrl() == "service_module/slapos_compute_node_subscription":
if project_value is None:
......@@ -20,10 +22,15 @@ elif resource.getPortalType() == "Service":
source_project_value = project_value
trade_condition_type = "compute_node"
assert item_value is not None
assert item_value.getPortalType() == 'Compute Node'
elif resource.getRelativeUrl() == "service_module/slapos_virtual_master_subscription":
if project_value is None:
raise AssertionError('Project is required for %s %s' % (resource.getRelativeUrl(), project_value))
item = project_value
assert item_value is None
item_value = project_value
trade_condition_type = "virtual_master"
else:
raise NotImplementedError('Unsupported resource: %s' % resource.getRelativeUrl())
......@@ -110,8 +117,8 @@ else:
if not price:
raise AssertionError('Can not find a price to generate the Subscription Request (%s)' % tmp_sale_order.getSpecialiseValue())
subscription_request = portal.subscription_request_module.newContent(
portal_type='Subscription Request',
subscription_request = portal.getDefaultModuleValue(portal_type).newContent(
portal_type=portal_type,
temp_object=temp_object,
destination_value=subscriber_person_value,
# Do not set a default destination section if it is not defined on a trade condition
......@@ -128,7 +135,7 @@ subscription_request = portal.subscription_request_module.newContent(
effective_date=now,
resource_value=resource,
variation_category_list=variation_category_list,
aggregate_value=item,
aggregate_value=item_value,
quantity_unit=resource.getQuantityUnit(),
quantity=1,
ledger="automated",
......@@ -139,6 +146,7 @@ subscription_request = portal.subscription_request_module.newContent(
price_currency=tmp_sale_order.getPriceCurrency(),
price=price,
# XXX activate_kw=activate_kw
causality_value=causality_value,
)
if temp_object:
subscription_request.edit(reference="foo")
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>subscriber_person_value, variation_category_list, project_value, currency_value=None, default_price=None, temp_object=False</string> </value>
<value> <string>subscriber_person_value, variation_category_list, project_value, currency_value=None, default_price=None, temp_object=False, portal_type=\'Subscription Request\', item_value=None, causality_value=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
<?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_doSelect</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>
<string>listbox</string>
</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/>
</value>
</item>
<item>
<key> <string>right</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SubscriptionChangeRequestModule_viewSubscriptionChangeRequestList</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>SubscriptionChangeRequestModule_viewSubscriptionChangeRequestList</string> </value>
</item>
<item>
<key> <string>pt</string> </key>
<value> <string>form_list</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>Subscription Change Requests</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>portal_type</string>
<string>selection_name</string>
<string>title</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>listbox</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>field_id</string> </key>
<value> <string>my_list_mode_listbox</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value>
<list>
<tuple>
<string>Subscription Change Request</string>
<string>Subscription Change Request</string>
</tuple>
</list>
</value>
</item>
<item>
<key> <string>selection_name</string> </key>
<value> <string>subscription_change_request_module_selection</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Subscription Change Requests</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
portal = context.getPortalObject()
reference = "SUBCHREQ-%s" % portal.portal_ids.generateNewId(
id_group='slap_subscription_change_request_reference',
id_generator='uid')
context.edit(reference=reference)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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>**kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SubscriptionChangeRequest_init</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from erp5.component.module.DateUtils import getClosestDate
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
subscription_change_request = context
assert subscription_change_request.getPortalType() == 'Subscription Change Request'
assert subscription_change_request.getSimulationState() == 'submitted'
def invalidate(document, comment):
context.validate()
context.invalidate(comment=comment)
# Subscription Change Request will change an ongoing Open Sale Order
open_sale_order = subscription_change_request.getCausalityValue(portal_type='Open Sale Order')
if (open_sale_order is None) or (open_sale_order.getValidationState() != 'validated'):
return invalidate(subscription_change_request, 'No Open Sale Order to update')
# Search the line/cell
open_order_movement = None
open_order_movement_list = open_sale_order.contentValues(portal_type='Open Sale Order Line')
if len(open_order_movement_list) == 1:
open_order_movement = open_order_movement_list[0]
open_order_movement_list = open_order_movement.contentValues(portal_type='Open Sale Order Cell')
if 1 < len(open_order_movement_list):
open_order_movement = None
elif 1 == len(open_order_movement_list):
open_order_movement = open_order_movement_list[0]
if open_order_movement is None:
return invalidate(subscription_change_request, 'Can not find the open order movement')
identical_order_base_category_list = [
'specialise',
# 'destination',
# 'destination_section',
# 'destination_decisition',
'destination_project',
'source',
'source_section',
'source_project',
'price_currency',
'resource',
'variation_category_list',
'quantity_unit',
'quantity',
'price'
]
for identical_order_base_category in identical_order_base_category_list:
if open_order_movement.getProperty(identical_order_base_category) != subscription_change_request.getProperty(identical_order_base_category):
return invalidate(subscription_change_request, 'Unhandled requested changes on: %s' % identical_order_base_category)
# Ensure the subscribed item is the same
subscribed_item = open_order_movement.getAggregateValue(portal_type=['Instance Tree', 'Compute Node', 'Project'])
if subscription_change_request.getAggregateUid() != subscribed_item.getUid():
return invalidate(subscription_change_request, 'Unhandled requested changes on: aggregate')
# Ensure destination is different
if subscription_change_request.getDestination() == open_sale_order.getDestination():
return invalidate(subscription_change_request, 'Expected change on: destination')
# Create new Open Sale Order
next_open_sale_order = subscription_change_request.SubscriptionRequest_createOpenSaleOrder()
current_date = getClosestDate(target_date=next_open_sale_order.getCreationDate(), precision='day')
# XXX Compensation
open_sale_order.OpenSaleOrder_archiveIfUnusedItem(check_unused_item=False)
# if we want to always activate a discount as soon as an open order is archived (outside subscription change request)
# it is needed to call OpenSaleOrderCell_createDiscountSalePackingList is an interaction workflow
# with more extra checks.
open_order_movement.OpenSaleOrderCell_createDiscountSalePackingList(
current_date,
'transfer discount from %s to %s' % (open_sale_order.getReference(), next_open_sale_order.getReference()),
subscription_change_request
)#, activate_kw=activate_kw)
# Change Subscripted Item user if needed
subscribed_item = open_order_movement.getAggregateValue(portal_type=['Instance Tree', 'Compute Node', 'Project'])
if subscribed_item is None:
raise NotImplementedError('Unsupported subscribed item')
elif subscribed_item.getPortalType() == 'Compute Node':
# No user is set on Compute Node
pass
elif subscribed_item.getPortalType() == 'Instance Tree':
subscribed_item.edit(destination_section=subscription_change_request.getDestination())
elif subscribed_item.getPortalType() == 'Project':
subscribed_item.edit(destination=subscription_change_request.getDestination())
else:
raise NotImplementedError('Not implemented subscribed item')
return invalidate(subscription_change_request, 'New open order: %s' % next_open_sale_order.getRelativeUrl())
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SubscriptionChangeRequest_validateIfSubmitted</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_title</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>SubscriptionChangeRequest_view</string> </value>
</item>
<item>
<key> <string>method</string> </key>
<value> <string>POST</string> </value>
</item>
<item>
<key> <string>name</string> </key>
<value> <string>SubscriptionChangeRequest_view</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>Subscription Change Request</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>
from erp5.component.module.DateUtils import getClosestDate, addToDate
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
portal = context.getPortalObject()
subscription_request = context
......@@ -126,38 +130,11 @@ open_sale_order.validate()
#######################################################
# Discount
unused_day_count = current_date - start_date
if (subscription_request.getPrice() != 0) and (0 < unused_day_count):
# If the open order starts before today,
# generate a discount to the user on his next invoice
open_order_edit_kw['title'] = "first invoice discount for %s" % open_sale_order.getReference()
sale_packing_list = portal.sale_packing_list_module.newContent(
portal_type="Sale Packing List",
# It should match the first open order invoice
stop_date=next_period_date,
**open_order_edit_kw
)
price = -subscription_request.getPrice() * (unused_day_count / (next_period_date - start_date))
precision = context.getQuantityPrecisionFromResource(subscription_request.getPriceCurrencyValue())
# Use currency precision to reduce the float length
price = float(('%%0.%sf' % precision) % price)
discount_service = portal.restrictedTraverse('service_module/slapos_discount')
sale_packing_list.newContent(
portal_type="Sale Packing List Line",
resource_value=discount_service,
# Use a quantity of 1 to be able to count how many discount were distributed
quantity=1,
price=price,
quantity_unit_value=discount_service.getQuantityUnitValue(),
base_contribution_list=discount_service.getBaseContributionList(),
use=discount_service.getUse(),
activate_kw=activate_kw
)
sale_packing_list.Delivery_fixBaseContributionTaxableRate()
sale_packing_list.Base_checkConsistency()
sale_packing_list.confirm()
sale_packing_list.stop()
sale_packing_list.deliver()
open_order_cell.OpenSaleOrderCell_createDiscountSalePackingList(
current_date,
"first invoice discount for %s" % open_sale_order.getReference(),
subscription_request,
activate_kw=activate_kw
)
return open_sale_order
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
subscription_request = context
portal = context.getPortalObject()
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
<value> <string>REQUEST=None</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Interaction Workflow Interaction" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>categories</string> </key>
<value>
<tuple>
<string>after_script/portal_workflow/slapos_subscription_request_interaction_workflow/script_Base_triggerSubscriptionChangeRequestValidationAlarm</string>
</tuple>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>interaction_SubscriptionChangeRequest_submit</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Interaction Workflow Interaction</string> </value>
</item>
<item>
<key> <string>portal_type_filter</string> </key>
<value>
<tuple>
<string>Subscription Change Request</string>
</tuple>
</value>
</item>
<item>
<key> <string>portal_type_group_filter</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>temporary_document_disallowed</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>trigger_method_id</string> </key>
<value>
<tuple>
<string>submit</string>
</tuple>
</value>
</item>
<item>
<key> <string>trigger_once_per_transaction</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>trigger_type</string> </key>
<value> <int>2</int> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Workflow Script" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</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>state_change</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>script_Base_triggerSubscriptionChangeRequestValidationAlarm</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Workflow Script</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value>
<none/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Instance Tree | jump_to_related_subscription_request
Person | jump_to_subscription_request
Sale Invoice Transaction | jump_to_related_subscription_request
Subscription Change Request Module | view
Subscription Change Request | view
Subscription Request Module | view
Subscription Request | view
\ No newline at end of file
subscription_change_request_module
subscription_request_module
\ No newline at end of file
portal_alarms/slapos_subscription_change_request_validate_submitted
portal_alarms/slapos_subscription_request_create_from_orphaned_item
portal_alarms/slapos_subscription_request_validate_submitted
service_module/slapos_reservation_fee_2
\ No newline at end of file
Subscription Change Request Module | Subscription Change Request
Subscription Request Module | Subscription Request
Subscription Request | Email
\ No newline at end of file
Subscription Change Request Module | business_application
Subscription Request Module | business_application
Subscription Request | aggregate
Subscription Request | causality
......
Subscription Change Request
Subscription Change Request Module
Subscription Request
Subscription Request Module
\ No newline at end of file
Subscription Change Request | Amount
Subscription Change Request | CredentialRequest
Subscription Change Request | DublinCore
Subscription Change Request | InstanceTree
Subscription Change Request | Person
Subscription Change Request | Price
Subscription Change Request | Reference
Subscription Change Request | SlapOSSaleProfileConstraint
Subscription Change Request | SlapOSSubscriptionRequestConstraint
Subscription Change Request | Task
Subscription Change Request | Url
Subscription Change Request | VariationRange
Subscription Request | Amount
Subscription Request | CredentialRequest
Subscription Request | DublinCore
......
......@@ -5,6 +5,9 @@ Open Sale Order | slapos_subscription_request_interaction_workflow
Sale Invoice Transaction Line | slapos_subscription_request_interaction_workflow
Sale Trade Condition | slapos_subscription_request_interaction_workflow
Software Product | slapos_subscription_request_interaction_workflow
Subscription Change Request | edit_workflow
Subscription Change Request | slapos_subscription_request_interaction_workflow
Subscription Change Request | subscription_request_workflow
Subscription Request | edit_workflow
Subscription Request | slapos_subscription_request_interaction_workflow
Subscription Request | subscription_request_workflow
\ No newline at end of file
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