Fix Price not always using Decimal
This commit is contained in:
parent
97536c5ce1
commit
8889496a34
|
@ -1,5 +1,6 @@
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from typing import Set, Union
|
from typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
@ -488,9 +489,11 @@ class AggregateRate(Rate):
|
||||||
|
|
||||||
|
|
||||||
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
SCALE = 4
|
||||||
|
ROUND = ROUND_HALF_EVEN
|
||||||
currency = Column(DBEnum(Currency), nullable=False)
|
currency = Column(DBEnum(Currency), nullable=False)
|
||||||
currency.comment = """The currency of this price as for ISO 4217."""
|
currency.comment = """The currency of this price as for ISO 4217."""
|
||||||
price = Column(Numeric(precision=19, scale=4), check_range('price', 0), nullable=False)
|
price = Column(Numeric(precision=19, scale=SCALE), check_range('price', 0), nullable=False)
|
||||||
price.comment = """The value."""
|
price.comment = """The value."""
|
||||||
software = Column(DBEnum(PriceSoftware))
|
software = Column(DBEnum(PriceSoftware))
|
||||||
software.comment = """The software used to compute this price,
|
software.comment = """The software used to compute this price,
|
||||||
|
@ -510,8 +513,17 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
primaryjoin=AggregateRate.id == rating_id)
|
primaryjoin=AggregateRate.id == rating_id)
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
if 'price' in kwargs:
|
||||||
self.currency = self.currency or app.config['PRICE_CURRENCY']
|
assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal'
|
||||||
|
super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
||||||
|
"""Returns a Decimal value with the correct scale for Price.price."""
|
||||||
|
if isinstance(value, float):
|
||||||
|
value = Decimal(value)
|
||||||
|
# equation from marshmallow.fields.Decimal
|
||||||
|
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
|
||||||
|
|
||||||
|
|
||||||
class EreusePrice(Price):
|
class EreusePrice(Price):
|
||||||
|
@ -522,9 +534,10 @@ class EreusePrice(Price):
|
||||||
}
|
}
|
||||||
|
|
||||||
class Type:
|
class Type:
|
||||||
def __init__(self, percentage, price) -> None:
|
def __init__(self, percentage: float, price: Decimal) -> None:
|
||||||
# see https://stackoverflow.com/a/29651462 for the - 0.005
|
# see https://stackoverflow.com/a/29651462 for the - 0.005
|
||||||
self.amount = round(price * percentage - 0.005, 2)
|
self.amount = EreusePrice.to_price(price * Decimal(percentage))
|
||||||
|
self.percentage = EreusePrice.to_price(price * Decimal(percentage))
|
||||||
self.percentage = round(percentage - 0.005, 2)
|
self.percentage = round(percentage - 0.005, 2)
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
@ -560,7 +573,7 @@ class EreusePrice(Price):
|
||||||
}
|
}
|
||||||
SCHEMA[Server] = SCHEMA[Desktop]
|
SCHEMA[Server] = SCHEMA[Desktop]
|
||||||
|
|
||||||
def __init__(self, device, rating_range, role, price) -> None:
|
def __init__(self, device, rating_range, role, price: Decimal) -> None:
|
||||||
cls = device.__class__ if device.__class__ != Server else Desktop
|
cls = device.__class__ if device.__class__ != Server else Desktop
|
||||||
rate = self.SCHEMA[cls][rating_range]
|
rate = self.SCHEMA[cls][rating_range]
|
||||||
self.standard = EreusePrice.Type(rate[self.STANDARD][role], price)
|
self.standard = EreusePrice.Type(rate[self.STANDARD][role], price)
|
||||||
|
@ -570,11 +583,15 @@ class EreusePrice(Price):
|
||||||
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
||||||
if rating.rating_range == RatingRange.VERY_LOW:
|
if rating.rating_range == RatingRange.VERY_LOW:
|
||||||
raise ValueError('Cannot compute price for Range.VERY_LOW')
|
raise ValueError('Cannot compute price for Range.VERY_LOW')
|
||||||
self.price = round(rating.rating * self.MULTIPLIER[rating.device.__class__], 2)
|
# We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts
|
||||||
super().__init__(rating=rating, device=rating.device, **kwargs)
|
price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP)
|
||||||
|
super().__init__(rating=rating,
|
||||||
|
device=rating.device,
|
||||||
|
price=price,
|
||||||
|
software=kwargs.pop('software', app.config['PRICE_SOFTWARE']),
|
||||||
|
version=kwargs.pop('version', app.config['PRICE_VERSION']),
|
||||||
|
**kwargs)
|
||||||
self._compute()
|
self._compute()
|
||||||
self.software = self.software or app.config['PRICE_SOFTWARE']
|
|
||||||
self.version = self.version or app.config['PRICE_VERSION']
|
|
||||||
|
|
||||||
@orm.reconstructor
|
@orm.reconstructor
|
||||||
def _compute(self):
|
def _compute(self):
|
||||||
|
|
|
@ -242,6 +242,8 @@ class WorkbenchRate(ManualRate):
|
||||||
|
|
||||||
|
|
||||||
class Price(EventWithOneDevice):
|
class Price(EventWithOneDevice):
|
||||||
|
SCALE = ...
|
||||||
|
ROUND = ...
|
||||||
currency = ... # type: Column
|
currency = ... # type: Column
|
||||||
price = ... # type: Column
|
price = ... # type: Column
|
||||||
software = ... # type: Column
|
software = ... # type: Column
|
||||||
|
@ -257,6 +259,10 @@ class Price(EventWithOneDevice):
|
||||||
self.version = ... # type: StrictVersion
|
self.version = ... # type: StrictVersion
|
||||||
self.rating = ... # type: AggregateRate
|
self.rating = ... # type: AggregateRate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EreusePrice(Price):
|
class EreusePrice(Price):
|
||||||
MULTIPLIER = ... # type: Dict
|
MULTIPLIER = ... # type: Dict
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import decimal
|
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
||||||
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
||||||
|
@ -156,8 +154,8 @@ class AggregateRate(Rate):
|
||||||
|
|
||||||
class Price(EventWithOneDevice):
|
class Price(EventWithOneDevice):
|
||||||
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
|
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
|
||||||
price = Decimal(places=4,
|
price = Decimal(places=m.Price.SCALE,
|
||||||
ounding=decimal.ROUND_HALF_EVEN,
|
rounding=m.Price.ROUND,
|
||||||
required=True,
|
required=True,
|
||||||
description=m.Price.price.comment)
|
description=m.Price.price.comment)
|
||||||
software = EnumField(PriceSoftware, dump_only=True, description=m.Price.software.comment)
|
software = EnumField(PriceSoftware, dump_only=True, description=m.Price.software.comment)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -265,7 +266,7 @@ def test_migrate():
|
||||||
def test_price_custom():
|
def test_price_custom():
|
||||||
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
||||||
chassis=ComputerChassis.Docking)
|
chassis=ComputerChassis.Docking)
|
||||||
price = models.Price(price=25.25, currency=Currency.EUR)
|
price = models.Price(price=Decimal(25.25), currency=Currency.EUR)
|
||||||
price.device = computer
|
price.device = computer
|
||||||
assert computer.price == price
|
assert computer.price == price
|
||||||
db.session.add(computer)
|
db.session.add(computer)
|
||||||
|
@ -280,3 +281,12 @@ def test_price_custom():
|
||||||
|
|
||||||
c, _ = client.get(res=Device, item=computer.id)
|
c, _ = client.get(res=Device, item=computer.id)
|
||||||
assert c['price']['id'] == p['id']
|
assert c['price']['id'] == p['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reson='Develop test')
|
||||||
|
def test_ereuse_price():
|
||||||
|
"""Tests the several ways of creating eReuse Price, emulating
|
||||||
|
from an AggregateRate and ensuring that the different Range
|
||||||
|
return correct results."""
|
||||||
|
# important to check Range.low no returning warranty2
|
||||||
|
# Range.verylow not returning nothing
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from decimal import Decimal
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -64,17 +65,17 @@ def test_rate():
|
||||||
rate.device = pc
|
rate.device = pc
|
||||||
events = main.main(rate, RatingSoftware.ECost, StrictVersion('1.0'))
|
events = main.main(rate, RatingSoftware.ECost, StrictVersion('1.0'))
|
||||||
price = next(e for e in events if isinstance(e, EreusePrice))
|
price = next(e for e in events if isinstance(e, EreusePrice))
|
||||||
assert price.price == 92.2
|
assert price.price == Decimal('92.2001')
|
||||||
assert price.retailer.standard.amount == 40.97
|
assert price.retailer.standard.amount == Decimal('40.9714')
|
||||||
assert price.platform.standard.amount == 18.84
|
assert price.platform.standard.amount == Decimal('18.8434')
|
||||||
assert price.refurbisher.standard.amount == 32.38
|
assert price.refurbisher.standard.amount == Decimal('32.3853')
|
||||||
assert price.price >= price.retailer.standard.amount \
|
assert price.price >= price.retailer.standard.amount \
|
||||||
+ price.platform.standard.amount \
|
+ price.platform.standard.amount \
|
||||||
+ price.refurbisher.standard.amount
|
+ price.refurbisher.standard.amount
|
||||||
assert price.retailer.warranty2.amount == 55.30
|
assert price.retailer.warranty2.amount == Decimal('55.3085')
|
||||||
assert price.platform.warranty2.amount == 25.43
|
assert price.platform.warranty2.amount == Decimal('25.4357')
|
||||||
assert price.refurbisher.warranty2.amount == 43.72
|
assert price.refurbisher.warranty2.amount == Decimal('43.7259')
|
||||||
assert price.warranty2 == 124.45
|
assert price.warranty2 == Decimal('124.47')
|
||||||
# Checks relationships
|
# Checks relationships
|
||||||
workbench_rate = next(e for e in events if isinstance(e, WorkbenchRate))
|
workbench_rate = next(e for e in events if isinstance(e, WorkbenchRate))
|
||||||
aggregate_rate = next(e for e in events if isinstance(e, AggregateRate))
|
aggregate_rate = next(e for e in events if isinstance(e, AggregateRate))
|
||||||
|
|
Reference in a new issue