def test_units_match(): class XxxMoney(int): currency = 'XXX' m1 = Money(1, 'EUR') m2 = Money(2, 'EUR') m3 = Money(3, 'XXX') m4 = XxxMoney(4) assert m1.unit_matches_with(m2) assert not m1.unit_matches_with(m3) assert m3.unit_matches_with(m4)
def test_units_match(): class XxxMoney(int): currency = "XXX" m1 = Money(1, "EUR") m2 = Money(2, "EUR") m3 = Money(3, "XXX") m4 = XxxMoney(4) assert m1.unit_matches_with(m2) assert not m1.unit_matches_with(m3) assert m3.unit_matches_with(m4)
def test_as_rounded_rounding_mode(): set_precision_provider(babel_precision_provider.get_precision) prec2 = Decimal('0.01') m1 = Money('2.345', 'EUR') m2 = Money('2.344', 'EUR') assert m1.as_rounded(2).value == Decimal('2.34') assert m2.as_rounded(2).value == Decimal('2.34') from decimal import ROUND_HALF_DOWN, ROUND_HALF_UP, ROUND_FLOOR assert m1.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal('2.34') assert m2.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal('2.34') assert m1.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal('2.35') assert m2.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal('2.34') assert m1.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal('2.34') assert m2.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal('2.34')
def test_as_rounded_rounding_mode(): set_precision_provider(babel_precision_provider.get_precision) prec2 = Decimal("0.01") m1 = Money("2.345", "EUR") m2 = Money("2.344", "EUR") assert m1.as_rounded(2).value == Decimal("2.34") assert m2.as_rounded(2).value == Decimal("2.34") from decimal import ROUND_HALF_DOWN, ROUND_HALF_UP, ROUND_FLOOR assert m1.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal("2.34") assert m2.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal("2.34") assert m1.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal("2.35") assert m2.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal("2.34") assert m1.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal("2.34") assert m2.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal("2.34")
def test_money_property_set_invalid_unit(): w = get_wallet() with pytest.raises(UnitMixupError): w.amount = Money(3, 'USD')
def get_total_unpaid_amount(self): difference = self.taxful_total_price.amount - self.get_total_paid_amount( ) return max(difference, Money(0, self.currency))
def get_total_refunded_amount(self): total = sum( [line.taxful_price.amount.value for line in self.lines.refunds()]) return Money(-total, self.currency)
def usd(value): """ Get Money with USD currency for given value. """ return Money(value, "USD")
def test_format_money(): assert format_money(Money("3.6", "EUR"), locale="fi") == "3,60\xa0\u20ac" assert format_money(Money("3.6", "EUR"), widen=2, locale="fi") == "3,6000\xa0\u20ac" assert format_money(Money("3.6", "EUR"), digits=0, locale="fi") == "4\xa0\u20ac"
def test_broken_order(admin_user): """ """ quantities = [44, 23, 65] expected = sum(quantities) * 50 expected_based_on = expected / 1.5 # Shuup is calculating taxes per line so there will be some "errors" expected_based_on = ensure_decimal_places( Decimal("%s" % (expected_based_on + 0.01))) shop = get_default_shop() supplier = get_default_supplier() product1 = create_product("simple-test-product1", shop, supplier, 50) product2 = create_product("simple-test-product2", shop, supplier, 50) product3 = create_product("simple-test-product3", shop, supplier, 50) tax = get_default_tax() source = BasketishOrderSource(get_default_shop()) billing_address = get_address(country="US") shipping_address = get_address(name="Test street", country="US") source.status = get_initial_order_status() source.billing_address = billing_address source.shipping_address = shipping_address source.customer = create_random_person() source.payment_method = get_default_payment_method() source.shipping_method = get_default_shipping_method() source.add_line( type=OrderLineType.PRODUCT, product=product1, supplier=get_default_supplier(), quantity=quantities[0], base_unit_price=source.create_price(50), ) source.add_line( type=OrderLineType.PRODUCT, product=product2, supplier=get_default_supplier(), quantity=quantities[1], base_unit_price=source.create_price(50), ) source.add_line( type=OrderLineType.PRODUCT, product=product3, supplier=get_default_supplier(), quantity=quantities[2], base_unit_price=source.create_price(50), ) currency = "EUR" summary = source.get_tax_summary() assert len(summary) == 1 summary = summary[0] assert summary.taxful == Money(expected, "EUR") assert summary.based_on == Money(expected_based_on, "EUR") # originally non-rounded value assert bankers_round(source.get_total_tax_amount()) == summary.tax_amount assert source.taxless_total_price.value == expected_based_on assert summary.taxful.value == source.taxful_total_price.value assert summary.tax_amount == Money( bankers_round(source.taxful_total_price.value - source.taxless_total_price.value), currency) assert summary.taxful == summary.raw_based_on + summary.tax_amount assert summary.tax_rate == tax.rate assert summary.taxful.value == ( summary.based_on + summary.tax_amount).value - Decimal("%s" % 0.01) # create order from basket creator = OrderCreator() order = creator.create_order(source) assert order.taxless_total_price.value == expected_based_on # originally non-rounded value assert bankers_round(order.get_total_tax_amount()) == summary.tax_amount
def money_sum(iterable): return sum(iterable, Money(0, price.currency))
def test_money_init_from_value_with_currency(): class Dollar(int): currency = "USD" assert Money(Dollar(42)) == Money(42, "USD")
def test_str(): assert str(Money("42.25", "EUR")) == "42.25 EUR" assert str(Money("100", "USD")) == "100 USD" assert str(Money(42, "EUR")) == "42 EUR" assert str(Money("12.345", "EUR")) == "12.345 EUR"
def test_money_property_get(): w = get_wallet() assert w.amount == Money(42, 'EUR')
def test_refunds(admin_user): shop = get_default_shop() supplier = get_default_supplier() product = create_product( "test-sku", shop=get_default_shop(), default_price=10, ) tax_rate = Decimal("0.1") taxless_base_unit_price = shop.create_price(200) order = create_order_with_product(product, supplier, 3, taxless_base_unit_price, tax_rate, shop=shop) order.payment_status = PaymentStatus.DEFERRED order.cache_prices() order.save() assert len(order.lines.all()) == 1 assert order.can_create_refund() assert not order.has_refunds() client = _get_client(admin_user) # Refund first and the only order line in 3 parts refund_url = "/api/shuup/order/%s/create_refund/" % order.id product_line = order.lines.first() data = { "refund_lines": [{ "line": product_line.id, "quantity": 1, "amount": (product_line.taxful_price.amount.value / 3), "restock_products": False }] } # First refund response = client.post(refund_url, data, format="json") assert response.status_code == status.HTTP_201_CREATED order.refresh_from_db() assert order.lines.count() == 2 assert order.has_refunds() # Second refund response = client.post(refund_url, data, format="json") assert response.status_code == status.HTTP_201_CREATED order.refresh_from_db() assert order.lines.count() == 3 assert order.can_create_refund() # Third refund response = client.post(refund_url, data, format="json") assert response.status_code == status.HTTP_201_CREATED order.refresh_from_db() assert order.lines.count() == 4 assert not order.can_create_refund() assert not order.taxful_total_price.amount assert order.get_total_tax_amount() == Money( (order.taxful_total_price_value - order.taxless_total_price_value), order.currency) # Test error message response = client.post(refund_url, data, format="json") assert response.status_code == status.HTTP_400_BAD_REQUEST error_msg = json.loads(response.content.decode("utf-8"))["error"] assert error_msg == "Order can not be refunded at the moment."
def test_str(): assert str(Money('42.25', 'EUR')) == '42.25 EUR' assert str(Money('100', 'USD')) == '100 USD' assert str(Money(42, 'EUR')) == '42 EUR' assert str(Money('12.345', 'EUR')) == '12.345 EUR'
def test_repr(): assert repr(Money(42, 'EUR')) == "Money('42', 'EUR')" assert repr(Money('42.123', 'EUR')) == "Money('42.123', 'EUR')" assert repr(Money('42.0', 'EUR')) == "Money('42.0', 'EUR')" assert repr(Money('42.123', 'EUR')) == "Money('42.123', 'EUR')" assert repr(Money('42.123', 'USD')) == "Money('42.123', 'USD')"
def test_money_init_from_money(): assert Money(Money(123, 'GBP')) == Money(123, 'GBP')
def test_repr(): assert repr(Money(42, "EUR")) == "Money('42', 'EUR')" assert repr(Money("42.123", "EUR")) == "Money('42.123', 'EUR')" assert repr(Money("42.0", "EUR")) == "Money('42.0', 'EUR')" assert repr(Money("42.123", "EUR")) == "Money('42.123', 'EUR')" assert repr(Money("42.123", "USD")) == "Money('42.123', 'USD')"
def test_order_creator(rf, admin_user): source = seed_source(admin_user) source.add_line( type=OrderLineType.PRODUCT, product=get_default_product(), supplier=get_default_supplier(), quantity=1, base_unit_price=source.create_price(10), ) source.add_line(accounting_identifier="strawberries", type=OrderLineType.OTHER, quantity=1, base_unit_price=source.create_price(10), require_verification=True, extra={"runner": "runner"}) the_line = [ sl for sl in source.get_lines() if sl.accounting_identifier == "strawberries" ] assert the_line[0].data["extra"]["runner"] == "runner" creator = OrderCreator() order = creator.create_order(source) zero = Money(0, order.currency) taxful_total_price = TaxfulPrice(-50, order.currency) last_price = order.taxful_total_price order.taxful_total_price = taxful_total_price order.save() assert not order.is_paid() assert not order.is_canceled() assert not order.get_total_unpaid_amount() > zero assert order.get_total_unpaid_amount() == zero assert not order.get_total_unpaid_amount() < zero assert not order.can_create_payment() order.taxful_total_price = last_price order.save() assert not order.is_paid() assert not order.is_canceled() assert order.get_total_unpaid_amount() > zero assert not order.get_total_unpaid_amount() == zero assert not order.get_total_unpaid_amount() < zero assert order.can_create_payment() order.set_canceled() assert not order.is_paid() assert order.is_canceled() assert order.get_total_unpaid_amount() > zero assert not order.get_total_unpaid_amount() == zero assert not order.get_total_unpaid_amount() < zero assert not order.can_create_payment() order.create_payment(order.get_total_unpaid_amount()) assert order.is_paid() assert order.is_canceled() assert not order.get_total_unpaid_amount() > zero assert order.get_total_unpaid_amount() == zero assert not order.get_total_unpaid_amount() < zero assert not order.can_create_payment() with pytest.raises(NoPaymentToCreateException): order.create_payment(order.get_total_unpaid_amount()) order.create_payment(order.get_total_unpaid_amount() + Money(10, order.currency)) order.create_payment(order.get_total_unpaid_amount() - Money(10, order.currency)) assert get_data_dict(source.billing_address) == get_data_dict( order.billing_address) assert get_data_dict(source.shipping_address) == get_data_dict( order.shipping_address) customer = source.customer assert customer == order.customer assert customer.groups.count() == 1 assert customer.groups.first() == order.customer_groups.first() assert customer.tax_group is not None assert customer.tax_group == order.tax_group assert source.payment_method == order.payment_method assert source.shipping_method == order.shipping_method assert order.pk assert order.lines.filter(accounting_identifier="strawberries").first( ).extra_data["runner"] == "runner"
def test_order_partial_refund_with_taxes(include_tax): tax_rate = Decimal(0.2) # 20% product_price = 100 discount_amount = 30 random_line_price = 5 refunded_amount = 15 shop = factories.get_shop(include_tax) source = OrderSource(shop) source.status = factories.get_initial_order_status() supplier = factories.get_default_supplier() create_default_order_statuses() tax = factories.get_tax("sales-tax", "Sales Tax", tax_rate) factories.create_default_tax_rule(tax) product = factories.create_product("sku", shop=shop, supplier=supplier, default_price=product_price) line = source.add_line( line_id="product-line", type=OrderLineType.PRODUCT, product=product, supplier=supplier, quantity=1, shop=shop, base_unit_price=source.create_price(product_price), ) discount_line = source.add_line( line_id="discount-line", type=OrderLineType.DISCOUNT, supplier=supplier, quantity=1, base_unit_price=source.create_price(0), discount_amount=source.create_price(discount_amount), parent_line_id=line.line_id) raw_total_price = Decimal(product_price - discount_amount) total_taxful = bround(source.taxful_total_price.value) total_taxless = bround(source.taxless_total_price.value) if include_tax: assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: assert total_taxful == bround(raw_total_price * (1 + tax_rate)) assert total_taxless == bround(raw_total_price) creator = OrderCreator() order = creator.create_order(source) assert order.taxful_total_price.value == total_taxful assert order.taxless_total_price.value == total_taxless order.create_payment(order.taxful_total_price) assert order.is_paid() refund_data = [ dict( amount=Money(refunded_amount, shop.currency), quantity=1, line=order.lines.products().first(), ) ] order.create_refund(refund_data) total_taxful = bround(order.taxful_total_price.value) total_taxless = bround(order.taxless_total_price.value) taxless_refunded_amount = (refunded_amount / (1 + tax_rate)) if include_tax: raw_total_price = Decimal(product_price - discount_amount - refunded_amount) assert total_taxful == bround(raw_total_price) assert total_taxless == bround(raw_total_price / (1 + tax_rate)) else: # the refunded amount it considered a taxful price internally raw_total_price = Decimal(product_price - discount_amount) assert total_taxful == bround((raw_total_price * (1 + tax_rate)) - refunded_amount) assert total_taxless == bround(raw_total_price - taxless_refunded_amount) refund_line = order.lines.refunds().filter( type=OrderLineType.REFUND).first() if include_tax: assert refund_line.taxful_price.value == -bround(refunded_amount) assert refund_line.taxless_price.value == -bround( taxless_refunded_amount) else: assert refund_line.taxful_price.value == -bround(refunded_amount) assert refund_line.taxless_price.value == -bround( taxless_refunded_amount)
def money_round(value): return Money(value, shop.currency).as_rounded(2)
def get_total_tax_amount(self): return sum((line.tax_amount for line in self.lines.all()), Money(0, self.currency))
def test_refunds(): shop = get_default_shop() supplier = get_default_supplier() product = create_product( "test-sku", shop=get_default_shop(), default_price=10, ) tax_rate = Decimal("0.1") taxless_base_unit_price = shop.create_price(200) order = create_order_with_product(product, supplier, 3, taxless_base_unit_price, tax_rate, shop=shop) order.payment_status = PaymentStatus.DEFERRED order.cache_prices() order.save() assert order.get_total_refunded_amount().value == 0 assert order.get_total_unrefunded_amount( ).value == order.taxful_total_price.value assert not order.can_edit() assert len(order.lines.all()) == 1 product_line = order.lines.first() assert product_line.ordering == 0 assert order.can_create_refund() assert not order.has_refunds() order.create_refund([{ "line": product_line, "quantity": 1, "amount": (product_line.taxful_price.amount / 3) }]) assert len(order.lines.all()) == 2 assert order.lines.last().ordering == 1 assert order.has_refunds() # Confirm the value of the refund assert order.lines.last().taxful_price == -product_line.base_unit_price assert order.lines.last().tax_amount == -( product_line.taxless_base_unit_price * tax_rate).amount # Create a refund with a parent line and amount order.create_refund([{ "line": product_line, "quantity": 1, "amount": product_line.taxful_price.amount / 3 }]) assert len(order.lines.all()) == 3 assert order.lines.last().ordering == 2 assert order.lines.last( ).taxful_price.amount == -taxless_base_unit_price.amount * (1 + tax_rate) assert order.lines.last( ).tax_amount == -taxless_base_unit_price.amount * tax_rate assert order.taxless_total_price.amount == taxless_base_unit_price.amount assert order.taxful_total_price.amount == taxless_base_unit_price.amount * ( 1 + tax_rate) assert order.can_create_refund() assert order.get_total_tax_amount() == Money( (order.taxful_total_price_value - order.taxless_total_price_value), order.currency) # Try to refunding remaining amount without a parent line with pytest.raises(AssertionError): order.create_refund([{"amount": taxless_base_unit_price}]) # refund remaining amount order.create_refund([{ "line": product_line, "quantity": 1, "amount": product_line.taxful_price.amount / 3 }]) assert len(order.lines.all()) == 4 assert order.lines.last().ordering == 3 assert order.lines.last( ).taxful_price.amount == -taxless_base_unit_price.amount * (1 + tax_rate) assert not order.taxful_total_price.amount assert not order.can_create_refund() assert order.get_total_tax_amount() == Money( (order.taxful_total_price_value - order.taxless_total_price_value), order.currency) with pytest.raises(RefundExceedsAmountException): order.create_refund([{ "line": product_line, "quantity": 1, "amount": taxless_base_unit_price.amount }])
def tax_amount(self): """ :rtype: shuup.utils.money.Money """ zero = Money(0, self.order.currency) return sum((x.amount for x in self.taxes.all()), zero)
def test_product_summary(): shop = get_default_shop() supplier = get_simple_supplier() product = create_product("test-sku", shop=get_default_shop(), default_price=10, stock_behavior=StockBehavior.STOCKED) supplier.adjust_stock(product.id, 5) # Order with 2 unshipped, non-refunded items and a shipping cost order = create_order_with_product(product, supplier, 2, 200, shop=shop) order.cache_prices() product_line = order.lines.first() shipping_line = order.lines.create(type=OrderLineType.SHIPPING, base_unit_price_value=5, quantity=1) # Make sure no invalid entries and check product quantities product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=0, refunded=0, unshipped=2) # Create a shipment for the other item, make sure status changes assert order.shipping_status == ShippingStatus.NOT_SHIPPED assert order.can_create_shipment() order.create_shipment(supplier=supplier, product_quantities={product: 1}) assert order.shipping_status == ShippingStatus.PARTIALLY_SHIPPED order.create_refund([{ "line": shipping_line, "quantity": 1, "amount": Money(5, order.currency), "restock": False }]) product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=1, refunded=0, unshipped=1) # Create a refund for 2 items, we should get no negative values order.create_refund([{ "line": product_line, "quantity": 2, "amount": Money(200, order.currency), "restock": False }]) product_summary = order.get_product_summary() assert all(product_summary.keys()) summary = product_summary[product.id] assert_defaultdict_values(summary, ordered=2, shipped=1, refunded=2, unshipped=0)
def get_total_refunded_amount(self): # rounding here to follow suit with cache_prices total = sum(_round_price(line.taxful_price.amount.value) for line in self.lines.refunds()) return Money(-total, self.currency)
def test_as_rounded_rounding_mode(): set_precision_provider(babel_precision_provider.get_precision) prec2 = Decimal("0.01") m1 = Money("2.345", "EUR") m2 = Money("2.344", "EUR") assert m1.as_rounded(2).value == Decimal("2.34") assert m2.as_rounded(2).value == Decimal("2.34") from decimal import ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_UP assert m1.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal("2.34") assert m2.as_rounded(2, rounding=ROUND_HALF_DOWN).value == Decimal("2.34") assert m1.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal("2.35") assert m2.as_rounded(2, rounding=ROUND_HALF_UP).value == Decimal("2.34") assert m1.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal("2.34") assert m2.as_rounded(2, rounding=ROUND_FLOOR).value == Decimal("2.34")
def get_total_paid_amount(self): amounts = self.payments.values_list('amount_value', flat=True) return Money(sum(amounts, Decimal(0)), self.currency)
def test_money_init_does_not_call_settings(): def guarded_getattr(self, name): assert False, "nobody should read settings yet" with patch.object(type(settings), "__getattr__", guarded_getattr): Money(42, "EUR")
def create_refund(self, refund_data, created_by=None): """ Create a refund if passed a list of refund line data. Refund line data is simply a list of dictionaries where each dictionary contains data for a particular refund line. Additionally, if the parent line is of enum type `OrderLineType.PRODUCT` and the `restock_products` boolean flag is set to `True`, the products will be restocked with the order's supplier the exact amount of the value of the `quantity` field. :param refund_data: List of dicts containing refund data. :type refund_data: [dict] :param created_by: Refund creator's user instance, used for adjusting supplier stock. :type created_by: django.contrib.auth.User|None """ index = self.lines.all().aggregate( models.Max("ordering"))["ordering__max"] zero = Money(0, self.currency) refund_lines = [] total_refund_amount = zero order_total = self.taxful_total_price.amount product_summary = self.get_product_summary() for refund in refund_data: index += 1 amount = refund.get("amount", zero) quantity = refund.get("quantity", 0) parent_line = refund.get("line") restock_products = refund.get("restock_products") refund_line = None assert parent_line assert quantity # ensure the amount to refund and the order line amount have the same signs if ((amount > zero and parent_line.taxful_price.amount < zero) or (amount < zero and parent_line.taxful_price.amount > zero)): raise InvalidRefundAmountException if abs(amount) > abs(parent_line.max_refundable_amount): raise RefundExceedsAmountException # If restocking products, calculate quantity of products to restock product = parent_line.product if (restock_products and quantity and product and (product.stock_behavior == StockBehavior.STOCKED)): from shuup.core.suppliers.enums import StockAdjustmentType # restock from the unshipped quantity first unshipped_quantity_to_restock = min( quantity, product_summary[product.pk]["unshipped"]) shipped_quantity_to_restock = min( quantity - unshipped_quantity_to_restock, product_summary[product.pk]["ordered"] - product_summary[product.pk]["refunded"]) if unshipped_quantity_to_restock > 0: product_summary[product.pk][ "unshipped"] -= unshipped_quantity_to_restock parent_line.supplier.adjust_stock( product.id, unshipped_quantity_to_restock, created_by=created_by, type=StockAdjustmentType.RESTOCK_LOGICAL) if shipped_quantity_to_restock > 0: parent_line.supplier.adjust_stock( product.id, shipped_quantity_to_restock, created_by=created_by, type=StockAdjustmentType.RESTOCK) product_summary[product.pk]["refunded"] += quantity base_amount = amount if self.prices_include_tax else amount / ( 1 + parent_line.tax_rate) refund_line = OrderLine.objects.create( text=_("Refund for %s" % parent_line.text), order=self, type=OrderLineType.REFUND, parent_line=parent_line, ordering=index, base_unit_price_value=-(base_amount / quantity), quantity=quantity, ) for line_tax in parent_line.taxes.all(): tax_base_amount = amount / (1 + line_tax.tax.rate) tax_amount = tax_base_amount * line_tax.tax.rate refund_line.taxes.create(tax=line_tax.tax, name=_("Refund for %s" % line_tax.name), amount_value=-tax_amount, base_amount_value=-tax_base_amount, ordering=line_tax.ordering) total_refund_amount += refund_line.taxful_price.amount refund_lines.append(refund_line) if abs(total_refund_amount) > order_total: raise RefundExceedsAmountException self.cache_prices() self.save() self.update_shipping_status() refund_created.send(sender=type(self), order=self, refund_lines=refund_lines)
def test_money_without_currency(): with pytest.raises(TypeError): Money(42)
def get_total_unrefunded_amount(self): return max(self.taxful_total_price.amount, Money(0, self.currency))
def test_money_init_from_money(): assert Money(Money(123, "GBP")) == Money(123, "GBP")