testBPMEvaluation.py 35.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
# -*- coding: utf-8 -*-
##############################################################################
# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved.
#          Łukasz Nowak <luke@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly advised to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
"""
This is BPM Evaluation Test class using erp5_bpm development Business Template

Generally it tries to use two Business Processes - one with sequence very
similar to normal ERP5 - TestBPMEvaluationDefaultProcessMixin, second one
inverted - TestBPMEvaluationDifferentProcessMixin.

It uses only Sale path to demonstrate BPM.

It is advised to *NOT* remove erp5_administration.
"""
import unittest
40
import transaction
41 42

from Products.ERP5.tests.testBPMCore import TestBPMMixin
43 44
from Products.ERP5.DivergenceSolutionDecision import DivergenceSolutionDecision

45 46 47 48 49 50
from DateTime import DateTime

class TestBPMEvaluationMixin(TestBPMMixin):
  node_portal_type = 'Organisation'
  order_portal_type = 'Sale Order'
  order_line_portal_type = 'Sale Order Line'
51 52
  packing_list_portal_type = 'Sale Packing List'
  packing_list_line_portal_type = 'Sale Packing List Line'
53
  trade_condition_portal_type = 'Sale Trade Condition'
54
  invoice_portal_type = 'Sale Invoice Transaction'
55 56 57 58 59 60
  product_portal_type = 'Product'
  order_start_date = DateTime()
  order_stop_date = order_start_date + 10

  def getBusinessTemplateList(self):
    return TestBPMMixin.getBusinessTemplateList(self) + ('erp5_bpm',
61
        'erp5_administration', 'erp5_simulation',)
62 63 64 65 66 67

  def afterSetUp(self):
    TestBPMMixin.afterSetUp(self)
    self._createNodes()
    self._createBusinessProcess()
    self._createTradeCondition()
68
    self._createRootDocument()
69
    self._setUpRules()
70 71
    self.stepTic()

72 73 74 75 76 77 78 79 80 81 82 83
  def _setUpRules(self):
    """Setups rules

    Rules are part of configuration, so anything provided by Business
    Templates or previous test runs is ignored - all old rules are invalidated
    between tests and new rules are created, configured and validated.
    """
    self.rule_tool = self.portal.portal_rules
    for rule in self.rule_tool.contentValues():
      if rule.getValidationState() == 'validated':
        rule.invalidate()
    transaction.commit()
84 85 86
    self._createOrderRule()
    self._createDeliveryRule()
    self._createInvoicingRule()
87
    self._createInvoiceRule()
88 89 90 91 92 93 94 95 96
    self._createTradeModelRule()

  def _createRootTradeRule(self, **kw):
    edit_dict = {}
    edit_dict.update(
      trade_phase = 'default/delivery',
    )
    edit_dict.update(**kw)
    rule = self.rule_tool.newContent(**edit_dict)
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

    # matching providers
    for category in ('resource', 'order'):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=False,
        matching_provider=True)
    rule.newContent(
      portal_type='Variation Divergence Tester',
      title='variation divergence tester',
      tested_property='variation_property_dict',
      divergence_provider=False,
      matching_provider=True)

    # divergence providers
    for category in ('source_section',
                     'resource',
                     'destination_section',
                     'source',
                     'aggregate'):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Net Converted Quantity Divergence Tester',
      title='quantity divergence tester',
      tested_property='quantity',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
    for property_id in ('start_date', 'stop_date'):
      rule.newContent(
        portal_type='DateTime Divergence Tester',
        title='%s divergence tester' % property_id,
        tested_property=property_id,
        quantity=0,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Float Divergence Tester',
      title='price divergence tester',
      tested_property='price',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
147 148 149

    return rule

150 151
  def _createOrderRule(self):
    rule = self._createRootTradeRule(portal_type='Order Rule',
152
        reference='default_order_rule')
153 154 155
    rule.validate()
    transaction.commit()

156 157
  def _createDeliveryRule(self):
    rule = self._createRootTradeRule(portal_type='Delivery Rule',
158
        reference='default_delivery_rule')
159 160 161 162 163 164 165 166
    rule.validate()
    transaction.commit()

  def _createTradeModelRule(self):
    rule = self.rule_tool.newContent(portal_type='Trade Model Rule',
      reference='default_trade_model_rule',
      test_method_id = ('SimulationMovement_testTradeModelRule',)
      )
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    # matching providers
    for category in ('resource',):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=False,
        matching_provider=True)
    rule.newContent(
      portal_type='Variation Divergence Tester',
      title='variation divergence tester',
      tested_property='variation_property_dict',
      divergence_provider=False,
      matching_provider=True)

    # divergence providers
    for category in ('resource',
                     'source_section',
                     'destination_section',
                     'source',
                     'source_function',
                     'destination_function',
                     'source_project',
                     'destination_project',
                     'aggregate',
                     'price_currency',
                     'base_contribution',
                     'base_application',
                     'source_account',
                     'destination_account',
                     ):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Net Converted Quantity Divergence Tester',
      title='quantity divergence tester',
      tested_property='quantity',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
    for property_id in ('start_date', 'stop_date'):
      rule.newContent(
        portal_type='DateTime Divergence Tester',
        title='%s divergence tester' % property_id,
        tested_property=property_id,
        quantity=0,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Float Divergence Tester',
      title='price divergence tester',
      tested_property='price',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
226 227 228 229

    rule.validate()
    transaction.commit()

230
  def _createInvoiceRule(self):
231 232
    # Note: This is not used, but invoices, even if built from simulation,
    #       need those rule to create empty one applied rule
233 234 235 236 237 238 239 240
    rule_tool = self.portal.portal_rules

    clipboard = rule_tool.manage_copyObjects(ids = ['default_invoice_rule'])
    pasted = rule_tool.manage_pasteObjects(clipboard)
    new_rule = getattr(rule_tool, pasted[0]['new_id'])
    new_rule.validate()
    transaction.commit()

241
  def _createInvoicingRule(self):
242 243 244
    edit_dict = {}
    edit_dict.update(
    )
245 246
    rule = self.rule_tool.newContent(portal_type='Invoicing Rule',
      reference='default_invoicing_rule',
247
      trade_phase = 'default/invoicing',
248
      test_method_id = ('SimulationMovement_testInvoicingRule',)
249
      )
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
    # matching providers
    for category in ('resource',):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=False,
        matching_provider=True)
    rule.newContent(
      portal_type='Variation Divergence Tester',
      title='variation divergence tester',
      tested_property='variation_property_dict',
      divergence_provider=False,
      matching_provider=True)

    # divergence providers
    for category in ('resource',
                     'source_section',
                     'destination_section',
                     'source',
                     'source_function',
                     'destination_function',
                     'source_project',
                     'destination_project',
                     'aggregate',
                     'price_currency',
                     'base_contribution',
                     'base_application',
                     'source_account',
                     'destination_account',
                     ):
      rule.newContent(
        portal_type='Category Membership Divergence Tester',
        title='%s divergence tester' % category,
        tested_property=category,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Net Converted Quantity Divergence Tester',
      title='quantity divergence tester',
      tested_property='quantity',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
    for property_id in ('start_date', 'stop_date'):
      rule.newContent(
        portal_type='DateTime Divergence Tester',
        title='%s divergence tester' % property_id,
        tested_property=property_id,
        quantity=0,
        divergence_provider=True,
        matching_provider=False)
    rule.newContent(
      portal_type='Float Divergence Tester',
      title='price divergence tester',
      tested_property='price',
      quantity=0,
      divergence_provider=True,
      matching_provider=False)
309 310 311 312

    rule.validate()
    transaction.commit()

313 314 315 316 317 318 319 320 321 322 323 324 325
  def _createDocument(self, portal_type, **kw):
    module = self.portal.getDefaultModule(portal_type=portal_type)
    return module.newContent(portal_type=portal_type, **kw)

  def _createProduct(self, **kw):
    return self._createDocument(self.product_portal_type, **kw)

  def _createNode(self, **kw):
    return self._createDocument(self.node_portal_type, **kw)

  def _createTradeCondition(self, **kw):
    self.trade_condition = self._createDocument(
        self.trade_condition_portal_type,
326
        title = self.id(),
327 328
        specialise_value=self.business_process, **kw)

329 330 331
  def _createRootDocumentLine(self, **kw):
    return self.root_document.newContent(
        portal_type=self.root_document_line_portal_type, **kw)
332 333 334 335 336 337

  def _createNodes(self):
    self.source, self.source_section = self._createNode(), self._createNode()
    self.destination, self.destination_section = self._createNode() \
        , self._createNode()

Łukasz Nowak's avatar
Łukasz Nowak committed
338 339 340 341 342 343 344 345
  def _createBusinessStateList(self):
    """Creates list of defaults states, set them on self as name_state property"""
    for state_name in ('ordered', 'delivered', 'invoiced', 'accounted',
        'paid'):
      state_document = self.createBusinessState(self.business_process,
        title=state_name)
      setattr(self,'%s_state' % state_name, state_document)

346 347
  def _createRootDocument(self):
    self.root_document = self._createDocument(self.root_document_portal_type,
348 349 350 351 352 353 354 355
        source_value = self.source,
        source_section_value = self.source_section,
        destination_value = self.destination,
        destination_section_value = self.destination_section,
        start_date = self.order_start_date,
        stop_date = self.order_stop_date,
        specialise_value = self.trade_condition)

356
  def _checkBPMSimulation(self):
357
    """Checks BPMised related simumation.
Łukasz Nowak's avatar
Łukasz Nowak committed
358 359 360

    Note: Simulation tree is the same, it is totally independent from
    BPM sequence"""
361 362
    # TODO:
    #  - gather errors into one list
363
    bpm_root_rule = self.root_document.getCausalityRelatedValue(
364
        portal_type='Applied Rule')
365
    # check that correct root rule applied
366
    self.assertEqual(bpm_root_rule.getSpecialiseValue().getPortalType(),
367
        self.root_rule_portal_type)
368 369 370
    root_simulation_movement_list = bpm_root_rule.contentValues()
    for root_simulation_movement in root_simulation_movement_list:
      self.assertEqual(root_simulation_movement.getPortalType(),
371
          'Simulation Movement')
372
      movement = root_simulation_movement.getOrderValue()
373
      property_problem_list = []
374 375
      # check some properties equality between delivery line and simulation
      # movement, gather errors
376 377 378
      for property in 'resource', 'price', 'start_date', 'stop_date', \
                      'source', 'destination', 'source_section', \
                      'destination_section':
379
        if movement.getProperty(property) != root_simulation_movement \
380 381
            .getProperty(property):
          property_problem_list.append('property %s movement %s '
382 383
              'simulation %s' % (property, movement.getProperty(property),
                root_simulation_movement.getProperty(property)))
384 385
      if len(property_problem_list) > 0:
        self.fail('\n'.join(property_problem_list))
386 387 388
      self.assertEqual(
        movement.getQuantity() * root_simulation_movement.getOrderRatio(),
        root_simulation_movement.getQuantity())
389 390
      # root rule is order or delivery - so below each movement invoicing one
      # is expected
391 392
      self.assertEquals(len(root_simulation_movement.contentValues()), 1)
      for bpm_invoicing_rule in root_simulation_movement.contentValues():
393 394
        self.assertEqual(bpm_invoicing_rule.getPortalType(), 'Applied Rule')
        self.assertEqual(bpm_invoicing_rule.getSpecialiseValue() \
395
            .getPortalType(), 'Invoicing Rule')
396
        # only one movement inside invoicing rule
397
        self.assertEquals(len(bpm_invoicing_rule.contentValues()), 1)
398 399 400 401 402 403
        for invoicing_simulation_movement in bpm_invoicing_rule \
            .contentValues():
          self.assertEqual(invoicing_simulation_movement.getPortalType(),
              'Simulation Movement')
          self.assertEqual(invoicing_simulation_movement.getCausalityValue(),
              self.invoice_path)
Łukasz Nowak's avatar
Łukasz Nowak committed
404
          property_problem_list = []
405
          # check equality of some properties, gather them
406
          for property in 'resource', 'price', 'start_date', \
Łukasz Nowak's avatar
Łukasz Nowak committed
407 408
            'stop_date', 'source', 'destination', 'source_section', \
            'destination_section':
409 410
            if movement.getProperty(property) != \
                invoicing_simulation_movement.getProperty(property):
Łukasz Nowak's avatar
Łukasz Nowak committed
411 412 413 414 415
              property_problem_list.append('property %s movement %s '
                  'simulation %s' % (property, movement.getProperty(property),
                    invoicing_simulation_movement.getProperty(property)))
          if len(property_problem_list) > 0:
            self.fail('\n'.join(property_problem_list))
416 417 418
          self.assertEqual(
            movement.getQuantity() * root_simulation_movement.getOrderRatio(),
            invoicing_simulation_movement.getQuantity())
419 420
          # simple check for trade model rule existence, without movements,
          # as no trade condition configured
421 422
          self.assertEquals(
              len(invoicing_simulation_movement.contentValues()), 1)
423 424 425 426 427 428 429 430 431 432
          for trade_model_rule in invoicing_simulation_movement \
              .contentValues():
            self.assertEqual(trade_model_rule.getPortalType(), 'Applied Rule')
            self.assertEqual(trade_model_rule.getSpecialiseValue() \
                .getPortalType(), 'Trade Model Rule')
            self.assertSameSet(trade_model_rule.contentValues(
              portal_type='Simulation Movement'), [])

class TestBPMEvaluationDefaultProcessMixin:
  def _createBusinessProcess(self):
433 434
    self.business_process = self.createBusinessProcess(title=self.id(),
        referential_date='start_date')
Łukasz Nowak's avatar
Łukasz Nowak committed
435
    self._createBusinessStateList()
436 437

    self.delivery_path = self.createBusinessPath(self.business_process,
438 439
        predecessor_value=self.ordered_state,
        successor_value=self.delivered_state,
Łukasz Nowak's avatar
Łukasz Nowak committed
440 441
        trade_phase='default/delivery',
        deliverable=1,
442 443
        completed_state_list=['started', 'stopped', 'delivered'],
        frozen_state_list=['started', 'stopped', 'delivered'],
Łukasz Nowak's avatar
Łukasz Nowak committed
444
        delivery_builder='portal_deliveries/bpm_sale_packing_list_builder',
Łukasz Nowak's avatar
Łukasz Nowak committed
445
        )
446 447

    self.invoice_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
448 449 450 451
        predecessor_value=self.delivered_state,
        successor_value=self.invoiced_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
452
        delivery_builder='portal_deliveries/bpm_sale_invoice_builder',
453 454 455
        trade_phase='default/invoicing')

    self.account_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
456 457 458 459
        predecessor_value=self.invoiced_state,
        successor_value=self.accounted_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
460 461 462
        trade_phase='default/accounting')

    self.pay_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
463 464 465 466
        predecessor_value=self.invoiced_state,
        successor_value=self.accounted_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
467 468 469 470 471 472
        trade_phase='default/payment')

    self.stepTic()

class TestBPMEvaluationDifferentProcessMixin:
  def _createBusinessProcess(self):
473 474
    self.business_process = self.createBusinessProcess(title=self.id(),
        referential_date='start_date')
Łukasz Nowak's avatar
Łukasz Nowak committed
475
    self._createBusinessStateList()
476 477

    self.invoice_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
478 479 480 481
        predecessor_value=self.ordered_state,
        successor_value=self.invoiced_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
482 483 484
        trade_phase='default/invoicing')

    self.account_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
485 486 487 488
        predecessor_value=self.invoiced_state,
        successor_value=self.accounted_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
489 490 491
        trade_phase='default/accounting')

    self.pay_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
492 493 494 495
        predecessor_value=self.accounted_state,
        successor_value=self.paid_state,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'],
496 497 498
        trade_phase='default/payment')

    self.delivery_path = self.createBusinessPath(self.business_process,
Łukasz Nowak's avatar
Łukasz Nowak committed
499 500 501 502 503 504
        predecessor_value=self.paid_state,
        successor_value=self.delivered_state,
        trade_phase='default/delivery',
        deliverable=1,
        completed_state_list=['delivered'],
        frozen_state_list=['stopped', 'delivered'])
505 506 507

    self.stepTic()

508
class GenericRuleTestsMixin:
509
  """Tests which are generic for BPMised Order, Delivery and Invoice Rule"""
510 511 512
  def test_transition(self):
    self.order_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
513 514
    self.stepTic()

515
    self._doFirstTransition(self.root_document)
516
    self.stepTic()
517
    self._checkBPMSimulation()
518

519 520 521
  def _split(self):
    """Invoke manual splitting"""
    ratio = .5 # hardcoded value, hopefully float friendly
Łukasz Nowak's avatar
Łukasz Nowak committed
522 523 524 525
    applied_rule = self.root_document.getCausalityRelatedValue(
        portal_type='Applied Rule')
    for movement in applied_rule.contentValues(
        portal_type='Simulation Movement'):
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
      new_movement = movement.Base_createCloneDocument(batch_mode=1)
      old_quantity = movement.getQuantity()
      movement.edit(
        quantity = old_quantity * ratio
      )

      new_movement.edit(
        quantity = old_quantity * (1 - ratio)
      )

    self.stepTic()

    # recalculate order ratio
    for movement in self.root_document.getMovementList():
      movement_quantity = movement.getQuantity()
      for simulation_movement in movement.getOrderRelatedValueList():
        new_ratio = simulation_movement.getQuantity() / movement_quantity
        simulation_movement.edit(order_ratio = new_ratio)
        if simulation_movement.getDelivery() is not None:
          simulation_movement.edit(delivery_ratio = new_ratio)

    # reexpand
    applied_rule.expand()
    self.stepTic()

    self._checkBPMSimulation()

  def test_transition_split(self):
    self.order_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
    self.stepTic()

    self._doFirstTransition(self.root_document)
    self.stepTic()
    self._checkBPMSimulation()

    self._split()

    # expand
    self.root_document.edit(title = self.root_document.getTitle() + 'a')

    self.stepTic()
    self._checkBPMSimulation()

  def test_transition_split_line_add(self):
    self.test_transition_split()
    self.order_line_2 = self._createRootDocumentLine(
        resource_value = self._createProduct(), quantity = 4, price = 2)
    self.stepTic()
    self._checkBPMSimulation()

  def test_transition_split_line_add_split(self):
    self.test_transition_split_line_add()

    # second split
    self._split()

    # expand
    self.root_document.edit(title = self.root_document.getTitle() + 'a')

    self.stepTic()
    self._checkBPMSimulation()

589 590
  def test_transition_line_edit(self):
    self.test_transition()
591 592
    self.order_line.edit(quantity = 8, price = 6)
    self.stepTic()
593
    self._checkBPMSimulation()
594

595 596 597
  def test_transition_line_edit_add(self):
    self.test_transition_line_edit()
    self.order_line_2 = self._createRootDocumentLine(
598 599
        resource_value = self._createProduct(), quantity = 4, price = 2)
    self.stepTic()
600
    self._checkBPMSimulation()
601

602 603 604
  def test_transition_line_edit_add_many_transactions(self):
    self.test_transition_line_edit()
    self.order_line_9 = self._createRootDocumentLine()
605
    self.stepTic()
606
    self._checkBPMSimulation()
607 608 609

    self.order_line_9.edit(resource_value = self._createProduct())
    self.stepTic()
610
    self._checkBPMSimulation()
611 612 613

    self.order_line_9.edit(quantity = 1)
    self.stepTic()
614
    self._checkBPMSimulation()
615 616 617

    self.order_line_9.edit(price = 33)
    self.stepTic()
618
    self._checkBPMSimulation()
619 620 621

    self.order_line_9.edit(resource_value = self._createProduct())
    self.stepTic()
622
    self._checkBPMSimulation()
623

624 625
  def test_transition_line_edit_add_same_resource(self):
    self.test_transition_line_edit()
626
    resource = self.order_line.getResourceValue()
627 628
    self.order_line_10 = self._createRootDocumentLine(
      resource_value = resource, quantity = 9, price = 2)
629
    self.stepTic()
630
    self._checkBPMSimulation()
631

632 633
  def test_transition_line_edit_add_same_resource_edit_again(self):
    self.test_transition_line_edit_add_same_resource()
634

635 636 637
    self.root_document.edit(title = self.root_document.getTitle() + 'a' )
    self.stepTic()
    self._checkBPMSimulation()
638

639
class TestOrder(TestBPMEvaluationMixin, GenericRuleTestsMixin):
640
  """Check BPMised Order Rule behaviour"""
641 642
  root_document_portal_type = 'Sale Order'
  root_document_line_portal_type = 'Sale Order Line'
643
  root_rule_portal_type = 'Order Rule'
644

645 646
  def _doFirstTransition(self, document):
    document.plan()
647

648 649 650
  def test_confirming(self):
    self.order_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
651 652
    self.stepTic()

653
    self.root_document.confirm()
654
    self.stepTic()
655 656 657 658 659 660 661 662 663 664
    self._checkBPMSimulation()
    self.assertEqual(
      2,
      len(self.root_document.getCausalityRelatedList())
    )
    self.assertEqual(
      'Applied Rule',
      self.root_document.getCausalityRelatedValue(
        portal_type='Applied Rule').getPortalType()
    )
665

666 667 668 669 670
    self.assertEqual(
      self.packing_list_portal_type,
      self.root_document.getCausalityRelatedValue(
        portal_type=self.packing_list_portal_type).getPortalType()
    )
671

672 673 674 675
class TestPackingList(TestBPMEvaluationMixin, GenericRuleTestsMixin):
  """Check BPM Delivery Rule behaviour"""
  root_document_portal_type = 'Sale Packing List'
  root_document_line_portal_type = 'Sale Packing List Line'
676
  root_rule_portal_type = 'Delivery Rule'
677

678 679 680 681 682 683 684 685 686 687 688
  def _packDelivery(self):
    """Packs delivery fully, removes possible containers before"""
    self.root_document.deleteContent(self.root_document.contentIds(
      filter={'portal_type':'Container'}))
    cont = self.root_document.newContent(portal_type='Container')
    for movement in self.root_document.getMovementList():
      cont.newContent(portal_type='Container Line',
        resource = movement.getResource(), quantity = movement.getQuantity())
    self.stepTic()
    self._checkBPMSimulation()

689 690 691
  def _doFirstTransition(self, document):
    document.confirm()

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
  def test_starting(self):
    self.delivery_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
    self.stepTic()

    self.root_document.confirm()
    self.stepTic()
    self._checkBPMSimulation()

    self._packDelivery()

    self.root_document.start()
    self.stepTic()
    self._checkBPMSimulation()

    self.assertEqual(
      2,
      len(self.root_document.getCausalityRelatedList())
    )
    self.assertEqual(
      'Applied Rule',
      self.root_document.getCausalityRelatedValue(
        portal_type='Applied Rule').getPortalType()
    )

    self.assertEqual(
      self.invoice_portal_type,
      self.root_document.getCausalityRelatedValue(
        portal_type=self.invoice_portal_type).getPortalType()
    )

723 724 725
class TestInvoice(TestBPMEvaluationMixin, GenericRuleTestsMixin):
  """Check BPM Invoice Rule behaviour"""
  # not implemented yet
726 727
  pass

728 729 730
class TestOrderDefaultProcess(TestOrder,
    TestBPMEvaluationDefaultProcessMixin):
  pass
731

732 733 734
class TestPackingListDefaultProcess(TestPackingList,
    TestBPMEvaluationDefaultProcessMixin):
  pass
735

736 737 738
class TestInvoiceDefaultProcess(TestInvoice,
    TestBPMEvaluationDefaultProcessMixin):
  pass
739

740 741
class TestOrderDifferentProcess(TestOrder,
    TestBPMEvaluationDifferentProcessMixin):
742 743 744 745
  def test_confirming(self):
    # in current BPM configuration nothing shall be built
    # as soon as test business process will be finished, it shall built proper
    # delivery
746 747
    self.order_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
748 749
    self.stepTic()

750
    self.root_document.confirm()
751
    self.stepTic()
752
    self._checkBPMSimulation()
753 754
    self.assertEqual(
      1,
755
      len(self.root_document.getCausalityRelatedList())
756 757 758
    )
    self.assertEqual(
      'Applied Rule',
759
      self.root_document.getCausalityRelatedValue().getPortalType()
760
    )
761

762 763
class TestPackingListDifferentProcess(TestPackingList,
    TestBPMEvaluationDifferentProcessMixin):
764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
  def test_starting(self):
    self.delivery_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = 10, price = 5)
    self.stepTic()

    self.root_document.confirm()
    self.stepTic()
    self._checkBPMSimulation()

    self._packDelivery()
    self.root_document.start()
    self.stepTic()
    self._checkBPMSimulation()

    self.assertEqual(
      1,
      len(self.root_document.getCausalityRelatedList())
    )
    self.assertEqual(
      'Applied Rule',
      self.root_document.getCausalityRelatedValue(
        portal_type='Applied Rule').getPortalType()
    )
787

788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971
class TestDivergenceSolving(TestBPMEvaluationMixin,
    TestBPMEvaluationDefaultProcessMixin):
  # FIXME: make it more generic:
  #  * use erp5_dummy_movement
  #  * use dummy Rule class
  #  * do manual reexpanding
  root_document_portal_type = 'Sale Order'
  root_document_line_portal_type = 'Sale Order Line'
  root_rule_portal_type = 'Order Rule'

  quantity = 10
  price = 5
  new_quantity = 9

  def afterSetUp(self):
    TestBPMEvaluationMixin.afterSetUp(self)

    self.order_line = self._createRootDocumentLine(
      resource_value = self._createProduct(), quantity = self.quantity,
      price = self.price)

    self.stepTic()

    self.root_document.confirm()
    self.stepTic()
    self._checkBPMSimulation()
    self.assertEqual(
      2,
      len(self.root_document.getCausalityRelatedList())
    )
    self.assertEqual(
      'Applied Rule',
      self.root_document.getCausalityRelatedValue(
        portal_type='Applied Rule').getPortalType()
    )

    self.assertEqual(
      self.packing_list_portal_type,
      self.root_document.getCausalityRelatedValue(
        portal_type=self.packing_list_portal_type).getPortalType()
    )
    self.packing_list = self.root_document.getCausalityRelatedValue(
        portal_type=self.packing_list_portal_type)
    self.movement = self.packing_list.getMovementList()[0]
    self.simulation_movement = self.movement.getDeliveryRelatedValue()

    # change quantity on movement
    self.movement.edit(quantity = self.new_quantity)
    self.stepTic()

    self.assertEqual('diverged', self.packing_list.getCausalityState())
    self.assertEqual(self.quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual(self.new_quantity, self.movement.getQuantity())

    divergence_list = self.movement.getDivergenceList()

    self.assertEqual(1, len(divergence_list))

    self.divergence = divergence_list[0]

    self.assertEqual('quantity', self.divergence.divergence_scope)
    self.assertEqual(self.new_quantity, self.divergence.decision_value)
    self.assertEqual(self.quantity, self.divergence.prevision_value)
    self.assertEqual(self.simulation_movement,
        self.divergence.simulation_movement)

  def test_divergence_adopt(self):
    decision = DivergenceSolutionDecision(self.divergence, 'adopt')
    self.movement.solve([decision])
    transaction.commit()

    self.assertEqual(self.quantity, self.simulation_movement.getQuantity())
    self.assertEqual(self.quantity, self.movement.getQuantity())

    simulation_movement_solution_history_list = self.simulation_movement \
        .divergence_solution_history['quantity']
    self.assertEqual(1, len(simulation_movement_solution_history_list))
    simulation_movement_solution \
        = simulation_movement_solution_history_list[0]

    self.assertEqual(decision, simulation_movement_solution)

    # reexpand
    self.packing_list.Delivery_updateAppliedRule()
    transaction.commit()
    self.tic()
    self.assertEqual(self.quantity, self.simulation_movement.getQuantity())
    self.assertEqual('solved', self.packing_list.getCausalityState())

  def test_divergence_accept(self):
    decision = DivergenceSolutionDecision(self.divergence, 'accept',
        'Distribute', 'CopyAndPropagate')
    self.movement.solve([decision])
    transaction.commit()

    self.assertEqual(self.new_quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual(self.new_quantity, self.movement.getQuantity())

    simulation_movement_solution_history_list = self.simulation_movement \
        .divergence_solution_history['quantity']
    self.assertEqual(1, len(simulation_movement_solution_history_list))
    simulation_movement_solution \
        = simulation_movement_solution_history_list[0]

    self.assertEqual(decision, simulation_movement_solution)

    # reexpand
    self.packing_list.Delivery_updateAppliedRule()
    transaction.commit()
    self.tic()
    # property is not forced - so change after expand
    self.assertEqual(self.quantity, self.simulation_movement.getQuantity())
    self.assertEqual('diverged', self.packing_list.getCausalityState())

  def test_divergence_accept_force(self):
    decision = DivergenceSolutionDecision(self.divergence, 'accept',
        'Distribute', 'CopyAndPropagate', True)
    self.movement.solve([decision])
    transaction.commit()

    self.assertEqual(self.new_quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual(self.new_quantity, self.movement.getQuantity())

    simulation_movement_solution_history_list = self.simulation_movement \
        .divergence_solution_history['quantity']
    self.assertEqual(1, len(simulation_movement_solution_history_list))
    simulation_movement_solution \
        = simulation_movement_solution_history_list[0]

    self.assertEqual(decision, simulation_movement_solution)

    # reexpand
    self.packing_list.Delivery_updateAppliedRule()
    transaction.commit()
    self.tic()
    # property is forced - so no change after expand
    self.assertEqual(self.new_quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual('solved', self.packing_list.getCausalityState())

  def test_divergence_split(self):
    split_kw = {}
    split_kw.update(start_date = DateTime(), stop_date = DateTime())
    decision = DivergenceSolutionDecision(self.divergence, 'split', None,
        'SplitAndDefer', split_kw = split_kw)
    self.movement.solve([decision])
    transaction.commit()

    self.assertEqual(self.new_quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual(self.new_quantity, self.movement.getQuantity())

    simulation_movement_solution_history_list = self.simulation_movement \
        .divergence_solution_history['quantity']
    self.assertEqual(1, len(simulation_movement_solution_history_list))
    simulation_movement_solution \
        = simulation_movement_solution_history_list[0]

    self.assertEqual(decision, simulation_movement_solution)

    new_simulation_movement_list = [simulation_movement \
        for simulation_movement \
        in self.simulation_movement.getParentValue().contentValues(
          portal_type='Simulation Movement') \
        if simulation_movement != self.simulation_movement]
    self.assertEqual(1, len(new_simulation_movement_list))
    new_simulation_movement = new_simulation_movement_list[0]

    self.assertEqual(self.quantity - self.new_quantity,
        new_simulation_movement.getQuantity())

    # reexpand
    self.packing_list.Delivery_updateAppliedRule()
    transaction.commit()
    self.tic()
    self.assertEqual(self.new_quantity,
        self.simulation_movement.getQuantity())
    self.assertEqual(self.quantity - self.new_quantity,
        new_simulation_movement.getQuantity())
    self.assertEqual('solved', self.packing_list.getCausalityState())

972 973
class TestInvoiceDifferentProcess(TestInvoice,
    TestBPMEvaluationDifferentProcessMixin):
974 975 976 977 978 979
  pass

def test_suite():
  suite = unittest.TestSuite()
  suite.addTest(unittest.makeSuite(TestOrderDefaultProcess))
  suite.addTest(unittest.makeSuite(TestPackingListDefaultProcess))
980 981
#  suite.addTest(unittest.makeSuite(TestInvoiceDefaultProcess))

982 983
  suite.addTest(unittest.makeSuite(TestOrderDifferentProcess))
  suite.addTest(unittest.makeSuite(TestPackingListDifferentProcess))
984 985
#  suite.addTest(unittest.makeSuite(TestInvoiceDifferentProcess))

986
  suite.addTest(unittest.makeSuite(TestDivergenceSolving))
987
  return suite