def from_line_taxes(cls, line_taxes, untaxed): """ Create TaxSummary from LineTaxes. :param line_taxes: List of line taxes to summarize :type line_taxes: list[LineTax] :param untaxed: Sum of taxless prices that have no taxes added :type untaxed: shuup.core.pricing.TaxlessPrice """ zero_amount = Money(0, untaxed.currency) tax_amount_by_tax = defaultdict(lambda: zero_amount) raw_base_amount_by_tax = defaultdict(lambda: zero_amount) base_amount_by_tax = defaultdict(lambda: zero_amount) for line_tax in line_taxes: assert isinstance(line_tax, LineTax) tax_amount_by_tax[line_tax.tax] += line_tax.amount raw_base_amount_by_tax[line_tax.tax] += line_tax.base_amount base_amount_by_tax[line_tax.tax] += bankers_round(line_tax.base_amount, 2) lines = [ TaxSummaryLine.from_tax(tax, base_amount_by_tax[tax], raw_base_amount_by_tax[tax], tax_amount) for (tax, tax_amount) in tax_amount_by_tax.items() ] if untaxed: lines.append( TaxSummaryLine( tax_id=None, tax_code='', tax_name=_("Untaxed"), tax_rate=Decimal(0), based_on=bankers_round(untaxed.amount, 2), raw_based_on=untaxed.amount, tax_amount=zero_amount)) return cls(sorted(lines, key=TaxSummaryLine.get_sort_key))
def test_rounding(prices): expected = 0 for p in prices: expected += bankers_round(p, 2) order = create_empty_order(prices_include_tax=False) order.save() currency = order.shop.currency for x, price in enumerate(prices): ol = OrderLine(order=order, type=OrderLineType.OTHER, quantity=1, text="Thing", ordering=x, base_unit_price=order.shop.create_price(price)) ol.save() order.cache_prices() for x, order_line in enumerate(order.lines.all().order_by("ordering")): price = Decimal(prices[x]).quantize(Decimal(".1")**9) # make sure prices are in database with original precision assert order_line.base_unit_price == order.shop.create_price(price) # make sure the line taxless price is rounded assert order_line.taxless_price == order.shop.create_price( bankers_round(price, 2)) # make sure the line price is rounded assert order_line.price == order.shop.create_price(price) # make sure order total is rounded assert order.taxless_total_price == order.shop.create_price( bankers_round(expected, 2))
def __get__(self, instance, type=None): if instance is None: return self taxful = self.params.get("includes_tax", instance.prices_include_tax) zero = (TaxfulPrice if taxful else TaxlessPrice)(0, instance.currency) lines = getattr(instance, self.line_getter)() return sum((bankers_round(getattr(x, self.field), 2) for x in lines), zero)
def source_line_to_order_lines(self, order, source_line): """ Convert a source line into one or more order lines. Normally each source line will yield just one order line, but package products will yield lines for both the parent and its children products. :type order: shuup.core.models.Order :param order: The order. :type source_line: shuup.core.order_creator.SourceLine :param source_line: The SourceLine. :rtype: Iterable[OrderLine] """ order_line = OrderLine(order=order) product = source_line.product quantity = Decimal(source_line.quantity) if product: order_line.product = product if product.sales_unit: quantized_quantity = bankers_round(quantity, product.sales_unit.decimals) if quantized_quantity != quantity: raise ValueError( "Error! Sales unit decimal conversion causes precision loss." ) else: order_line.product = None def text(value): return force_text(value) if value is not None else "" order_line.quantity = quantity order_line.supplier = source_line.supplier order_line.sku = text(source_line.sku) order_line.text = (text(source_line.text))[:192] if source_line.base_unit_price: order_line.base_unit_price = source_line.base_unit_price if source_line.discount_amount: order_line.discount_amount = source_line.discount_amount order_line.type = (source_line.type if source_line.type is not None else OrderLineType.OTHER) order_line.accounting_identifier = text( source_line.accounting_identifier) order_line.require_verification = bool( source_line.require_verification) order_line.verified = (not order_line.require_verification) order_line.source_line = source_line order_line.parent_source_line = source_line.parent_line extra_data = source_line.data.get("extra", {}) if hasattr( source_line, "data") else {} extra_data.update({"source_line_id": source_line.line_id}) order_line.extra_data = extra_data self._check_orderability(order_line) yield order_line for child_order_line in self.create_package_children(order_line): yield child_order_line
def __get__(self, instance, type=None): if instance is None: return self taxful = self.params.get('includes_tax', instance.prices_include_tax) zero = (TaxfulPrice if taxful else TaxlessPrice)(0, instance.currency) lines = getattr(instance, self.line_getter)() return sum((bankers_round(getattr(x, self.field), 2) for x in lines), zero)
def taxful_price(self): """ :rtype: TaxfulPrice """ price = self.price return bankers_round( (price if price.includes_tax else TaxfulPrice(price.amount + self.tax_amount)), 2)
def taxless_price(self): """ :rtype: TaxlessPrice """ price = self.price return bankers_round( (TaxlessPrice(price.amount - self.tax_amount) if price.includes_tax else price), 2)
def __init__(self, tax_id, tax_code, tax_name, tax_rate, based_on, raw_based_on, tax_amount): self.tax_id = tax_id self.tax_code = tax_code self.tax_name = tax_name self.tax_rate = tax_rate self.raw_based_on = ensure_decimal_places(raw_based_on) self.based_on = ensure_decimal_places(based_on) self.tax_amount = ensure_decimal_places(tax_amount) self.taxful = bankers_round(self.raw_based_on + tax_amount, 2)
def _check_taxful_price(source): for line in source.get_lines(): from_components = bankers_round(line.taxful_base_unit_price*line.quantity - line.taxful_discount_amount, 2) assert from_components == line.taxful_price assert line.price == line.base_unit_price * line.quantity - line.discount_amount assert line.discount_amount == line.base_price - line.price if line.base_price: assert line.discount_rate == (1 - (line.price / line.base_price)) assert line.discount_percentage == 100 * (1 - (line.price / line.base_price)) if line.quantity: assert line.unit_discount_amount == line.discount_amount / line.quantity
def source_line_to_order_lines(self, order, source_line): """ Convert a source line into one or more order lines. Normally each source line will yield just one order line, but package products will yield a parent line and its child lines. :type order: shuup.core.models.Order :param order: The order :type source_line: shuup.core.order_creator.SourceLine :param source_line: The SourceLine :rtype: Iterable[OrderLine] """ order_line = OrderLine(order=order) product = source_line.product quantity = Decimal(source_line.quantity) if product: order_line.product = product if product.sales_unit: quantized_quantity = bankers_round(quantity, product.sales_unit.decimals) if quantized_quantity != quantity: raise ValueError("Sales unit decimal conversion causes precision loss!") else: order_line.product = None def text(value): return force_text(value) if value is not None else "" order_line.quantity = quantity order_line.supplier = source_line.supplier order_line.sku = text(source_line.sku) order_line.text = (text(source_line.text))[:192] if source_line.base_unit_price: order_line.base_unit_price = source_line.base_unit_price if source_line.discount_amount: order_line.discount_amount = source_line.discount_amount order_line.type = (source_line.type if source_line.type is not None else OrderLineType.OTHER) order_line.accounting_identifier = text(source_line.accounting_identifier) order_line.require_verification = bool(source_line.require_verification) order_line.verified = (not order_line.require_verification) order_line.source_line = source_line order_line.parent_source_line = source_line.parent_line extra_data = source_line.data.get("extra", {}) if hasattr(source_line, "data") else {} extra_data.update({"source_line_id": source_line.line_id}) order_line.extra_data = extra_data self._check_orderability(order_line) yield order_line for child_order_line in self.create_package_children(order_line): yield child_order_line
def test_order_source_rounding(prices): shop = Shop.objects.create(name="test", identifier="test", status=ShopStatus.ENABLED, public_name="test", prices_include_tax=False) expected = 0 for p in prices: expected += bankers_round(p, 2) source = BasketishOrderSource(shop) for x, price in enumerate(prices): source.add_line( type=OrderLineType.OTHER, quantity=1, text=x, base_unit_price=source.create_price(price), ordering=x, ) for x, order_source in enumerate(source.get_lines()): price = Decimal(prices[x]).quantize(Decimal(".1")**9) # make sure prices are in database with original precision assert order_source.base_unit_price == source.shop.create_price(price) # make sure the line taxless price is rounded assert order_source.taxless_price == source.shop.create_price( bankers_round(price, 2)) # Check that total prices calculated from priceful parts still matches assert _get_taxless_price(order_source) == order_source.taxless_price assert _get_taxful_price(order_source) == order_source.taxful_price # make sure the line price is rounded assert order_source.price == source.shop.create_price(price) # make sure order total is rounded assert source.taxless_total_price == source.shop.create_price( bankers_round(expected, 2))
def test_order_source_rounding(prices): shop = Shop.objects.create( name="test", identifier="test", status=ShopStatus.ENABLED, public_name="test", prices_include_tax=False ) expected = 0 for p in prices: expected += bankers_round(p, 2) source = BasketishOrderSource(shop) for x, price in enumerate(prices): source.add_line( type=OrderLineType.OTHER, quantity=1, text=x, base_unit_price=source.create_price(price), ordering=x, ) for x, order_source in enumerate(source.get_lines()): price = Decimal(prices[x]).quantize(Decimal(".1") ** 9) # make sure prices are in database with original precision assert order_source.base_unit_price == source.shop.create_price(price) # make sure the line taxless price is rounded assert order_source.taxless_price == source.shop.create_price(bankers_round(price, 2)) # Check that total prices calculated from priceful parts still matches assert _get_taxless_price(order_source) == order_source.taxless_price assert _get_taxful_price(order_source) == order_source.taxful_price # make sure the line price is rounded assert order_source.price == source.shop.create_price(price) # make sure order total is rounded assert source.taxless_total_price == source.shop.create_price(bankers_round(expected, 2))
def test_rounding(prices): expected = 0 for p in prices: expected += bankers_round(p, 2) order = create_empty_order(prices_include_tax=False) order.save() for x, price in enumerate(prices): ol = OrderLine( order=order, type=OrderLineType.OTHER, quantity=1, text="Thing", ordering=x, base_unit_price=order.shop.create_price(price) ) ol.save() order.cache_prices() for x, order_line in enumerate(order.lines.all().order_by("ordering")): price = Decimal(prices[x]).quantize(Decimal(".1") ** 9) # make sure prices are in database with original precision assert order_line.base_unit_price == order.shop.create_price(price) # make sure the line taxless price is rounded assert order_line.taxless_price == order.shop.create_price(bankers_round(price, 2)) # Check that total prices calculated from priceful parts still matches assert _get_taxless_price(order_line) == order_line.taxless_price assert _get_taxful_price(order_line) == order_line.taxful_price # make sure the line price is rounded assert order_line.price == order.shop.create_price(price) # make sure order total is rounded assert order.taxless_total_price == order.shop.create_price(bankers_round(expected, 2))
def get_chart(self): orders = get_orders_by_currency(self.currency) aggregate_data = group_by_period( orders.valid().since(days=365), "order_date", "month", sum=Sum("taxful_total_price_value") ) locale = get_current_babel_locale() bar_chart = BarChart( title=_("Sales per Month (last year)"), labels=[format_date(k, format=get_year_and_month_format(locale), locale=locale) for k in aggregate_data], ) bar_chart.add_data( _("Sales (%(currency)s)") % {"currency": self.currency}, [bankers_round(v["sum"], 2) for v in aggregate_data.values()], # TODO: To be fixed in SHUUP-1912 ) return bar_chart
def get_chart(self): orders = get_orders_by_currency(self.currency) aggregate_data = group_by_period(orders.valid().since(days=365), "order_date", "month", sum=Sum("taxful_total_price_value")) locale = get_current_babel_locale() bar_chart = BarChart(title=_("Sales per Month (last year)"), labels=[ format_date( k, format=get_year_and_month_format(locale), locale=locale) for k in aggregate_data ]) bar_chart.add_data( _("Sales (%(currency)s)") % {"currency": self.currency}, [ bankers_round(v["sum"], 2) # TODO: To be fixed in SHUUP-1912 for v in aggregate_data.values() ]) return bar_chart
def test_basket_with_package_product(admin_user): with override_settings(**REQUIRED_SETTINGS): shop = factories.get_default_shop() factories.get_default_shipping_method() factories.get_default_payment_method() OrderStatusManager().ensure_default_statuses() client = get_client(admin_user) response = client.post("/api/shuup/basket/new/", format="json", data={"shop": shop.pk}) assert response.status_code == status.HTTP_201_CREATED basket_uuid = response.data["uuid"] supplier = factories.get_supplier(SimpleSupplierModule.identifier, shop=shop, stock_managed=True) # base product - 1kg of sand base_sand_product = factories.create_product( "Sand", shop=shop, supplier=supplier, default_price="15.2", net_weight=Decimal(1) ) # 10kg bag of sand - package made by 10kg of sand sand_bag_10kg_product = factories.create_product( "Sand-bag-10-kg", shop=shop, supplier=supplier, default_price="149.9", net_weight=Decimal(10000) ) sand_bag_10kg_product.make_package({ base_sand_product: 10 }) sand_bag_10kg_product.save() # 18.45kg bag of sand - package made by 18.45kg of sand sand_bag_1845kg_product = factories.create_product( "Sand-bag-1845-kg", shop=shop, supplier=supplier, default_price="179.9", net_weight=Decimal(18450) ) sand_bag_1845kg_product.make_package({ base_sand_product: 18.45 }) sand_bag_1845kg_product.save() # 25kg bag of sand - package made by 25kg of sand sand_bag_25kg_product = factories.create_product( "Sand-bag-25-kg", shop=shop, supplier=supplier, default_price="2450.25", net_weight=Decimal(25000) ) sand_bag_25kg_product.make_package({ base_sand_product: 25 }) sand_bag_25kg_product.save() initial_stock = 55 # put 55 sands (55kg) in stock supplier.adjust_stock(base_sand_product.id, initial_stock) stock_status = supplier.get_stock_status(base_sand_product.id) assert stock_status.physical_count == initial_stock assert stock_status.logical_count == initial_stock # zero stock for packages assert supplier.get_stock_status(sand_bag_10kg_product.id).logical_count == 0 assert supplier.get_stock_status(sand_bag_1845kg_product.id).logical_count == 0 assert supplier.get_stock_status(sand_bag_25kg_product.id).logical_count == 0 # add all the 3 packages to the basket, this will require (10 + 18.45 + 25 = 53.45 Sands) for product in [sand_bag_10kg_product, sand_bag_1845kg_product, sand_bag_25kg_product]: response = client.post("/api/shuup/basket/{}/add/".format(basket_uuid), format="json", data={ "shop": shop.pk, "product": product.id }) assert response.status_code == status.HTTP_200_OK # get the basket response = client.get("/api/shuup/basket/{}/".format(basket_uuid)) assert response.status_code == status.HTTP_200_OK assert response.data["validation_errors"] == [] # now add more 25kg and it shouldn't have enough stock response = client.post("/api/shuup/basket/{}/add/".format(basket_uuid), format="json", data={ "shop": shop.pk, "product": sand_bag_25kg_product.id }) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Insufficient stock" in response.data["error"] # create order anyway response = client.post("/api/shuup/basket/{}/create_order/".format(basket_uuid), format="json") assert response.status_code == status.HTTP_201_CREATED order = Order.objects.get(id=response.data["id"]) line_counter = Counter() for line in order.lines.products(): line_counter[line.product.id] += line.quantity assert bankers_round(line_counter[base_sand_product.id]) == bankers_round( Decimal(10) + Decimal(18.45) + Decimal(25) ) assert line_counter[sand_bag_10kg_product.id] == 1 assert line_counter[sand_bag_1845kg_product.id] == 1 assert line_counter[sand_bag_25kg_product.id] == 1
def _get_taxful_price(line): return bankers_round( line.taxful_base_unit_price * line.quantity - line.taxful_discount_amount, 2)
def bround(value): return bankers_round(value, 2)
def test_basket_with_package_product(admin_user): shop = factories.get_default_shop() factories.get_default_shipping_method() factories.get_default_payment_method() OrderStatusManager().ensure_default_statuses() client = get_client(admin_user) response = client.post("/api/shuup/basket/new/", format="json", data={"shop": shop.pk}) assert response.status_code == status.HTTP_201_CREATED basket_uuid = response.data["uuid"] supplier = factories.get_supplier(SimpleSupplierModule.identifier, shop=shop, stock_managed=True) # base product - 1kg of sand base_sand_product = factories.create_product("Sand", shop=shop, supplier=supplier, default_price="15.2", net_weight=Decimal(1)) # 10kg bag of sand - package made by 10kg of sand sand_bag_10kg_product = factories.create_product("Sand-bag-10-kg", shop=shop, supplier=supplier, default_price="149.9", net_weight=Decimal(10000)) sand_bag_10kg_product.make_package({base_sand_product: 10}) sand_bag_10kg_product.save() # 18.45kg bag of sand - package made by 18.45kg of sand sand_bag_1845kg_product = factories.create_product( "Sand-bag-1845-kg", shop=shop, supplier=supplier, default_price="179.9", net_weight=Decimal(18450)) sand_bag_1845kg_product.make_package({base_sand_product: 18.45}) sand_bag_1845kg_product.save() # 25kg bag of sand - package made by 25kg of sand sand_bag_25kg_product = factories.create_product("Sand-bag-25-kg", shop=shop, supplier=supplier, default_price="2450.25", net_weight=Decimal(25000)) sand_bag_25kg_product.make_package({base_sand_product: 25}) sand_bag_25kg_product.save() initial_stock = 55 # put 55 sands (55kg) in stock supplier.adjust_stock(base_sand_product.id, initial_stock) stock_status = supplier.get_stock_status(base_sand_product.id) assert stock_status.physical_count == initial_stock assert stock_status.logical_count == initial_stock # zero stock for packages assert supplier.get_stock_status( sand_bag_10kg_product.id).logical_count == 0 assert supplier.get_stock_status( sand_bag_1845kg_product.id).logical_count == 0 assert supplier.get_stock_status( sand_bag_25kg_product.id).logical_count == 0 # add all the 3 packages to the basket, this will require (10 + 18.45 + 25 = 53.45 Sands) for product in [ sand_bag_10kg_product, sand_bag_1845kg_product, sand_bag_25kg_product ]: response = client.post( "/api/shuup/basket/{}/add/".format(basket_uuid), format="json", data={ "shop": shop.pk, "product": product.id }, ) assert response.status_code == status.HTTP_200_OK # get the basket response = client.get("/api/shuup/basket/{}/".format(basket_uuid)) assert response.status_code == status.HTTP_200_OK assert response.data["validation_errors"] == [] # now add more 25kg and it shouldn't have enough stock response = client.post( "/api/shuup/basket/{}/add/".format(basket_uuid), format="json", data={ "shop": shop.pk, "product": sand_bag_25kg_product.id }, ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "Insufficient quantity" in response.data["error"] # create order anyway response = client.post( "/api/shuup/basket/{}/create_order/".format(basket_uuid), format="json") assert response.status_code == status.HTTP_201_CREATED order = Order.objects.get(id=response.data["id"]) line_counter = Counter() for line in order.lines.products(): line_counter[line.product.id] += line.quantity assert bankers_round(line_counter[base_sand_product.id]) == bankers_round( Decimal(10) + Decimal(18.45) + Decimal(25)) assert line_counter[sand_bag_10kg_product.id] == 1 assert line_counter[sand_bag_1845kg_product.id] == 1 assert line_counter[sand_bag_25kg_product.id] == 1
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 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 _round_price(value): return bankers_round(value, 2) # TODO: To be fixed in SHUUP-1912
def _get_taxful_price(line): return bankers_round(line.taxful_base_unit_price*line.quantity - line.taxful_discount_amount, 2)
def round(self, value): return bankers_round(parse_decimal_string(value), self.decimals)
def rounded_amount(self): return bankers_round(self.amount, 2)