Commit 4354b756 authored by Romain Courteaud's avatar Romain Courteaud

Simplify open order creation.

Try to reduce the number of documents created.
Always set a stop date on the open order line.
parent 7632c3b2
...@@ -56,7 +56,9 @@ if REQUEST is not None:\n ...@@ -56,7 +56,9 @@ if REQUEST is not None:\n
\n \n
if context.getCausalityState() == \'diverged\':\n if context.getCausalityState() == \'diverged\':\n
context.getDestinationSectionValue(portal_type="Person").Person_storeOpenSaleOrderJournal()\n context.getDestinationSectionValue(portal_type="Person").Person_storeOpenSaleOrderJournal()\n
context.converge()\n \n
# Person_storeOpenSaleOrderJournal should fix all divergent Hosting Subscription in one run\n
assert context.getCausalityState() == \'solved\'\n
</string> </value> </string> </value>
</item> </item>
<item> <item>
......
<?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>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
from Products.ERP5Type.DateUtils import addToDate, getClosestDate\n
from DateTime import DateTime\n
\n
person = context\n
portal = person.getPortalObject()\n
\n
previous_open_sale_order = portal.portal_catalog.getResultValue(\n
default_destination_uid=person.getUid(),\n
portal_type="Open Sale Order",\n
validation_state="validated")\n
\n
hosting_subscription_mapping = {}\n
if previous_open_sale_order is not None:\n
for open_sale_order_line in previous_open_sale_order.contentValues(portal_type=\'Open Sale Order Line\'):\n
hosting_subscription_mapping[open_sale_order_line.getAggregate(portal_type=\'Hosting Subscription\')] = open_sale_order_line.getId()\n
\n
now = DateTime()\n
\n
add_kw_kw = {}\n
for add_kw in add_kw_list:\n
hosting_url = add_kw[\'aggregate\']\n
if hosting_url not in hosting_subscription_mapping:\n
existing = len(portal.portal_catalog(\n
portal_type=\'Open Sale Order Line\',\n
default_aggregate_uid=portal.restrictedTraverse(hosting_url).getUid(),\n
limit=1,\n
)) > 0\n
if not existing:\n
# Hosting Subscription has never been related to any open order\n
add_kw_kw[hosting_url] = add_kw.copy()\n
add_kw_kw[hosting_url][\'explanation\'] = [\'Added because of %s \' % hosting_url]\n
\n
modify_kw_kw = {}\n
for hosting_url, start_date in start_date_tuple_list:\n
start_date = getClosestDate(target_date=start_date, precision=\'day\')\n
while start_date.day() >= 29:\n
start_date = addToDate(start_date, to_add={\'day\': -1})\n
if hosting_url in hosting_subscription_mapping:\n
line_id = hosting_subscription_mapping[hosting_url]\n
line = previous_open_sale_order[line_id]\n
if line.getStartDate() is None:\n
modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')] = {\'start_date\': start_date}\n
modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')][\'explanation\'] = [\'Set start date because of %s\' % hosting_url]\n
elif hosting_url in add_kw_kw:\n
add_kw_kw[hosting_url][\'start_date\'] = start_date\n
add_kw_kw[hosting_url][\'explanation\'].append(\'Added start date because of %s\' % hosting_url)\n
\n
if previous_open_sale_order is not None:\n
for line in previous_open_sale_order.contentValues(portal_type=\'Open Sale Order Line\'):\n
old_stop_date = line.getStopDate()\n
if old_stop_date is not None:\n
# if stop_date is not None and old_stop_date < stop_date:\n
# modify_kw_kw.setdefault(line.getAggregate(portal_type=\'Hosting Subscription\'), {})\n
# modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')].setdefault(\'explanation\', [])\n
# modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')][\'stop_date\'] = stop_date\n
# modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')][\'explanation\'].append(\'Set stop date because of stop_date %s\' % stop_date)\n
if old_stop_date < now:\n
new_stop_date = addToDate(old_stop_date, to_add={\'month\': 1})\n
while new_stop_date < now:\n
new_stop_date = addToDate(new_stop_date, to_add={\'month\': 1})\n
hosting_id = line.getAggregate(portal_type=\'Hosting Subscription\')\n
modify_kw_kw.setdefault(hosting_id, {})\n
modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')].setdefault(\'explanation\', [])\n
modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')][\'stop_date\'] = new_stop_date\n
modify_kw_kw[line.getAggregate(portal_type=\'Hosting Subscription\')][\'explanation\'].append(\'Set stop date becase of new_stop_date %s\' % new_stop_date)\n
\n
for (key, value) in add_kw_kw.copy().iteritems():\n
# if stop_date is not None:\n
# add_kw_kw[key][\'stop_date\'] = stop_date\n
# add_kw_kw[key][\'explanation\'].append(\'Set stop date beacuse of stop_date %s \' % stop_date)\n
# else:\n
start_date = add_kw_kw[key].get(\'start_date\')\n
if start_date is not None:\n
new_stop_date = addToDate(start_date, to_add={\'month\': 1})\n
while new_stop_date < now:\n
new_stop_date = addToDate(new_stop_date, to_add={\'month\': 1})\n
add_kw_kw[key][\'stop_date\'] = new_stop_date\n
add_kw_kw[key][\'explanation\'].append(\'Set stop date because of new_stop_date %s\' % new_stop_date)\n
\n
for (key, value) in modify_kw_kw.copy().iteritems():\n
# if stop_date is not None:\n
# modify_kw_kw[key][\'stop_date\'] = stop_date\n
# modify_kw_kw[key][\'explanation\'].append(\'Set stop date because of stop_date %s\' % stop_date)\n
# else:\n
start_date = modify_kw_kw[key].get(\'start_date\')\n
if start_date is not None:\n
new_stop_date = addToDate(start_date, to_add={\'month\': 1})\n
while new_stop_date < now:\n
new_stop_date = addToDate(new_stop_date, to_add={\'month\': 1})\n
modify_kw_kw[key][\'stop_date\'] = new_stop_date\n
modify_kw_kw[key][\'explanation\'].append(\'Set stop date because of new_stop_date %s\' % new_stop_date)\n
\n
remove_id_list = set()\n
if previous_open_sale_order is not None:\n
for remove_hosting in remove_hosting_list:\n
if remove_hosting in add_kw_kw:\n
add_kw_kw[remove_hosting][\'explanation\'].append(\'Not removed because just added\')\n
continue\n
if remove_hosting in modify_kw_kw:\n
modify_kw_kw[remove_hosting][\'explanation\'].append(\'Not removed because just modified\')\n
continue\n
if remove_hosting in hosting_subscription_mapping:\n
line = previous_open_sale_order[hosting_subscription_mapping.get(remove_hosting)]\n
if (line.getStartDate() is not None and line.getStopDate() is not None and line.getStartDate() < line.getStopDate()):\n
remove_id_list.add((line.getId(), \'Removed %s\' % line.getPath()))\n
return add_kw_kw, modify_kw_kw, list(remove_id_list)\n
]]></string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>remove_hosting_list, add_kw_list, start_date_tuple_list</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Person_getOpenSaleOrderDifference</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?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>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
from DateTime import DateTime\n
\n
person = context\n
portal = person.getPortalObject()\n
tag = \'%s_updateOpenSaleOrder\' % person.getUid()\n
activate_kw = {\'tag\': tag}\n
if portal.portal_activities.countMessageWithTag(tag) > 0:\n
# nothing to do\n
return\n
\n
def storeWorkflowComment(document, comment):\n
portal.portal_workflow.doActionFor(document, \'edit_action\', comment=comment)\n
\n
add_kw_kw, modify_kw_kw, remove_id_list = context.Person_getOpenSaleOrderDifference(\n
remove_hosting_list,\n
add_kw_list,\n
start_date_tuple_list)\n
\n
now = DateTime()\n
previous_open_sale_order = portal.portal_catalog.getResultValue(\n
default_destination_uid=person.getUid(),\n
portal_type="Open Sale Order",\n
validation_state="validated")\n
\n
if not(add_kw_kw or modify_kw_kw or remove_id_list):\n
return # nothing to do\n
\n
open_order_edit_kw = {\n
\'effective_date\': now,\n
\'activate_kw\': activate_kw,\n
}\n
if previous_open_sale_order is None:\n
open_sale_order_template = portal.restrictedTraverse(\n
portal.portal_preferences.getPreferredOpenSaleOrderTemplate())\n
open_sale_order = open_sale_order_template.Base_createCloneDocument(batch_mode=1)\n
open_order_edit_kw.update({\n
\'destination\': person.getRelativeUrl(),\n
\'destination_decision\': person.getRelativeUrl(),\n
\'title\': "SlapOS Subscription Open Sale Order",\n
})\n
else:\n
open_sale_order = previous_open_sale_order.Base_createCloneDocument(batch_mode=1)\n
previous_open_sale_order.setExpirationDate(now, activate_kw=activate_kw)\n
open_sale_order.edit(**open_order_edit_kw)\n
open_sale_order.order(activate_kw=activate_kw)\n
open_sale_order.validate(activate_kw=activate_kw)\n
\n
open_sale_order_line_template = portal.restrictedTraverse(\n
portal.portal_preferences.getPreferredOpenSaleOrderLineTemplate())\n
for kw in add_kw_kw.itervalues():\n
explanation_list = kw.pop(\'explanation\', [])\n
line = open_sale_order_line_template.Base_createCloneDocument(batch_mode=1,\n
destination=open_sale_order)\n
line.edit(activate_kw=activate_kw, **kw)\n
for explanation in explanation_list:\n
storeWorkflowComment(line, explanation)\n
\n
for hs, kw in modify_kw_kw.iteritems():\n
line = [q for q in open_sale_order.contentValues() if q.getAggregate(portal_type=\'Hosting Subscription\') == hs][0]\n
explanation_list = kw.pop(\'explanation\', [])\n
line.edit(activate_kw=activate_kw, **kw)\n
for explanation in explanation_list:\n
storeWorkflowComment(line, explanation)\n
\n
open_sale_order.deleteContent([q[0] for q in remove_id_list])\n
for explanation in remove_id_list:\n
storeWorkflowComment(open_sale_order, explanation[1])\n
]]></string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>remove_hosting_list, add_kw_list, start_date_tuple_list</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Person_updateOpenSaleOrder</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -714,7 +714,7 @@ class TestOpenSaleOrderAlarm(testSlapOSMixin): ...@@ -714,7 +714,7 @@ class TestOpenSaleOrderAlarm(testSlapOSMixin):
.Base_createCloneDocument(batch_mode=1) .Base_createCloneDocument(batch_mode=1)
self.tic() self.tic()
person.Person_updateOpenSaleOrder() person.Person_storeOpenSaleOrderJournal()
self.tic() self.tic()
open_sale_order_list = self.portal.portal_catalog( open_sale_order_list = self.portal.portal_catalog(
...@@ -722,16 +722,8 @@ class TestOpenSaleOrderAlarm(testSlapOSMixin): ...@@ -722,16 +722,8 @@ class TestOpenSaleOrderAlarm(testSlapOSMixin):
portal_type='Open Sale Order', portal_type='Open Sale Order',
default_destination_uid=person.getUid() default_destination_uid=person.getUid()
) )
self.assertEqual(1, len(open_sale_order_list)) # No need to create any open order without hosting subscription
open_sale_order = open_sale_order_list[0] self.assertEqual(0, len(open_sale_order_list))
self.assertEqual('SlapOS Subscription Open Sale Order',
open_sale_order.getTitle())
self.assertEqual(0, len(open_sale_order.contentValues()))
open_sale_order_template = self.portal.restrictedTraverse(
self.portal.portal_preferences.getPreferredOpenSaleOrderTemplate())
self.assertTrue(all([q in open_sale_order.getCategoryList() \
for q in open_sale_order_template.getCategoryList()]))
@simulateByEditWorkflowMark('HostingSubscription_requestUpdateOpenSaleOrder') @simulateByEditWorkflowMark('HostingSubscription_requestUpdateOpenSaleOrder')
def test_alarm_HS_diverged(self): def test_alarm_HS_diverged(self):
...@@ -805,7 +797,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -805,7 +797,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
line.getQuantity()) line.getQuantity())
self.assertEqual(open_sale_order_line_template.getPrice(), self.assertEqual(open_sale_order_line_template.getPrice(),
line.getPrice()) line.getPrice())
self.assertEqual(None, line.getStartDate()) self.assertEqual(DateTime().earliestTime(), line.getStartDate())
def test_usualLifetime_HostingSubscription(self): def test_usualLifetime_HostingSubscription(self):
person = self.portal.person_module.template_member\ person = self.portal.person_module.template_member\
...@@ -870,7 +862,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -870,7 +862,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
self.assertEqual(request_time, line.getStartDate()) self.assertEqual(request_time, line.getStartDate())
self.assertEqual(stop_date, line.getStopDate()) self.assertEqual(stop_date, line.getStopDate())
destroy_time = DateTime('2012/02/01') destroy_time = DateTime('2112/02/01')
subscription.workflow_history['instance_slap_interface_workflow'].append({ subscription.workflow_history['instance_slap_interface_workflow'].append({
'comment':'Simulated request instance', 'comment':'Simulated request instance',
'error_message': '', 'error_message': '',
...@@ -926,6 +918,9 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -926,6 +918,9 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
self.assertEqual(open_sale_order_line_template.getPrice(), self.assertEqual(open_sale_order_line_template.getPrice(),
line.getPrice()) line.getPrice())
self.assertEqual(request_time, archived_line.getStartDate()) self.assertEqual(request_time, archived_line.getStartDate())
now = DateTime()
while stop_date <= destroy_time:
stop_date = addToDate(stop_date, to_add={'month': 1})
self.assertEqual(stop_date, line.getStopDate()) self.assertEqual(stop_date, line.getStopDate())
def test_lateAnalysed_HostingSubscription(self): def test_lateAnalysed_HostingSubscription(self):
...@@ -993,8 +988,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -993,8 +988,7 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
self.assertEqual(request_time, line.getStartDate()) self.assertEqual(request_time, line.getStartDate())
stop_date = request_time stop_date = request_time
now = DateTime() while stop_date <= destroy_time:
while stop_date < now:
stop_date = addToDate(stop_date, to_add={'month': 1}) stop_date = addToDate(stop_date, to_add={'month': 1})
self.assertEqual(stop_date, line.getStopDate()) self.assertEqual(stop_date, line.getStopDate())
...@@ -1085,42 +1079,23 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -1085,42 +1079,23 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
default_destination_uid=person.getUid() default_destination_uid=person.getUid()
) )
self.assertEqual(2, len(open_sale_order_list)) self.assertEqual(1, len(open_sale_order_list))
validated_open_sale_order_list = [q for q in open_sale_order_list validated_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'validated'] if q.getValidationState() == 'validated']
archived_open_sale_order_list = [q for q in open_sale_order_list archived_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'archived'] if q.getValidationState() == 'archived']
self.assertEqual(1, len(validated_open_sale_order_list)) self.assertEqual(1, len(validated_open_sale_order_list))
self.assertEqual(1, len(archived_open_sale_order_list)) self.assertEqual(0, len(archived_open_sale_order_list))
validated_open_sale_order = validated_open_sale_order_list[0].getObject() validated_open_sale_order = validated_open_sale_order_list[0].getObject()
archived_open_sale_order = archived_open_sale_order_list[0]\
.getObject()
self.assertEqual(open_sale_order.getRelativeUrl(),
archived_open_sale_order.getRelativeUrl())
validated_line_list = validated_open_sale_order.contentValues( validated_line_list = validated_open_sale_order.contentValues(
portal_type='Open Sale Order Line') portal_type='Open Sale Order Line')
archived_line_list = archived_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(2, len(validated_line_list)) self.assertEqual(2, len(validated_line_list))
self.assertEqual(1, len(archived_line_list))
archived_line = archived_line_list[0].getObject()
self.assertEqual(line.getRelativeUrl(), archived_line.getRelativeUrl())
self.assertEqual(subscription.getRelativeUrl(),
archived_line.getAggregate())
self.assertTrue(all([q in archived_line.getCategoryList() \
for q in open_sale_order_line_template.getCategoryList()]))
self.assertEqual(open_sale_order_line_template.getResource(),
archived_line.getResource())
self.assertEqual(open_sale_order_line_template.getQuantity(), self.assertEqual(open_sale_order_line_template.getQuantity(),
line.getQuantity()) line.getQuantity())
self.assertEqual(open_sale_order_line_template.getPrice(), self.assertEqual(open_sale_order_line_template.getPrice(),
line.getPrice()) line.getPrice())
self.assertEqual(request_time, archived_line.getStartDate())
self.assertEqual(stop_date, archived_line.getStopDate())
stop_date_2 = request_time_2 stop_date_2 = request_time_2
now = DateTime() now = DateTime()
...@@ -1157,7 +1132,79 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin): ...@@ -1157,7 +1132,79 @@ class TestHostingSubscription_requestUpdateOpenSaleOrder(testSlapOSMixin):
def test_hosting_subscription_start_date_not_changed(self): def test_hosting_subscription_start_date_not_changed(self):
# if there was no request_instance the getCreationDate has been used # if there was no request_instance the getCreationDate has been used
# but if request_instance appeared start_date is not changed # but if request_instance appeared start_date is not changed
raise NotImplementedError person = self.portal.person_module.template_member\
.Base_createCloneDocument(batch_mode=1)
self.tic()
subscription = self.portal.hosting_subscription_module\
.template_hosting_subscription.Base_createCloneDocument(batch_mode=1)
subscription.edit(reference='TESTHS-%s' % self.generateNewId(),
destination_section=person.getRelativeUrl())
self.portal.portal_workflow._jumpToStateFor(subscription, 'validated')
self.tic()
subscription.HostingSubscription_requestUpdateOpenSaleOrder()
self.tic()
request_time = DateTime('2112/01/01')
subscription.workflow_history['instance_slap_interface_workflow'].append({
'comment':'Simulated request instance',
'error_message': '',
'actor': 'ERP5TypeTestCase',
'slap_state': 'start_requested',
'time': request_time,
'action': 'request_instance'
})
self.tic()
subscription.HostingSubscription_requestUpdateOpenSaleOrder()
self.tic()
self.assertEqual(subscription.getCausalityState(), 'solved')
open_sale_order_list = self.portal.portal_catalog(
portal_type='Open Sale Order',
default_destination_uid=person.getUid()
)
self.assertEqual(1, len(open_sale_order_list))
open_sale_order = open_sale_order_list[0].getObject()
self.assertEqual('validated', open_sale_order.getValidationState())
open_sale_order_line_list = open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(1, len(open_sale_order_line_list))
line = open_sale_order_line_list[0].getObject()
self.assertEqual(subscription.getCreationDate().earliestTime(),
line.getStartDate())
def test_hosting_subscription_diverged_to_solve(self):
# check that HS becomes solved even if not modification is needed on open
# order
person = self.portal.person_module.template_member\
.Base_createCloneDocument(batch_mode=1)
self.tic()
subscription = self.portal.hosting_subscription_module\
.template_hosting_subscription.Base_createCloneDocument(batch_mode=1)
subscription.edit(reference='TESTHS-%s' % self.generateNewId(),
destination_section=person.getRelativeUrl())
self.portal.portal_workflow._jumpToStateFor(subscription, 'validated')
self.assertEqual(subscription.getCausalityState(), 'diverged')
self.tic()
subscription.HostingSubscription_requestUpdateOpenSaleOrder()
self.tic()
self.assertEqual(subscription.getCausalityState(), 'solved')
self.portal.portal_workflow._jumpToStateFor(subscription, 'diverged')
subscription.reindexObject()
self.assertEqual(subscription.getCausalityState(), 'diverged')
self.assertEqual(subscription.getSlapState(), 'draft')
self.tic()
subscription.HostingSubscription_requestUpdateOpenSaleOrder()
self.tic()
self.assertEqual(subscription.getCausalityState(), 'solved')
class TestSlapOSTriggerBuildAlarm(testSlapOSMixin): class TestSlapOSTriggerBuildAlarm(testSlapOSMixin):
@simulateByTitlewMark('SimulationMovement_buildSlapOS') @simulateByTitlewMark('SimulationMovement_buildSlapOS')
......
233 234
\ No newline at end of file \ 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