Commit e72e22f6 authored by Julien Muchembled's avatar Julien Muchembled

Delivery: add support for composition

CompositionMixin provides a 'asComposedDocument' method replacing
find{Effective,}SpecialiseValueList on Trade Condition.

Some deprecated code in TradeCondition is moved back to PaySheetModel for
compatibility reasons.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@34217 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 9b907283
......@@ -87,9 +87,7 @@ if price_currency:\n
\n
def copyPaymentCondition(paysheet, model):\n
filter_dict = {\'portal_type\': \'Payment Condition\'}\n
effective_model_list = model.findEffectiveSpecialiseValueList(\\\n
context=model,\n
start_date=paysheet.getStartDate(), stop_date=paysheet.getStopDate())\n
effective_model_list = model.findEffectiveSpecialiseValueList(paysheet)\n
for effective_model in effective_model_list:\n
to_copy = effective_model.contentIds(filter=filter_dict)\n
if len(to_copy) > 0 :\n
......
......@@ -153,7 +153,7 @@ def getPaymentConditionText(order):\n
def getSocialOrganisationValue():\n
model = context.getSpecialiseValue()\n
if model is not None:\n
business_process_list = model.findSpecialiseValueList(\\\n
business_process_list = model.findEffectiveSpecialiseValueList(\\\n
context=context, portal_type_list=[\'Business Process\'])\n
business_process = None\n
if len(business_process_list):\n
......
559
\ No newline at end of file
560
\ No newline at end of file
......@@ -36,11 +36,12 @@ from Products.ERP5Type import Permissions, PropertySheet, interfaces
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
from Products.ERP5Type.XMLObject import XMLObject
from Products.ERP5.Document.ImmobilisationDelivery import ImmobilisationDelivery
from Products.ERP5.mixin.composition import CompositionMixin
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
from zLOG import LOG, PROBLEM
class Delivery(XMLObject, ImmobilisationDelivery):
class Delivery(XMLObject, ImmobilisationDelivery, CompositionMixin):
"""
Each time delivery is modified, it MUST launch a reindexing of
inventories which are related to the resources contained in the Delivery
......
......@@ -30,8 +30,10 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5.Document.TradeCondition import TradeCondition
from Products.ERP5Type.XMLMatrix import XMLMatrix
from Products.ERP5.Document.PaySheetTransaction import PaySheetTransaction
class PaySheetModel(TradeCondition):
class PaySheetModel(TradeCondition, XMLMatrix):
"""A PaySheetModel defines calculation rules for paysheets.
PaySheetModel are used to define calculating rules specific to a
......@@ -64,3 +66,74 @@ class PaySheetModel(TradeCondition):
, PropertySheet.DefaultAnnotationLine
)
security.declareProtected( Permissions.AccessContentsInformation, 'getCell')
def getCell(self, *args, **kw):
'''Overload the function getCell to be able to search a cell on the
inheritance model tree if the cell is not found on current one.
'''
paysheet = kw.get('paysheet')
if paysheet is None:
from Products.ERP5Type.Document import newTempPaySheetTransaction
paysheet = newTempPaySheetTransaction(self.getPortalObject(), '',
specialise_value=self)
model_list = self.findEffectiveSpecialiseValueList(paysheet)
for specialised_model in model_list:
cell = XMLMatrix.getCell(specialised_model, *args, **kw)
if cell is not None:
return cell
security.declareProtected(Permissions.AccessContentsInformation,
'getReferenceDict')
def getReferenceDict(self, portal_type_list, property_list=None):
"""Return a dict containing all id's of the objects contained in
this model and corresponding to the given portal_type. The key of the dict
are the reference (or id if no reference)
"""
if property_list is None:
property_list=[]
reference_dict = {}
object_list = self.contentValues(portal_type=portal_type_list,
sort_on='id')
for obj in object_list:
keep = (len(property_list) == 0)
for property_ in property_list:
if obj.hasProperty(property_):
keep = 1
break
if keep:
reference_dict[obj.getProperty('reference', obj.getId())] = obj.getId()
return reference_dict
security.declareProtected(Permissions.AccessContentsInformation,
'getInheritanceReferenceDict')
def getInheritanceReferenceDict(self, context, portal_type_list,
property_list=None):
'''Returns a dict with the model url as key and a list of reference as
value. A Reference can appear only one time in the final output.
If property_list is not empty, documents which don't have any of theses
properties will be skipped.
'''
reference_list = []
model_reference_dict = {}
for model in self.findEffectiveSpecialiseValueList(context):
id_list = []
model_reference_list = model.getReferenceDict(
portal_type_list, property_list=property_list)
for reference in model_reference_list.keys():
if reference not in reference_list:
reference_list.append(reference)
id_list.append(model_reference_list[reference])
if len(id_list) != 0:
model_reference_dict[model.getRelativeUrl()]=id_list
return model_reference_dict
security.declareProtected(Permissions.AccessContentsInformation,
'getModelInheritanceEffectiveProperty')
def getModelInheritanceEffectiveProperty(self, paysheet, property_name):
"""Get a property from an effective model
"""
model_list = self.findEffectiveSpecialiseValueList(paysheet)
for specialised_model in model_list:
v = specialised_model.getProperty(property_name)
if v:
return v
......@@ -32,7 +32,6 @@ from Products.ERP5Type import Permissions, PropertySheet
from Products.ERP5.Document.Invoice import Invoice
#XXX TODO: review naming of new methods
#XXX WARNING: current API naming may change although model should be stable.
class PaySheetTransaction(Invoice):
"""
......@@ -135,13 +134,11 @@ class PaySheetTransaction(Invoice):
If property_list is provided, only subobjects with at least one of those
properties will be taken into account
'''
model = self.getSpecialiseValue().getEffectiveModel(\
start_date=self.getStartDate(),
stop_date=self.getStopDate())
model = self.getSpecialiseValue()
sub_object_list = []
if model is not None:
# if there is an effective model
model_reference_dict = model.getInheritanceReferenceDict(
model_reference_dict = model.getInheritanceReferenceDict(self,
portal_type_list=portal_type_list,
property_list=property_list)
traverse = self.getPortalObject().unrestrictedTraverse
......@@ -166,9 +163,8 @@ class PaySheetTransaction(Invoice):
if len(parent.contentValues(portal_type='Pay Sheet Cell')) == 0:
# the line contain no movements, remove it
self.manage_delObjects(parent.getId())
business_process_list = paysheet_model.findSpecialiseValueList(\
context=paysheet_model,
portal_type_list=['Business Process'])
business_process_list = paysheet_model.findEffectiveSpecialiseValueList(
self, portal_type_list=['Business Process'])
if len(business_process_list):
# XXX currently, we consider that is to complicated to use more than one
# Business Process, so we take the first (wich is the nearest from
......
This diff is collapsed.
......@@ -196,12 +196,12 @@ class TradeModelLine(Predicate, XMLMatrix, Amount):
update = 1
else:
# get source and destination using Business Process
if getattr(document, 'findSpecialiseValueList', None) is None:
if getattr(document, 'findEffectiveSpecialiseValueList', None) is None:
# if parent don't have findSpecialiseValueList, this mean it's on the
# specialise_value
document = self.getParentValue().getSpecialiseValue()
try:
business_process_list = document.findSpecialiseValueList(
business_process_list = document.findEffectiveSpecialiseValueList(
context=context, portal_type_list=['Business Process'])
except AttributeError:
business_process_list = []
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved.
#
# 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 adviced 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from AccessControl import ClassSecurityInfo
from Acquisition import aq_base
from Products.ERP5Type import Permissions
from Products.ERP5.Document.Predicate import Predicate
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
def _getEffectiveModel(self, start_date=None, stop_date=None):
"""Return the most appropriate model using effective_date, expiration_date
and version number.
An effective model is a model which start and stop_date are equal (or
excluded) to the range of the given start and stop_date and with the
higher version number (if there is more than one)
XXX Should we moved this function to a class ? Which one ?
What about reusing IVersionable ?
"""
reference = self.getReference()
if not reference:
return self
query_list = [Query(reference=reference),
Query(portal_type=self.getPortalType()),
Query(validation_state=('deleted', 'invalidated'),
operator='NOT')]
if start_date is not None:
query_list.append(ComplexQuery(Query(effective_date=None),
Query(effective_date=start_date,
range='ngt'),
logical_operator='OR'))
if stop_date is not None:
query_list.append(ComplexQuery(Query(expiration_date=None),
Query(expiration_date=stop_date,
range='min'),
logical_operator='OR'))
# XXX What to do the catalog returns nothing (either because 'self' was just
# created and not yet indexed, or because it was invalidated) ?
# For the moment, we raise.
model_list = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
query=ComplexQuery(logical_operator='AND', *query_list),
sort_on=(('version', 'descending'),))
return model_list[0].getObject()
class CompositionMixin:
"""
"""
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
security.declareProtected(Permissions.AccessContentsInformation,
'asComposedDocument')
def asComposedDocument(self):
container_list = self._findEffectiveSpecialiseValueList()
self = self.asContext()
self._initBTrees()
reference_dict = {}
line_count = 0
for container in container_list:
for ob in container.contentValues():
if isinstance(ob, Predicate):
# reference is used to hide lines on farther containers
reference = ob.getProperty('reference')
if reference:
reference_set = reference_dict.setdefault(ob.getPortalType(), set())
if reference in reference_set:
continue
reference_set.add(reference)
id = str(line_count)
line_count += 1
self._setOb(id, aq_base(ob.asContext(id=id)))
return self
def _findEffectiveSpecialiseValueList(self):
"""Return a list of effective specialised objects that is the
inheritance tree.
An effective object is an object which have start_date and stop_date
included to the range of the given parameters start_date and stop_date.
This algorithm uses Breadth First Search.
"""
start_date = self.getStartDate()
stop_date = self.getStopDate()
def getEffectiveModel(model):
return _getEffectiveModel(model, start_date, stop_date)
model_list = [self]
model_set = set(model_list)
model_index = 0
while model_index < len(model_list):
model = model_list[model_index]
model_index += 1
for model in map(getEffectiveModel, model.getSpecialiseValueList()):
if model not in model_set:
model_set.add(model)
if 1: #model.test(self):
model_list.append(model)
return model_list
......@@ -1371,14 +1371,17 @@ class TestPayrollMixin(ERP5ReportTestCase, TestTradeModelLineMixin):
quantity=4)
def stepCheckInheritanceModelReferenceDict(self, sequence=None, **kw):
paysheet = self.createPaysheet()
model_employee = sequence.get('model_employee')
paysheet.setSpecialiseValue(model_employee)
model_employee_url = model_employee.getRelativeUrl()
model_company_url = sequence.get('model_company').getRelativeUrl()
model_company_alt_url = sequence.get('model_company_alt').getRelativeUrl()
model_country_url = sequence.get('model_country').getRelativeUrl()
model_reference_dict = model_employee.getInheritanceReferenceDict(\
portal_type_list=('Annotation Line',))
model_reference_dict = model_employee.getInheritanceReferenceDict(
paysheet, portal_type_list=('Annotation Line',))
self.assertEquals(len(model_reference_dict), 3) # there is 4 model but two
# models have the same
# reference.
......@@ -1394,8 +1397,6 @@ class TestPayrollMixin(ERP5ReportTestCase, TestTradeModelLineMixin):
self.assertNotEquals(model_reference_dict.has_key(model_country_url), True)
# check the object list :
paysheet = self.createPaysheet()
paysheet.setSpecialiseValue(model_employee)
object_list = paysheet.getInheritedObjectValueList(portal_type_list=\
('Annotation Line',))
self.assertEquals(len(object_list), 3) # one line have the same reference
......@@ -1527,6 +1528,7 @@ class TestPayrollMixin(ERP5ReportTestCase, TestTradeModelLineMixin):
base_contribution_list=['base_amount/payroll/base/contribution',
'base_amount/payroll/report/salary/gross'],
quantity=10000)
self.stepTic()
# create a paysheet without date
paysheet_without_date = self.createPaysheet()
......@@ -1694,9 +1696,7 @@ class TestPayrollMixin(ERP5ReportTestCase, TestTradeModelLineMixin):
# check the effective model tree list
effective_value_list = specialise_value.findEffectiveSpecialiseValueList(\
context=specialise_value,
start_date=paysheet.getStartDate(),
stop_date=paysheet.getStopDate())
context=paysheet)
self.assertEquals(effective_value_list, [model_2])
def stepCreateModelLineZeroPrice(self, sequence=None, **kw):
......@@ -1857,30 +1857,15 @@ class TestPayrollMixin(ERP5ReportTestCase, TestTradeModelLineMixin):
model_1.setSpecialiseValue(model_4)
model_4.setSpecialiseValue(model_6)
paysheet.PaySheetTransaction_applyModel()
self.assertEquals(specialise_value.findSpecialiseValueList(context=paysheet),
[model_1, model_4, model_6])
self.assertEquals(specialise_value.findEffectiveSpecialiseValueList(\
context=paysheet, start_date=paysheet.getStartDate(),
stop_date=paysheet.getStopDate()), [model_2,])
self.assertEquals([model_2],
specialise_value.findEffectiveSpecialiseValueList(context=paysheet))
model_1.setSpecialiseValue(None)
model_2.setSpecialiseValue(model_5)
model_5.setSpecialiseValue(model_6)
paysheet.PaySheetTransaction_applyModel()
self.assertEquals(specialise_value.findSpecialiseValueList(context=paysheet),
[model_1,])
self.assertEquals(specialise_value.findEffectiveSpecialiseValueList(\
context=paysheet, start_date=paysheet.getStartDate(),
stop_date=paysheet.getStopDate()), [model_2, model_5, model_7])
model_3.setSpecialiseValue(model_5)
model_5.setSpecialiseValue(model_6)
paysheet.PaySheetTransaction_applyModel()
self.assertEquals(specialise_value.findSpecialiseValueList(context=paysheet),
[model_1,])
self.assertEquals(specialise_value.findEffectiveSpecialiseValueList(\
context=paysheet, start_date=paysheet.getStartDate(),
stop_date=paysheet.getStopDate()), [model_2, model_5, model_7])
self.assertEquals([model_2, model_5, model_7],
specialise_value.findEffectiveSpecialiseValueList(context=paysheet))
def stepCheckPropertiesAreCopiedFromModelLineToPaySheetLine(self,
sequence=None, **kw):
......@@ -2383,6 +2368,7 @@ class TestPayroll(TestPayrollMixin):
sequence_string = """
CreateModelTree
ModelTreeAddAnnotationLines
Tic
CheckInheritanceModelReferenceDict
"""
sequence_list.addSequenceString(sequence_string)
......
......@@ -1369,31 +1369,10 @@ class TestTradeModelLine(TestTradeModelLineMixin):
order_line_discounted.getTotalPrice() * 0.32) * 0.8
)
# Tests
def test_TradeConditionTradeModelLineBasicComposition(self):
"""
If Trade Condition is specialised by another Trade Condition they
Trade Model Lines shall be merged.
"""
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
trade_condition_1.setSpecialiseValue(trade_condition_2)
trade_condition_1_trade_model_line = self.createTradeModelLine(
trade_condition_1,
reference='A')
trade_condition_2_trade_model_line = self.createTradeModelLine(
trade_condition_2,
reference='B')
self.assertSameSet(
[trade_condition_1_trade_model_line,
trade_condition_2_trade_model_line],
trade_condition_1.getTradeModelLineComposedList()
)
def assertSameUidSet(self, a, b, msg=None):
self.assertEqual(set(x.uid for x in a), set(x.uid for x in b), msg)
# Tests
def test_TradeConditionTradeModelLineBasicCompositionWithOrder(self):
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
......@@ -1414,66 +1393,26 @@ class TestTradeModelLine(TestTradeModelLineMixin):
order,
reference='C')
self.assertSameSet(
[trade_condition_1_trade_model_line, trade_condition_2_trade_model_line],
trade_condition_1.getTradeModelLineComposedList()
)
self.assertSameSet(
self.assertSameUidSet(
[trade_condition_1_trade_model_line, trade_condition_2_trade_model_line,
order_trade_model_line],
trade_condition_1.getTradeModelLineComposedList(context=order)
)
def test_TradeConditionCircularCompositionIsSafe(self):
order = self.createOrder()
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
order.setSpecialiseValue(trade_condition_1)
trade_condition_1.setSpecialiseValue(trade_condition_2)
trade_condition_2.setSpecialiseValue(trade_condition_1)
self.assertEquals(trade_condition_1. \
findSpecialiseValueList(trade_condition_1),
self.assertEqual(trade_condition_1.findEffectiveSpecialiseValueList(order),
[trade_condition_1, trade_condition_2]
)
def test_findSpecialiseValueList(self):
'''
check that findSpecialiseValueList is able to return all the inheritance
model tree using Depth-first search
trade_condition_1
/ \
/ \
/ \
trade_condition_2 trade_condition_3
|
|
|
trade_condition_4
According to Depth-first search algorithm, result of this graph is:
[trade_condition_1, trade_condition_2, trade_condition_3,
trade_condition_4]
'''
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
trade_condition_3 = self.createTradeCondition()
trade_condition_4 = self.createTradeCondition()
trade_condition_1.setSpecialiseValueList((trade_condition_2,
trade_condition_3))
trade_condition_2.setSpecialiseValue(trade_condition_4)
specialise_value_list = trade_condition_1.findSpecialiseValueList(
context=trade_condition_1)
self.assertEquals(len(specialise_value_list), 4)
self.assertEquals(
[trade_condition_1, trade_condition_2, trade_condition_3,
trade_condition_4], specialise_value_list)
def test_findSpecialiseValueListWithPortalType(self):
def test_findEffectiveSpecialiseValueListWithPortalType(self):
'''
check that findSpecialiseValueList is able to return all the inheritance
model tree using Depth-first search with a specific portal_type asked
......@@ -1495,43 +1434,26 @@ class TestTradeModelLine(TestTradeModelLineMixin):
As only business_process will be a "Business Process" and we search for business process
the result must be [business_process]
'''
order = self.createOrder()
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
trade_condition_3 = self.createTradeCondition()
trade_condition_4 = self.createTradeCondition()
business_process = self.createBusinessProcess()
order.setSpecialiseValue(trade_condition_1)
trade_condition_1.setSpecialiseValueList((trade_condition_2,
trade_condition_3))
trade_condition_2.setSpecialiseValue(trade_condition_4)
trade_condition_4.setSpecialiseValue(business_process)
specialise_value_list = trade_condition_1.findSpecialiseValueList(
portal_type_list = ['Business Process'],
context=trade_condition_1)
self.assertEquals(len(specialise_value_list), 1)
self.assertEquals([business_process,] , specialise_value_list)
def test_TradeConditionTradeModelLineReferenceIsShadowingComposition(self):
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
trade_condition_1.setSpecialiseValue(trade_condition_2)
trade_condition_1_trade_model_line = self.createTradeModelLine(
trade_condition_1,
reference='A')
trade_condition_2_trade_model_line = self.createTradeModelLine(
trade_condition_2,
reference='A')
self.assertSameSet(
[trade_condition_1_trade_model_line],
trade_condition_1.getTradeModelLineComposedList()
)
self.assertEqual(trade_condition_1.findEffectiveSpecialiseValueList(order),
[trade_condition_1, trade_condition_2, trade_condition_3,
trade_condition_4])
self.assertEqual(trade_condition_1.findEffectiveSpecialiseValueList(order,
portal_type_list = ['Business Process']), [business_process])
def test_TradeConditionTradeModelLineReferenceIsShadowingCompositionWithOrder(self):
def test_TradeConditionTradeModelLineReferenceIsShadowingComposition(self):
trade_condition_1 = self.createTradeCondition()
trade_condition_2 = self.createTradeCondition()
order = self.createOrder()
......@@ -1551,13 +1473,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
order,
reference = 'B')
self.assertSameSet(
[trade_condition_1_trade_model_line,
trade_condition_2_trade_model_line],
trade_condition_1.getTradeModelLineComposedList()
)
self.assertSameSet(
self.assertSameUidSet(
[trade_condition_1_trade_model_line, order_trade_model_line],
trade_condition_1.getTradeModelLineComposedList(context=order)
)
......@@ -1568,7 +1484,9 @@ class TestTradeModelLine(TestTradeModelLineMixin):
where we create trade model line in a wrong order in comparison to application relations
We have a contribution graph like this A ---> C ---> B so final order must be A, C, B
"""
order = self.createOrder()
trade_condition = self.createTradeCondition()
order.setSpecialiseValue(trade_condition)
A = self.createTradeModelLine(trade_condition, reference='A', id=1,
base_contribution_list=['base_amount/total'])
......@@ -1579,7 +1497,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
C = self.createTradeModelLine(trade_condition, reference='C', id=3,
base_contribution_list=['base_amount/total_tax'],
base_application_list=['base_amount/total'])
trade_model_line_list = trade_condition.getTradeModelLineComposedList()
trade_model_line_list = trade_condition.getTradeModelLineComposedList(order)
self.assertEquals([q.getReference() for q in trade_model_line_list],
[q.getReference() for q in [A, C, B,]])
......@@ -1646,7 +1564,9 @@ class TestTradeModelLine(TestTradeModelLineMixin):
* (DEFG) (BC) A
where everything in parenthesis can be not sorted
"""
order = self.createOrder()
trade_condition = self.createTradeCondition()
order.setSpecialiseValue(trade_condition)
A = self.createTradeModelLine(trade_condition, reference='A',
base_application_list=['base_amount/total'])
......@@ -1675,7 +1595,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
base_contribution_list=['base_amount/total_discount'],
base_application_list=['base_amount/discount'])
trade_model_line_list = trade_condition.getTradeModelLineComposedList()
trade_model_line_list = trade_condition.getTradeModelLineComposedList(order)
possible_sort_list = [
[[D,E], [B], [F, G], [C], [A]],
......@@ -1704,7 +1624,9 @@ class TestTradeModelLine(TestTradeModelLineMixin):
* A (BC) D
where everything in parenthesis can be not sorted
"""
order = self.createOrder()
trade_condition = self.createTradeCondition()
order.setSpecialiseValue(trade_condition)
C = self.createTradeModelLine(trade_condition, reference='C',
base_contribution_list=['base_amount/total'],
......@@ -1722,7 +1644,7 @@ class TestTradeModelLine(TestTradeModelLineMixin):
base_contribution_list=['base_amount/total'],
base_application_list=['base_amount/total_tax'])
trade_model_line_list = trade_condition.getTradeModelLineComposedList()
trade_model_line_list = trade_condition.getTradeModelLineComposedList(order)
possible_sort_list = [
[[A], [B,C], [D]]
......
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