testOrderBuilder.py 17.4 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
##############################################################################
3
#
4
# Copyright (c) 2008 Nexedi SA and Contributors. All Rights Reserved.
5 6 7
#          Łukasz Nowak <lukasz.nowak@ventis.com.pl>
#
# WARNING: This program as such is intended to be used by professional
8
# programmers who take the whole responsibility of assessing all potential
9 10
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
11
# guarantees and support are strongly adviced to contract a Free Software
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# 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.
#
##############################################################################

import unittest

from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
33
from DateTime import DateTime
34 35
from Products.ERP5Type.tests.Sequence import SequenceList
from Products.ERP5.tests.testOrder import TestOrderMixin
36 37
from Products.ERP5.tests.testInventoryAPI import InventoryAPITestCase
from Products.ERP5Type.tests.utils import createZODBPythonScript
38

39
class TestOrderBuilderMixin(TestOrderMixin, InventoryAPITestCase):
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

  run_all_test = 1

  order_builder_portal_type = 'Order Builder'

  order_module = 'purchase_order_module'
  order_portal_type = 'Purchase Order'
  order_line_portal_type = 'Purchase Order Line'
  order_cell_portal_type = 'Purchase Order Cell'

  packing_list_portal_type = 'Internal Packing List'
  packing_list_line_portal_type = 'Internal Packing List Line'
  packing_list_cell_portal_type = 'Internal Packing List Cell'

  # hardcoded values
  order_builder_hardcoded_time_diff = 10.0

  # defaults
  decrease_quantity = 1.0
59 60
  max_delay = 0.0
  min_flow = 0.0
61 62 63 64 65 66

  def afterSetUp(self):
    """
    Make sure to not use special apparel setting from TestOrderMixin
    """
    self.createCategories()
67
    self.validateRules()
68 69 70 71
    InventoryAPITestCase.afterSetUp(self)
    self.node_1 = self.portal.organisation_module.newContent(title="Node 1")
    self.node_2 = self.portal.organisation_module.newContent(title="Node 2")
    self.pinDateTime(None)
72

73 74 75 76
  def assertDateAlmostEquals(self, first_date, second_date):
    self.assertTrue(abs(first_date - second_date) < 1.0/86400,
                    "%r != %r" % (first_date, second_date))

77
  def stepSetMaxDelayOnResource(self, sequence):
78 79 80 81
    """
    Sets max_delay on resource
    """
    resource = sequence.get('resource')
82
    resource.edit(purchase_supply_line_max_delay=self.max_delay)
83

84
  def stepSetMinFlowOnResource(self, sequence):
85 86 87 88
    """
    Sets min_flow on resource
    """
    resource = sequence.get('resource')
89
    resource.edit(purchase_supply_line_min_flow=self.min_flow)
90

91
  def stepFillOrderBuilder(self, sequence):
92 93 94
    self.fillOrderBuilder(sequence=sequence)

  def fillOrderBuilder(self, sequence=None):
95 96 97
    """
    Fills Order Builder with proper quantites
    """
98 99 100 101 102
    order_builder = self.order_builder
    if sequence is not None:
      organisation = sequence.get('organisation')
    else:
      organisation = None
103 104 105 106 107 108 109 110

    order_builder.edit(
      delivery_module = self.order_module,
      delivery_portal_type = self.order_portal_type,
      delivery_line_portal_type = self.order_line_portal_type,
      delivery_cell_portal_type = self.order_cell_portal_type,
      destination_value = organisation,
      resource_portal_type = self.resource_portal_type,
111
      simulation_select_method_id='generateMovementListForStockOptimisation',
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
    order_builder.newContent(
      portal_type = 'Category Movement Group',
      collect_order_group='delivery',
      tested_property=['source', 'destination',
                       'source_section', 'destination_section'],
      int_index=1
      )
    order_builder.newContent(
      portal_type = 'Property Movement Group',
      collect_order_group='delivery',
      tested_property=['start_date', 'stop_date'],
      int_index=2
      )

    order_builder.newContent(
      portal_type = 'Category Movement Group',
      collect_order_group='line',
      tested_property=['resource'],
      int_index=1
      )
    order_builder.newContent(
      portal_type = 'Base Variant Movement Group',
      collect_order_group='line',
      int_index=2
      )

    order_builder.newContent(
      portal_type = 'Variant Movement Group',
      collect_order_group='cell',
      int_index=1
      )

145
  def stepCheckGeneratedDocumentListVariated(self, sequence):
146 147 148 149 150 151
    """
    Checks documents generated by Order Builders with its properties for variated resource
    """
    organisation = sequence.get('organisation')
    resource = sequence.get('resource')

152 153 154
    # XXX: add support for more generated documents
    order, = sequence.get('generated_document_list')
    self.assertEqual(order.getDestinationValue(), organisation)
155 156
    self.assertDateAlmostEquals(order.getStartDate(), self.wanted_start_date)
    self.assertDateAlmostEquals(order.getStopDate(), self.wanted_stop_date)
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

    # XXX: ... and for more lines/cells too
    order_line, = order.contentValues(portal_type=self.order_line_portal_type)
    self.assertEqual(order_line.getResourceValue(), resource)
    self.assertEqual(order_line.getTotalQuantity(),
      sum(self.wanted_quantity_matrix.itervalues()))

    quantity_matrix = {}
    for cell in order_line.contentValues(portal_type=self.order_cell_portal_type):
      key = cell.getProperty('membership_criterion_category')
      self.assertFalse(key in quantity_matrix)
      quantity_matrix[key] = cell.getQuantity()
    self.assertEqual(quantity_matrix, self.wanted_quantity_matrix)

  def stepCheckGeneratedDocumentList(self, sequence):
172 173 174 175 176 177
    """
    Checks documents generated by Order Builders with its properties
    """
    organisation = sequence.get('organisation')
    resource = sequence.get('resource')

178 179 180
    # XXX: add support for more generated documents
    order, = sequence.get('generated_document_list')
    self.assertEqual(order.getDestinationValue(), organisation)
181 182
    self.assertDateAlmostEquals(self.wanted_start_date, order.getStartDate())
    self.assertDateAlmostEquals(self.wanted_stop_date, order.getStopDate())
183

184 185 186
    # XXX: ... and for more lines/cells too
    order_line, = order.contentValues(portal_type=self.order_line_portal_type)
    self.assertEqual(order_line.getResourceValue(), resource)
187
    self.assertEqual(order_line.getTotalQuantity(), self.wanted_quantity)
188

189
  def stepBuildOrderBuilder(self, sequence):
190 191 192 193 194
    """
    Invokes build method for Order Builder
    """
    order_builder = sequence.get('order_builder')
    generated_document_list = order_builder.build()
195
    sequence.set('generated_document_list', generated_document_list)
196

197
  def createOrderBuilder(self):
198 199 200
    """
    Creates empty Order Builder
    """
201 202
    order_builder = self.portal.portal_orders.newContent(
      portal_type=self.order_builder_portal_type)
203 204 205 206 207
    self.order_builder = order_builder
    return order_builder

  def stepCreateOrderBuilder(self, sequence):
    order_builder = self.createOrderBuilder()
208
    sequence.set('order_builder', order_builder)
209

210
  def stepDecreaseOrganisationResourceQuantityVariated(self, sequence):
211 212 213 214 215 216 217 218
    """
    Creates movement with variation from organisation to None.
    Using Internal Packing List, confirms it.

    Note: Maybe use InventoryAPITestCase::_makeMovement instead of IPL ?
    """
    organisation = sequence.get('organisation')
    resource = sequence.get('resource')
219

220 221 222 223 224 225 226
    packing_list_module = self.portal.getDefaultModule(
      portal_type = self.packing_list_portal_type
    )

    packing_list = packing_list_module.newContent(
      portal_type = self.packing_list_portal_type,
      source_value = organisation,
227
      start_date = self.datetime+10,
228
      specialise = self.business_process,
229 230 231 232 233 234 235 236
    )

    packing_list_line = packing_list.newContent(
      portal_type = self.packing_list_line_portal_type,
      resource_value = resource,
      quantity = self.decrease_quantity,
    )

237 238 239 240 241 242 243
    self.decrease_quantity_matrix = {
      'variation/%s/blue' % resource.getRelativeUrl() : 1.0,
      'variation/%s/green' % resource.getRelativeUrl() : 2.0,
    }

    self.wanted_quantity_matrix = self.decrease_quantity_matrix.copy()

244
    packing_list_line.setVariationCategoryList(
245
      self.decrease_quantity_matrix.keys(),
246
    )
247

248 249 250 251 252 253 254
    self.tic()

    base_id = 'movement'
    cell_key_list = list(packing_list_line.getCellKeyList(base_id=base_id))
    cell_key_list.sort()

    for cell_key in cell_key_list:
255
      cell = packing_list_line.newCell(base_id=base_id,
256 257 258 259 260 261 262 263
                                portal_type=self.packing_list_cell_portal_type, *cell_key)
      cell.edit(mapped_value_property_list=['price','quantity'],
                quantity=self.decrease_quantity_matrix[cell_key[0]],
                predicate_category_list=cell_key,
                variation_category_list=cell_key)

    packing_list.confirm()

264
  def stepDecreaseOrganisationResourceQuantity(self, sequence):
265 266 267 268 269 270 271 272
    """
    Creates movement from organisation to None.
    Using Internal Packing List, confirms it.

    Note: Maybe use InventoryAPITestCase::_makeMovement instead of IPL ?
    """
    organisation = sequence.get('organisation')
    resource = sequence.get('resource')
273

274 275 276 277 278 279 280
    packing_list_module = self.portal.getDefaultModule(
      portal_type = self.packing_list_portal_type
    )

    packing_list = packing_list_module.newContent(
      portal_type = self.packing_list_portal_type,
      source_value = organisation,
281
      start_date = self.datetime+10,
282
      specialise = self.business_process,
283 284 285 286 287 288 289 290 291
    )

    packing_list.newContent(
      portal_type = self.packing_list_line_portal_type,
      resource_value = resource,
      quantity = self.decrease_quantity,
    )

    packing_list.confirm()
292

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  def stepCreateVariatedResource(self, sequence=None, sequence_list=None, \
                                 **kw):
    """
      Create a resource with variation
    """
    portal = self.getPortal()
    resource_module = portal.getDefaultModule(self.resource_portal_type)
    resource = resource_module.newContent(portal_type=self.resource_portal_type)
    resource.edit(
      title = "VariatedResource%s" % resource.getId(),
      variation_base_category_list = ['variation'],
    )
    for color in ['blue', 'green']:
      resource.newContent(portal_type='Product Individual Variation',
                          id=color, title=color)
    sequence.edit(resource=resource)

310 311 312 313 314 315
class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
  """
    Test Order Builder functionality
  """
  run_all_test = 1

316 317
  resource_portal_type = "Product"

318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
  common_sequence_string = """
      CreateOrganisation
      CreateNotVariatedResource
      SetMaxDelayOnResource
      SetMinFlowOnResource
      Tic
      DecreaseOrganisationResourceQuantity
      Tic
      CreateOrderBuilder
      FillOrderBuilder
      Tic
      BuildOrderBuilder
      Tic
      CheckGeneratedDocumentList
      """
333 334 335 336 337 338 339 340 341 342 343

  def getTitle(self):
    return "Order Builder"

  def test_01_simpleOrderBuilder(self, quiet=0, run=run_all_test):
    """
    Test simple Order Builder
    """
    if not run: return

    self.wanted_quantity = 1.0
344
    self.wanted_start_date = DateTime(
345
      str(self.datetime + self.order_builder_hardcoded_time_diff))
346

347
    self.wanted_stop_date = self.wanted_start_date
348 349 350 351 352 353 354 355 356 357 358

    sequence_list = SequenceList()
    sequence_list.addSequenceString(self.common_sequence_string)
    sequence_list.play(self)

  def test_01a_simpleOrderBuilderVariatedResource(self, quiet=0, run=run_all_test):
    """
    Test simple Order Builder for Variated Resource
    """
    if not run: return

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
    sequence_string = """
      CreateOrganisation
      CreateVariatedResource
      SetMaxDelayOnResource
      SetMinFlowOnResource
      Tic
      DecreaseOrganisationResourceQuantityVariated
      Tic
      CreateOrderBuilder
      FillOrderBuilder
      Tic
      BuildOrderBuilder
      Tic
      CheckGeneratedDocumentListVariated
      """
374 375

    self.wanted_quantity = 1.0
376
    self.wanted_start_date = DateTime(
377
      str(self.datetime +
378
          self.order_builder_hardcoded_time_diff))
379

380
    self.wanted_stop_date = self.wanted_start_date
381 382 383 384 385 386 387 388 389 390 391

    sequence_list = SequenceList()
    sequence_list.addSequenceString(sequence_string)
    sequence_list.play(self)

  def test_02_maxDelayResourceOrderBuilder(self, quiet=0, run=run_all_test):
    """
    Test max_delay impact on generated order start date
    """
    if not run: return

392
    self.max_delay = 4.0
393 394

    self.wanted_quantity = 1.0
395
    self.wanted_start_date = DateTime(
396
      str(self.datetime - self.max_delay
397
          + self.order_builder_hardcoded_time_diff))
398

399
    self.wanted_stop_date = DateTime(
400
      str(self.datetime + self.order_builder_hardcoded_time_diff))
401 402 403 404 405 406 407 408 409 410 411 412

    sequence_list = SequenceList()
    sequence_list.addSequenceString(self.common_sequence_string)
    sequence_list.play(self)

  def test_03_minFlowResourceOrderBuilder(self, quiet=0, run=run_all_test):
    """
    Test min_flow impact on generated order line quantity
    """
    if not run: return

    self.wanted_quantity = 1.0
413
    self.wanted_start_date = DateTime(
414
      str(self.datetime + self.order_builder_hardcoded_time_diff))
415

416
    self.wanted_stop_date = self.wanted_start_date
417 418 419 420 421 422 423

    sequence_list = SequenceList()
    sequence_list.addSequenceString(self.common_sequence_string)

    # case when min_flow > decreased_quantity
    self.min_flow = 15.0

424
    self.wanted_quantity = self.min_flow
425 426 427

    sequence_list.play(self)

428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
  def createSelectMethodForBuilder(self):
    portal = self.getPortal()

  def checkOrderBuilderStockOptimisationResult(self, expected_result, **kw):
    result_list = [(x.getResource(), x.getQuantity(),
                    x.getStartDate().strftime("%Y/%m/%d"),
                    x.getStopDate().strftime("%Y/%m/%d")) for x in \
                    self.order_builder.generateMovementListForStockOptimisation(**kw)]
    result_list.sort()
    expected_result.sort()
    self.assertEqual(expected_result, result_list)

  def test_04_generateMovementListWithDateInThePast(self):
    """
    If we can not find a future date for stock optimisation, make sure to
    take current date as default value (before if no date was found, no
    result was returned, introducing risk to forget ordering something, this
    could be big issue in real life)
    """
    node_1 = self.node_1
    fixed_date = DateTime('2016/08/30')
    self.pinDateTime(fixed_date)
    self.createOrderBuilder()
    self.fillOrderBuilder()
    node_1_uid = node_1.getUid()
    self.checkOrderBuilderStockOptimisationResult([], node_uid=node_1.getUid())
    self._makeMovement(quantity=-3, destination_value=node_1, simulation_state='confirmed')
    resource_url = self.resource.getRelativeUrl()
    self.checkOrderBuilderStockOptimisationResult(
       [(resource_url, 3.0, '2016/08/30', '2016/08/30')], node_uid=node_1.getUid())

  def test_05_generateMovementListForStockOptimisationForSeveralNodes(self):
    """
    It's common to have a warehouse composed of subparts, each subpart could have
    it's own subpart, etc. So we have to look at stock optimisation for the whole
    warehouse, since every resource might be stored in several distinct sub parts.
    Make sure that stock optimisation works fine in such case.
    """
    node_1 = self.node_1
    node_2 = self.node_2
    resource = self.resource
    self.createOrderBuilder()
    self.fillOrderBuilder()
    fixed_date = DateTime('2016/08/10')
    self.pinDateTime(fixed_date)
    resource_url = self.resource.getRelativeUrl()
    node_uid_list = [node_1.getUid(), self.node_2.getUid()]
    def checkStockOptimisationForTwoNodes(expected_result):
      self.checkOrderBuilderStockOptimisationResult(expected_result, node_uid=node_uid_list,
                                                    group_by_node=0)
    checkStockOptimisationForTwoNodes([])
    self._makeMovement(quantity=-3, destination_value=node_1, simulation_state='confirmed',
                       start_date=DateTime('2016/08/20'))
    checkStockOptimisationForTwoNodes([(resource_url, 3.0, '2016/08/20', '2016/08/20')])
    self._makeMovement(quantity=-2, destination_value=node_1, simulation_state='confirmed',
                       start_date=DateTime('2016/08/18'))
    checkStockOptimisationForTwoNodes([(resource_url, 5.0, '2016/08/18', '2016/08/18')])
    self._makeMovement(quantity=-7, destination_value=node_2, simulation_state='confirmed',
                       start_date=DateTime('2016/08/19'))
    checkStockOptimisationForTwoNodes([(resource_url, 12.0, '2016/08/18', '2016/08/18')])
    self._makeMovement(quantity=11, destination_value=node_2, simulation_state='confirmed',
                       start_date=DateTime('2016/08/16'))
    checkStockOptimisationForTwoNodes([(resource_url, 1.0, '2016/08/20', '2016/08/20')])
    self._makeMovement(quantity=7, destination_value=node_1, simulation_state='confirmed',
                       start_date=DateTime('2016/08/15'))
    checkStockOptimisationForTwoNodes([])

495 496 497 498
def test_suite():
  suite = unittest.TestSuite()
  suite.addTest(unittest.makeSuite(TestOrderBuilder))
  return suite