Commit 04dc4d86 authored by Romain Courteaud's avatar Romain Courteaud

slapos_accounting: generate one open order per instance tree

parent c20f2282
......@@ -2,11 +2,132 @@ from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
if context.getCausalityState() == 'diverged':
from DateTime import DateTime
person = context.getDestinationSectionValue(portal_type="Person")
portal = context.getPortalObject()
instance_tree = context
now = DateTime()
tag = '%s_%s' % (instance_tree.getUid(), script.id)
activate_kw = {'tag': tag}
if portal.portal_activities.countMessageWithTag(tag) > 0:
# nothing to do
return
def storeWorkflowComment(document, comment):
portal.portal_workflow.doActionFor(document, 'edit_action', comment=comment)
def newOpenOrder(open_sale_order):
open_sale_order_template = portal.restrictedTraverse(
portal.portal_preferences.getPreferredOpenSaleOrderTemplate())
open_order_edit_kw = {
'effective_date': DateTime(),
'activate_kw': activate_kw,
'source': open_sale_order_template.getSource(),
'source_section': open_sale_order_template.getSourceSection()
}
if open_sale_order is None:
new_open_sale_order = open_sale_order_template.Base_createCloneDocument(batch_mode=1)
open_order_edit_kw.update({
'destination': person.getRelativeUrl(),
'destination_decision': person.getRelativeUrl(),
'title': "%s SlapOS Subscription" % person.getTitle(),
})
else:
new_open_sale_order = open_sale_order.Base_createCloneDocument(batch_mode=1)
open_sale_order.setExpirationDate(now, activate_kw=activate_kw)
new_open_sale_order.edit(**open_order_edit_kw)
new_open_sale_order.order(activate_kw=activate_kw)
new_open_sale_order.validate(activate_kw=activate_kw)
return new_open_sale_order
if instance_tree.getCausalityState() == 'diverged':
person = instance_tree.getDestinationSectionValue(portal_type="Person")
# Template document does not have person relation
if person is not None:
person.Person_storeOpenSaleOrderJournal()
# Search an existing related open order
open_order_line = portal.portal_catalog.getResultValue(
portal_type='Open Sale Order Line',
default_aggregate_uid=instance_tree.getUid())
is_open_order_creation_needed = False
# Simply check that it has never been simulated
if instance_tree.getSlapState() == 'destroy_requested':
# Line should be deleted
if (open_order_line is not None) and (open_order_line.getValidationState() == "invalidated"):
instance_tree.converge(comment="Last open order: %s" % open_order_line.getRelativeUrl())
elif open_order_line is None:
# User has no Open Sale Order (likely), so we add the line to remove later. This allow us to charge
# eventual usage between the runs of the alarm.
is_open_order_creation_needed = True
elif open_order_line is None:
# Let's add
is_open_order_creation_needed = True
# Let's create the open order
if is_open_order_creation_needed:
open_sale_order = newOpenOrder(None)
open_order_explanation = ""
# Add lines
open_sale_order_line_template = portal.restrictedTraverse(
portal.portal_preferences.getPreferredOpenSaleOrderLineTemplate())
open_sale_order_line = open_sale_order_line_template.Base_createCloneDocument(batch_mode=1,
destination=open_sale_order)
start_date = instance_tree.InstanceTree_calculateSubscriptionStartDate()
edit_kw = {}
subscription_request = instance_tree.getAggregateRelatedValue(portal_type="Subscription Request")
# Define the start date of the period, this can variates with the time.
# start_date_delta = 0
if subscription_request is not None:
# Copy from Subscription Condition the source and Source Section into the line
# RAFAEL: As the model is use single Open Order, it isn't possible to use multiple
# companies per region, so we rely on Subscription Conditions to Describe the
# providers.
edit_kw["source"] = subscription_request.getSource()
edit_kw["source_section"] = subscription_request.getSourceSection()
# Quantity is double because the first invoice has to
# charge for 2 months
edit_kw['quantity'] = subscription_request.getQuantity()
edit_kw['price'] = subscription_request.getPrice()
edit_kw['price_currency'] = subscription_request.getPriceCurrency()
# While create move the start date to be at least 1 months
# So we can charge 3 months at once
# You can increase 65 days to generate 3 months
# You can increase 32 days to generate 2 months
# You can increase 0 days to keep generating one month only
# start_date_delta = 0
open_sale_order_line.edit(
activate_kw=activate_kw,
title=instance_tree.getTitle(),
start_date=start_date,
# Ensure stop date value is higher than start date
# it will be updated by OpenSaleOrder_updatePeriod
stop_date=start_date + 1,
# stop_date=calculateOpenOrderLineStopDate(open_sale_order_line,
# instance_tree, start_date_delta=start_date_delta),
aggregate_value=instance_tree,
**edit_kw
)
storeWorkflowComment(open_sale_order_line, "Created for %s" % instance_tree.getRelativeUrl())
# instance_tree.converge(comment="Last open order: %s" % open_sale_order_line.getRelativeUrl())
open_order_explanation = "Added %s." % str(open_sale_order_line.getId())
storeWorkflowComment(open_sale_order, open_order_explanation)
else:
open_sale_order = open_order_line.getParentValue()
open_sale_order.OpenSaleOrder_updatePeriod()
# Person_storeOpenSaleOrderJournal should fix all divergent Instance Tree in one run
assert context.getCausalityState() == 'solved'
assert instance_tree.getCausalityState() == 'solved'
......@@ -2,7 +2,86 @@ from zExceptions import Unauthorized
if REQUEST is not None:
raise Unauthorized
if context.getValidationState() == 'validated':
person = context.getDestinationDecisionValue(portal_type="Person")
from erp5.component.module.DateUtils import addToDate
from DateTime import DateTime
portal = context.getPortalObject()
open_sale_order = context
now = DateTime()
tag = '%s_%s' % (open_sale_order.getUid(), script.id)
activate_kw = {'tag': tag}
if portal.portal_activities.countMessageWithTag(tag) > 0:
# nothing to do
return
def storeWorkflowComment(document, comment):
portal.portal_workflow.doActionFor(document, 'edit_action', comment=comment)
def calculateOpenOrderLineStopDate(open_order_line, instance_tree, start_date_delta, next_stop_date_delta=0):
end_date = instance_tree.InstanceTree_calculateSubscriptionStopDate()
if end_date is None:
# Be sure that start date is different from stop date
# Consider the first period longer (delta), this allow us to change X days/months
# On a first invoice.
next_stop_date = instance_tree.getNextPeriodicalDate(
instance_tree.InstanceTree_calculateSubscriptionStartDate() + start_date_delta)
current_stop_date = next_stop_date
# Ensure the invoice is generated 15 days in advance of the next period.
while next_stop_date < now + next_stop_date_delta:
# Return result should be < now, it order to provide stability in simulation (destruction if it happen should be >= now)
current_stop_date = next_stop_date
next_stop_date = \
instance_tree.getNextPeriodicalDate(current_stop_date)
return addToDate(current_stop_date, to_add={'second': -1})
else:
stop_date = end_date
return stop_date
if open_sale_order.getValidationState() == 'validated':
person = open_sale_order.getDestinationDecisionValue(portal_type="Person")
if person is not None:
person.Person_storeOpenSaleOrderJournal()
for open_order_line in open_sale_order.contentValues(
portal_type='Open Sale Order Line'):
current_start_date = open_order_line.getStartDate()
current_stop_date = open_order_line.getStopDate()
# Prevent mistakes
assert current_start_date is not None
assert current_stop_date is not None
assert current_start_date < current_stop_date
instance_tree = open_order_line.getAggregateValue(portal_type='Instance Tree')
assert current_start_date == instance_tree.InstanceTree_calculateSubscriptionStartDate()
subscription_request = instance_tree.getAggregateRelatedValue(portal_type="Subscription Request")
# Define the start date of the period, this can variates with the time.
next_stop_date_delta = 0
if subscription_request is not None:
next_stop_date_delta = 46
# First check if the instance tree has been correctly simulated (this script may run only once per year...)
stop_date = calculateOpenOrderLineStopDate(open_order_line, instance_tree,
start_date_delta=0, next_stop_date_delta=next_stop_date_delta)
if current_stop_date < stop_date:
# Bingo, new subscription to generate
open_order_line.edit(
stop_date=stop_date,
activate_kw=activate_kw)
storeWorkflowComment(open_order_line,
'Stop date updated to %s' % stop_date)
if instance_tree.getSlapState() == 'destroy_requested':
# Line should be deleted
assert instance_tree.getCausalityState() == 'diverged'
instance_tree.converge(comment="Last open order: %s" % open_order_line.getRelativeUrl())
open_sale_order.archive()
storeWorkflowComment(open_sale_order, "Instance Tree destroyed: %s" % instance_tree.getRelativeUrl())
elif (instance_tree.getCausalityState() == 'diverged'):
instance_tree.converge(comment="Nothing to do on open order.")
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Person_storeOpenSaleOrderJournal</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -107,22 +107,6 @@ class TestOpenSaleOrderAlarm(SlapOSTestCaseMixin):
default_destination_uid=person.getUid()
))
def test_OSO_after_Person_updateOpenSaleOrder(self):
person = self.portal.person_module.template_member\
.Base_createCloneDocument(batch_mode=1)
self.tic()
person.Person_storeOpenSaleOrderJournal()
self.tic()
open_sale_order_list = self.portal.portal_catalog(
validation_state='validated',
portal_type='Open Sale Order',
default_destination_uid=person.getUid()
)
# No need to create any open order without instance tree
self.assertEqual(0, len(open_sale_order_list))
@simulateByEditWorkflowMark('InstanceTree_requestUpdateOpenSaleOrder')
def test_alarm_HS_diverged(self):
subscription = self.portal.instance_tree_module\
......@@ -291,28 +275,22 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
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
if q.getValidationState() == 'validated']
archived_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'archived']
self.assertEqual(0, len(validated_open_sale_order_list))
self.assertEqual(2, len(archived_open_sale_order_list))
archived_open_sale_order_list.sort(key=lambda x: x.getCreationDate())
self.assertEqual(1, len(archived_open_sale_order_list))
last_open_sale_order = archived_open_sale_order_list[-1].getObject()
archived_open_sale_order = archived_open_sale_order_list[0]\
.getObject()
self.assertEqual(open_sale_order.getRelativeUrl(),
archived_open_sale_order.getRelativeUrl())
last_line_list = last_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
archived_line_list = archived_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(0, len(last_line_list))
self.assertEqual(1, len(archived_line_list))
archived_line = archived_line_list[0].getObject()
......@@ -376,7 +354,7 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
default_destination_uid=person.getUid()
)
self.assertEqual(2, len(open_sale_order_list))
self.assertEqual(1, len(open_sale_order_list))
archived_open_sale_order_list = [x for x in open_sale_order_list \
if x.getValidationState() != 'validated' and \
len(x.objectValues()) > 0]
......@@ -421,7 +399,7 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
self.assertEqual('archived', new_open_sale_order.getValidationState())
open_sale_order_line_list = new_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(0, len(open_sale_order_line_list))
self.assertEqual(1, len(open_sale_order_line_list))
def test_two_InstanceTree(self):
person = self.portal.person_module.template_member\
......@@ -517,18 +495,23 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
default_destination_uid=person.getUid()
)
self.assertEqual(1, len(open_sale_order_list))
self.assertEqual(2, len(open_sale_order_list))
validated_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'validated']
archived_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'archived']
self.assertEqual(1, len(validated_open_sale_order_list))
self.assertEqual(2, len(validated_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_line_list = validated_open_sale_order.contentValues(
open_sale_order_2 = [x for x in validated_open_sale_order_list if x.getRelativeUrl() != open_sale_order.getRelativeUrl()][0]
self.assertEqual(open_sale_order.getRelativeUrl(), [x for x in validated_open_sale_order_list if x.getRelativeUrl() == open_sale_order.getRelativeUrl()][0].getRelativeUrl())
validated_line_list = open_sale_order_2.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(2, len(validated_line_list))
self.assertEqual(1, len(validated_line_list))
validated_line_2 = validated_line_list[0]
validated_line_1 = line
self.assertEqual(open_sale_order_line_template.getQuantity(),
line.getQuantity())
......@@ -543,10 +526,8 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
next_stop_date_2 = addToDate(stop_date_2, to_add={'month': 1})
stop_date_2 = addToDate(stop_date_2, to_add={'second': -1})
validated_line_1 = [q for q in validated_line_list if q.getAggregate() == \
subscription.getRelativeUrl()][0]
validated_line_2 = [q for q in validated_line_list if q.getAggregate() == \
subscription2.getRelativeUrl()][0]
self.assertEqual(validated_line_1.getAggregate(), subscription.getRelativeUrl())
self.assertEqual(validated_line_2.getAggregate(), subscription2.getRelativeUrl())
self.assertTrue(all([q in validated_line_1.getCategoryList() \
for q in open_sale_order_line_template.getCategoryList()]))
......@@ -667,7 +648,7 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
default_destination_uid=person.getUid()
)
self.assertEqual(2,len(open_sale_order_list))
self.assertEqual(1,len(open_sale_order_list))
archived_open_sale_order_list = [x for x in open_sale_order_list \
if x.getValidationState() != 'validated']
......@@ -680,7 +661,6 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
portal_type='Open Sale Order Line')
self.assertEqual(1, len(open_sale_order_line_list))
effective_date = open_sale_order.getEffectiveDate()
line = open_sale_order_line_list[0].getObject()
self.assertEqual(subscription.getRelativeUrl(), line.getAggregate())
......@@ -698,19 +678,6 @@ class TestInstanceTree_requestUpdateOpenSaleOrder(SlapOSTestCaseMixin):
self.assertEqual(addToDate(line.getStartDate(), to_add={'day': 1}),
line.getStopDate())
archived_open_sale_order_list = [x for x in open_sale_order_list \
if x.getValidationState() != 'validated']
archived_open_sale_order_list.sort(key=lambda x: x.getCreationDate())
new_open_sale_order = archived_open_sale_order_list[-1].getObject()
self.assertEqual('archived', new_open_sale_order.getValidationState())
new_effective_date = new_open_sale_order.getEffectiveDate()
open_sale_order_line_list = new_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(0, len(open_sale_order_line_list))
self.assertTrue(new_effective_date > effective_date,
"%s <= %s" % (new_effective_date, effective_date))
class TestSlapOSTriggerBuildAlarm(SlapOSTestCaseMixin):
@simulateByTitlewMark('SimulationMovement_buildSlapOS')
......@@ -1262,14 +1229,11 @@ class TestSlapOSUpdateOpenSaleOrderPeriod(SlapOSTestCaseMixin):
open_order.edit(
destination_decision_value=person,
)
open_order.newContent(
portal_type="Open Sale Order Line"
)
script_name = "Person_storeOpenSaleOrderJournal"
self._simulateScript(script_name)
try:
open_order.OpenSaleOrder_updatePeriod()
finally:
self._dropScript(script_name)
self.assertScriptVisited(person, script_name)
self.assertRaises(AssertionError, open_order.OpenSaleOrder_updatePeriod)
def test_updatePeriod_invalidated(self):
open_order = self.createOpenOrder()
......@@ -1279,14 +1243,10 @@ class TestSlapOSUpdateOpenSaleOrderPeriod(SlapOSTestCaseMixin):
destination_decision_value=person,
)
open_order.invalidate()
script_name = "Person_storeOpenSaleOrderJournal"
self._simulateScript(script_name)
try:
open_order.newContent(
portal_type="Open Sale Order Line"
)
open_order.OpenSaleOrder_updatePeriod()
finally:
self._dropScript(script_name)
self.assertScriptNotVisited(person, script_name)
def test_alarm(self):
open_order = self.createOpenOrder()
......
......@@ -617,18 +617,19 @@ class DefaultScenarioMixin(TestSlapOSSecurityMixin):
self.assertEqual(0, len(open_sale_order_list))
return
self.assertEqual(2, len(open_sale_order_list))
self.assertEqual(len(instance_tree_list), len(open_sale_order_list))
archived_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'archived']
self.assertEqual(len(instance_tree_list), len(archived_open_sale_order_list))
archived_open_sale_order_list.sort(key=lambda x: x.getCreationDate())
# Select the first archived
open_sale_order = archived_open_sale_order_list[0]
line_list = open_sale_order.contentValues(
line_list = []
for open_sale_order in archived_open_sale_order_list:
archived_line_list = open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(1, len(archived_line_list))
line_list.extend(archived_line_list)
self.assertEqual(len(instance_tree_list), len(line_list))
self.assertSameSet(
[q.getRelativeUrl() for q in instance_tree_list],
......@@ -637,15 +638,9 @@ class DefaultScenarioMixin(TestSlapOSSecurityMixin):
validated_open_sale_order_list = [q for q in open_sale_order_list
if q.getValidationState() == 'validated']
# if no line, all open orders are kept archived
self.assertEqual(len(validated_open_sale_order_list), 0)
latest_open_sale_order = archived_open_sale_order_list[-1]
line_list = latest_open_sale_order.contentValues(
portal_type='Open Sale Order Line')
self.assertEqual(len(line_list), 0)
def findMessage(self, email, body):
for candidate in reversed(self.portal.MailHost.getMessageList()):
if [q for q in candidate[1] if email in q] and body in candidate[2]:
......
......@@ -1652,8 +1652,12 @@ return dict(vads_url_already_registered="%s/already_registered" % (payment_trans
software_type="default",
partition_reference="_test_subscription_scenario_with_existing_user_extra_instance",
)
self.non_subscription_related_instance_amount = 1
# Trigger open order creation
self.portal.portal_alarms.slapos_request_update_instance_tree_open_sale_order.activeSense()
self.tic()
self.login()
self.requestAndCheckInstanceTree(
amount, name, default_email_text)
......
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