Commit e6f788f7 authored by Jérome Perrin's avatar Jérome Perrin

Support defining prices in different quantity units

See merge request nexedi/erp5!1293
parents d41db7ab e0528922
Pipeline #11928 failed with stage
# coding: utf-8
result = context.getPriceParameterDict(context=movement, **kw)
# Calculate
......@@ -76,7 +77,22 @@ unit_base_price *= 1 + result["surcharge_ratio"]
# Divide by the priced quantity
priced_quantity = result['priced_quantity']
if priced_quantity:
# If this priced quantity is in a different quantity unit from the
# resource's default quantity unit, we have to convert this quantity
# to the resource quantity unit.
# For example, if we have a resource managed in Kilogram and we have
# a supply line saying that the price for 250 Grams is 100€, we adjust this
# priced quantity to be 0.25 (Kilogram)
supply_line_quantity_unit = next(iter(result.get('quantity_unit', ())), None)
resource_default_quantity_unit = context.getDefaultQuantityUnit()
if resource_default_quantity_unit != supply_line_quantity_unit:
priced_quantity = context.convertQuantity(
priced_quantity or 1,
supply_line_quantity_unit,
resource_default_quantity_unit,
movement.getVariationCategoryList()
)
if priced_quantity and priced_quantity != 1:
unit_base_price /= priced_quantity
result["price"] = unit_base_price
......
......@@ -106,8 +106,9 @@
<string>my_internal_supply_line_base_price</string>
<string>my_internal_supply_line_base_unit_price</string>
<string>my_internal_supply_line_price_currency</string>
<string>my_internal_supply_line_source_account</string>
<string>my_internal_supply_line_priced_quantity</string>
<string>my_internal_supply_line_quantity_unit</string>
<string>my_internal_supply_line_source_account</string>
<string>my_internal_supply_line_start_date_range_min</string>
<string>my_internal_supply_line_start_date_range_max</string>
<string>my_internal_supply_line_min_delay</string>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>enabled</string>
<string>items</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_internal_supply_line_quantity_unit</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_quantity_unit</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewPDMFieldLibrary</string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: [(\'\', \'\')] + [(x.getTranslatedLogicalPath(), x.getCategoryRelativeUrl(base=0)) for x in here.getQuantityUnitValueList()]</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -106,8 +106,9 @@
<string>my_purchase_supply_line_base_price</string>
<string>my_purchase_supply_line_base_unit_price</string>
<string>my_purchase_supply_line_price_currency</string>
<string>my_purchase_supply_line_destination_account</string>
<string>my_purchase_supply_line_priced_quantity</string>
<string>my_purchase_supply_line_quantity_unit</string>
<string>my_purchase_supply_line_destination_account</string>
<string>my_purchase_supply_line_start_date_range_min</string>
<string>my_purchase_supply_line_start_date_range_max</string>
<string>my_purchase_supply_line_min_delay</string>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>enabled</string>
<string>items</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_purchase_supply_line_quantity_unit</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_quantity_unit</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewPDMFieldLibrary</string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: [(\'\', \'\')] + [(x.getTranslatedLogicalPath(), x.getCategoryRelativeUrl(base=0)) for x in here.getQuantityUnitValueList()]</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -106,8 +106,9 @@
<string>my_sale_supply_line_base_price</string>
<string>my_sale_supply_line_base_unit_price</string>
<string>my_sale_supply_line_price_currency</string>
<string>my_sale_supply_line_source_account</string>
<string>my_sale_supply_line_priced_quantity</string>
<string>my_sale_supply_line_quantity_unit</string>
<string>my_sale_supply_line_source_account</string>
<string>my_sale_supply_line_start_date_range_min</string>
<string>my_sale_supply_line_start_date_range_max</string>
<string>my_sale_supply_line_min_delay</string>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ProxyField" module="Products.ERP5Form.ProxyField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>enabled</string>
<string>items</string>
</list>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>my_sale_supply_line_quantity_unit</string> </value>
</item>
<item>
<key> <string>message_values</string> </key>
<value>
<dictionary>
<item>
<key> <string>external_validator_failed</string> </key>
<value> <string>The input failed the external validator.</string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>overrides</string> </key>
<value>
<dictionary>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>tales</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>target</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
<item>
<key> <string>values</string> </key>
<value>
<dictionary>
<item>
<key> <string>enabled</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>extra_context</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>field_id</string> </key>
<value> <string>my_quantity_unit</string> </value>
</item>
<item>
<key> <string>form_id</string> </key>
<value> <string>Base_viewPDMFieldLibrary</string> </value>
</item>
<item>
<key> <string>items</string> </key>
<value>
<list/>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: [(\'\', \'\')] + [(x.getTranslatedLogicalPath(), x.getCategoryRelativeUrl(base=0)) for x in here.getQuantityUnitValueList()]</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -35,8 +35,9 @@ if context.getParentValue().getParentValue().getPortalType() in (
#backwards compatibility
mapped_value_property_list = context.getMappedValuePropertyList()
if not 'priced_quantity' in mapped_value_property_list:
mapped_value_property_list.append('priced_quantity')
for mapped_property in ('priced_quantity', 'quantity_unit'):
if not mapped_property in mapped_value_property_list:
mapped_value_property_list.append(mapped_property)
context.setMappedValuePropertyList(mapped_value_property_list)
# XXX: An hack that the context cell may not have the start_date_range_min/max properties.
......
......@@ -36,8 +36,9 @@ if context.getParentValue().getPortalType() in (
#backwards compatibility
mapped_value_property_list = context.getMappedValuePropertyList()
if not 'priced_quantity' in mapped_value_property_list:
mapped_value_property_list.append('priced_quantity')
for mapped_property in ('priced_quantity', 'quantity_unit'):
if not mapped_property in mapped_value_property_list:
mapped_value_property_list.append(mapped_property)
context.setMappedValuePropertyList(mapped_value_property_list)
return context.generatePredicate(membership_criterion_base_category_list = base_category_tuple,
......
......@@ -9,4 +9,5 @@ context.setMappedValuePropertyList([
'surcharge_ratio', 'variable_additional_price',
'non_discountable_additional_price',
'priced_quantity', 'base_unit_price',
'quantity_unit',
])
......@@ -166,6 +166,22 @@ class TestResource(ERP5TypeTestCase):
self.quantity_unit_kilo = quantity_unit_weight.newContent(
portal_type='Category',
id='kilo')
quantity_unit_volume = self.portal.portal_categories.quantity_unit._getOb(
'volume', None)
if quantity_unit_volume is None:
quantity_unit_volume = self.portal.portal_categories.quantity_unit.newContent(
id='volume', portal_type='Category')
self.quantity_unit_liter = quantity_unit_volume._getOb('liter', None)
if self.quantity_unit_liter is None:
self.quantity_unit_liter = quantity_unit_volume.newContent(
portal_type='Category', id='liter')
self.metric_type_volume = self.portal.portal_categories.metric_type._getOb(
'volume', None)
if self.metric_type_volume is None:
self.metric_type_volume = self.portal.portal_categories.metric_type.newContent(
id='volume',
portal_type='Category')
unit_conversion_module = self.portal.quantity_unit_conversion_module
weight_group = unit_conversion_module._getOb('weight', None)
......@@ -183,6 +199,22 @@ class TestResource(ERP5TypeTestCase):
quantity=0.001)
gram_definition.validate()
volume_group = unit_conversion_module._getOb('volume', None)
if volume_group is None:
volume_group = unit_conversion_module.newContent(
id='volume',
portal_type='Quantity Unit Conversion Group',
quantity_unit_value=self.quantity_unit_liter)
volume_group.validate()
liter_definition = volume_group._getOb('liter', None)
if liter_definition is None:
liter_definition = volume_group.newContent(
id='liter',
portal_type='Quantity Unit Conversion Definition',
quantity_unit_value=self.quantity_unit_liter,
quantity=1)
liter_definition.validate()
# create some product line categories
product_line = self.portal.portal_categories.product_line
if product_line._getOb('a', None) is None:
......@@ -1034,6 +1066,80 @@ class TestResource(ERP5TypeTestCase):
self.assertEqual(1, sale_order_line.getPrice())
self.assertEqual(5000, sale_order_line.getTotalPrice())
def testGetPriceDefinedInDifferentQuantityUnit(self):
resource = self.portal.getDefaultModule(self.product_portal_type)\
.newContent(portal_type=self.product_portal_type)
resource.setDefaultQuantityUnitValue(self.quantity_unit_kilo)
supply_line = resource.newContent(
portal_type=self.sale_supply_line_portal_type)
supply_line.setDefaultQuantityUnitValue(self.quantity_unit_gram)
supply_line.setBasePrice(5)
# price for 1 gram is 5, so price for 1 kg is 5000
self.tic()
sale_order = self.portal.getDefaultModule("Sale Order").newContent(
portal_type='Sale Order',)
sale_order_line = sale_order.newContent(
portal_type=self.sale_order_line_portal_type,
resource_value=resource,
quantity=5)
self.assertEqual(sale_order_line.getPrice(), 5000)
# price for 250g is 100
supply_line.setPricedQuantity(250)
supply_line.setBasePrice(100)
self.tic()
# so for 1kg it is 400
sale_order_line.setPrice(None)
self.assertEqual(sale_order_line.getPrice(), 400)
def testGetPriceDefinedForDifferentMetric(self):
resource = self.portal.getDefaultModule(self.product_portal_type)\
.newContent(portal_type=self.product_portal_type)
resource.setQuantityUnitValueList([
self.quantity_unit_kilo,
self.quantity_unit_liter
])
# this resource exists in kg or liter, one Kg is 0.75 liter.
measure = resource.newContent(
portal_type='Measure'
)
measure.setMetricTypeValue(self.metric_type_volume)
measure.setQuantityUnitValue(self.quantity_unit_liter)
measure.setQuantity(0.75)
supply_line = resource.newContent(
portal_type=self.sale_supply_line_portal_type)
supply_line.setDefaultQuantityUnitValue(self.quantity_unit_liter)
supply_line.setBasePrice(4)
# price for 1l is 4, so price for 1kg is 3
self.tic()
sale_order = self.portal.getDefaultModule("Sale Order").newContent(
portal_type='Sale Order',)
sale_order_line = sale_order.newContent(
portal_type=self.sale_order_line_portal_type,
resource_value=resource,
quantity=1)
self.assertEqual(sale_order_line.getPrice(), 3)
def testGetPriceWithPricedQuantity(self):
resource = self.portal.getDefaultModule(self.product_portal_type)\
.newContent(portal_type=self.product_portal_type)
supply_line = resource.newContent(
portal_type=self.sale_supply_line_portal_type)
supply_line.setBasePrice(3000)
supply_line.setPricedQuantity(3)
self.tic()
sale_order = self.portal.getDefaultModule("Sale Order").newContent(
portal_type='Sale Order',)
sale_order_line = sale_order.newContent(
portal_type=self.sale_order_line_portal_type,
resource_value=resource,
quantity=5)
# price for 3 is 3000, so price for 1 is 1000
self.assertEqual(sale_order_line.getPrice(), 1000)
self.assertEqual(sale_order_line.getTotalPrice(), 5000)
def testGetPriceWithPriceCurrency(self):
currency_module = self.portal.getDefaultModule("Currency")
currency = currency_module.newContent(
......@@ -1302,36 +1408,14 @@ class TestResource(ERP5TypeTestCase):
In this test, always use Base.edit method. Because Base.edit is
used when real user edit document through edit form.
"""
# Set up quantity unit categories
# weight
quantity_unit_category_value = self.portal.portal_categories.quantity_unit
quantity_unit_weight = quantity_unit_category_value._getOb('weight', None)
if quantity_unit_weight is None:
quantity_unit_weight = quantity_unit_category_value.newContent(
id='weight', portal_type='Category')
quantity_unit_gram = quantity_unit_weight._getOb('gram', None)
if quantity_unit_gram is None:
quantity_unit_gram = quantity_unit_weight.newContent(
portal_type='Category', id='gram')
# volume
quantity_unit_volume = quantity_unit_category_value._getOb('volume', None)
if quantity_unit_volume is None:
quantity_unit_volume = quantity_unit_category_value.newContent(
id='volume', portal_type='Category')
quantity_unit_liter = quantity_unit_volume._getOb('liter', None)
if quantity_unit_liter is None:
quantity_unit_liter = quantity_unit_volume.newContent(
portal_type='Category', id='liter')
self.commit()
# Create resource
resource_value = self.portal.getDefaultModule(
self.product_portal_type).newContent(portal_type=self.product_portal_type)
resource_value.edit(quantity_unit_value_list=[
quantity_unit_gram, quantity_unit_liter])
self.quantity_unit_gram, self.quantity_unit_liter])
self.commit()
self.assertEqual(resource_value.getDefaultQuantityUnitValue(),
quantity_unit_gram)
self.quantity_unit_gram)
# Create sale order line
sale_order = self.portal.getDefaultModule('Sale Order').newContent(
......@@ -1344,35 +1428,35 @@ class TestResource(ERP5TypeTestCase):
sale_order_line.edit(resource_value=resource_value)
self.commit()
self.assertEqual(sale_order_line.getQuantityUnitValue(),
quantity_unit_gram)
self.quantity_unit_gram)
# Select different quantity unit
sale_order_line.edit(quantity_unit_value=quantity_unit_liter)
sale_order_line.edit(quantity_unit_value=self.quantity_unit_liter)
self.commit()
self.assertEqual(sale_order_line.getQuantityUnitValue(),
quantity_unit_liter)
self.quantity_unit_liter)
# Select empty(no quantity unit)
sale_order_line.edit(quantity_unit_value=None)
self.commit()
# Select default quantity unit again
sale_order_line.edit(quantity_unit_value=quantity_unit_gram)
sale_order_line.edit(quantity_unit_value=self.quantity_unit_gram)
self.commit()
self.assertEqual(sale_order_line.getQuantityUnitValue(),
quantity_unit_gram)
self.quantity_unit_gram)
# Change default quantity unit on resource
# Now liter is default quantity unit.
resource_value.edit(quantity_unit_value_list=[
quantity_unit_liter, quantity_unit_gram])
self.quantity_unit_liter, self.quantity_unit_gram])
self.commit()
# Check existing movement again and make sure that quantity
# unit is not changed.
expectedFailure(self.assertEqual)(
sale_order_line.getQuantityUnitValue(),
quantity_unit_gram)
self.quantity_unit_gram)
def test_suite():
suite = unittest.TestSuite()
......
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