diff --git a/openregistry/api/models/ocds.py b/openregistry/api/models/ocds.py index bdb0f36..5683898 100644 --- a/openregistry/api/models/ocds.py +++ b/openregistry/api/models/ocds.py @@ -2,11 +2,14 @@ from uuid import uuid4 from schematics.types import (StringType, FloatType, URLType, IntType, - BooleanType, BaseType, EmailType, MD5Type) -from schematics.exceptions import ValidationError + BooleanType, BaseType, EmailType, MD5Type, + DecimalType as BaseDecimalType) +from schematics.exceptions import ValidationError, ConversionError from schematics.types.compound import ModelType, ListType from schematics.types.serializable import serializable +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP + from openregistry.api.constants import (DEFAULT_CURRENCY, DEFAULT_ITEM_CLASSIFICATION, ITEM_CLASSIFICATIONS, DOCUMENT_TYPES, IDENTIFIER_CODES, DEBTOR_TYPES @@ -147,6 +150,29 @@ class Identifier(Model): uri = URLType() # A URI to identify the organization. +class DecimalType(BaseDecimalType): + + def __init__(self, precision=-3, **kwargs): + self.precision = Decimal("1E{:d}".format(precision)) + super(DecimalType, self).__init__(**kwargs) + + def to_primitive(self, value, context=None): + return value + + def to_native(self, value, context=None): + try: + value = Decimal(value).quantize(self.precision, rounding=ROUND_HALF_UP).normalize() + except (TypeError, InvalidOperation): + raise ConversionError(self.messages['number_coerce'].format(value)) + + if self.min_value is not None and value < self.min_value: + raise ConversionError(self.messages['number_min'].format(value)) + if self.max_value is not None and self.max_value < value: + raise ConversionError(self.messages['number_max'].format(value)) + + return value + + class Item(Model): """A good, service, or work to be contracted.""" id = StringType(required=True, min_length=1, default=lambda: uuid4().hex) @@ -156,7 +182,7 @@ class Item(Model): classification = ModelType(ItemClassification) additionalClassifications = ListType(ModelType(Classification), default=list()) unit = ModelType(Unit) # Description of the unit which the good comes in e.g. hours, kilograms - quantity = IntType() # The number of units required + quantity = DecimalType() # The number of units required address = ModelType(Address) location = ModelType(Location) diff --git a/openregistry/api/tests/models.py b/openregistry/api/tests/models.py index 17cf8b6..c5ea3e4 100644 --- a/openregistry/api/tests/models.py +++ b/openregistry/api/tests/models.py @@ -3,6 +3,7 @@ import mock from datetime import datetime, timedelta from schematics.exceptions import ConversionError, ValidationError, ModelValidationError +from decimal import Decimal from openregistry.api.utils import get_now @@ -12,7 +13,7 @@ from openregistry.api.models.ocds import ( Organization, ContactPoint, Identifier, Address, Item, Location, Unit, Value, ItemClassification, Classification, - Period, PeriodEndRequired, Document + Period, PeriodEndRequired, Document, DecimalType ) @@ -45,6 +46,31 @@ def test_IsoDateTimeType_model(self): with self.assertRaises(ConversionError): dt.to_native(dt.to_primitive(date)) + def test_DecimalType_model(self): + number = '5.001' + + dt = DecimalType() + + value = dt.to_primitive(number) + self.assertEqual(Decimal(number), dt.to_native(number)) + self.assertEqual(Decimal(number), dt.to_native(value)) + + for number in (None, '5,5'): + with self.assertRaisesRegexp(ConversionError, dt.messages['number_coerce'].format(number)): + dt.to_native(number) + + dt = DecimalType(precision=-3, min_value=Decimal('0'), max_value=Decimal('10.0')) + + self.assertEqual(Decimal('0.111'), dt.to_native('0.11111')) + self.assertEqual(Decimal('0.556'), dt.to_native('0.55555')) + + number = '-1.1' + with self.assertRaisesRegexp(ConversionError, dt.messages['number_min'].format(number)): + dt.to_native(dt.to_primitive(number)) + number = '11.1' + with self.assertRaisesRegexp(ConversionError, dt.messages['number_max'].format(number)): + dt.to_native(dt.to_primitive(number)) + def test_HashType_model(self): from uuid import uuid4 @@ -263,7 +289,7 @@ def test_Item_model(self): "name": u"item", "code": u"39513200-3" }, - "quantity": 5, + "quantity": Decimal('5.001'), "address": { "countryName": u"Україна", "postalCode": "79000",