from __future__ import absolute_import, unicode_literals
from decimal import ROUND_HALF_UP, Decimal
import plata
from plata.discount.models import DiscountBase
[docs]class ProcessorBase(object):
"""
Order processor class base. Offers helper methods for order total
aggregation and tax calculation.
"""
def __init__(self, shared_state):
self.shared_state = shared_state
[docs] def split_cost(self, cost_incl_tax, tax_rate):
"""Split a cost incl. tax into the part excl. tax and the tax"""
cost_incl_tax, tax_rate = Decimal(cost_incl_tax), Decimal(tax_rate)
cost_excl_tax = cost_incl_tax / (1 + tax_rate / 100)
return cost_excl_tax, cost_incl_tax - cost_excl_tax
[docs] def add_tax_details(self, tax_details, tax_rate, price, discount, tax_amount):
"""
Add tax details grouped by tax_rate. Especially useful if orders
potentially use more than one tax class. These values are not used
for the order total calculation -- they are only needed to show the
tax amount for different tax rates if this is necessary for your
invoices.
- ``tax_details``: The tax details dict, most often stored as
``order.data['tax_details'] = tax_details.items()``
- ``tax_rate``: The tax rate of the current entry
- ``price``: The price excl. tax
- ``discount``: The discount amount (will be subtracted from the
price before applying the tax)
- ``tax_amount``: The exact amount; a bit redundant because this
could be calculated using the values above as well
See the taxes documentation or the standard invoice PDF generation
code if you need to know more about the use of these values.
"""
zero = Decimal("0.00")
discount = discount or zero
row = tax_details.setdefault(
tax_rate,
{
"prices": zero,
"discounts": zero,
"tax_rate": tax_rate,
"tax_amount": zero,
"total": zero,
},
)
row["prices"] += price
row["discounts"] += discount
row["tax_amount"] += tax_amount
row["total"] += price - discount + tax_amount
def set_processor_value(self, group, key, value):
self.shared_state.setdefault(group, {})[key] = value
def get_processor_value(self, group, key=None):
dic = self.shared_state.get(group, {})
if key:
return dic.get(key)
return dic
[docs] def process(self, order, items):
"""
This is the method which must be implemented in order processor
classes.
"""
raise NotImplementedError # pragma: no cover
[docs]class InitializeOrderProcessor(ProcessorBase):
"""
Zero out all relevant order values and calculate line item prices
excl. tax.
"""
def process(self, order, items):
order.items_subtotal = Decimal("0.00")
order.items_tax = Decimal("0.00")
order.items_discount = Decimal("0.00")
for item in items:
# Recalculate item stuff
item._line_item_price = item.quantity * item._unit_price
item._line_item_discount = Decimal("0.00")
[docs]class DiscountProcessor(ProcessorBase):
"""
Apply all discounts which do not act as a means of payment but instead
act on the subtotal
"""
def process(self, order, items):
remaining = Decimal("0.00")
for applied in order.applied_discounts.exclude(
type=DiscountBase.MEANS_OF_PAYMENT
):
applied.apply(order, items)
remaining += applied.remaining
discounts = order.data.get("discounts", {})
discounts["remaining_subtotal"] = remaining
order.data["discounts"] = discounts
[docs]class MeansOfPaymentDiscountProcessor(ProcessorBase):
"""
Apply all discounts which act as a means of payment.
"""
def process(self, order, items):
remaining = Decimal("0.00")
for applied in order.applied_discounts.filter(
type=DiscountBase.MEANS_OF_PAYMENT
):
applied.apply(order, items)
remaining += applied.remaining
discounts = order.data.get("discounts", {})
discounts["remaining_means_of_payment"] = remaining
order.data["discounts"] = discounts
[docs]class TaxProcessor(ProcessorBase):
"""
Calculate taxes for every line item and aggregate tax details.
"""
def process(self, order, items):
tax_details = {}
for item in items:
taxable = item._line_item_price - (item._line_item_discount or 0)
item._line_item_tax = (taxable * item.tax_rate / 100).quantize(
Decimal("0.0000000000")
)
self.add_tax_details(
tax_details,
item.tax_rate,
item._line_item_price,
item._line_item_discount,
item._line_item_tax,
)
order.data["tax_details"] = list(tax_details.items())
[docs]class ItemSummationProcessor(ProcessorBase):
"""
Sum up line item prices, discounts and taxes.
"""
def process(self, order, items):
for item in items:
order.items_subtotal += item._line_item_price
order.items_discount += item._line_item_discount or 0
order.items_tax += item._line_item_tax
self.set_processor_value(
"total",
"items",
order.items_subtotal - order.items_discount + order.items_tax,
)
[docs]class ZeroShippingProcessor(ProcessorBase):
"""
Set shipping costs to zero.
"""
def process(self, order, items):
order.shipping_cost = Decimal("0.00")
order.shipping_discount = Decimal("0.00")
order.shipping_tax = Decimal("0.00")
# Not strictly necessary
self.set_processor_value("total", "shipping", 0)
[docs]class FixedAmountShippingProcessor(ProcessorBase):
"""
Set shipping costs to a fixed value. Uses ``PLATA_SHIPPING_FIXEDAMOUNT``.
If you have differing needs you should probably implement your own
shipping processor (and propose it for inclusion if you like) instead
of extending this one.
::
PLATA_SHIPPING_FIXEDAMOUNT = {
'cost': Decimal('8.00'),
'tax': Decimal('19.6'),
}
"""
def process(self, order, items):
cost = plata.settings.PLATA_SHIPPING_FIXEDAMOUNT["cost"]
tax = plata.settings.PLATA_SHIPPING_FIXEDAMOUNT["tax"]
order.shipping_cost, __ = self.split_cost(cost, tax)
order.shipping_discount = min(order.discount_remaining, order.shipping_cost)
order.shipping_tax = tax / 100 * (order.shipping_cost - order.shipping_discount)
self.set_processor_value(
"total",
"shipping",
order.shipping_cost - order.shipping_discount + order.shipping_tax,
)
tax_details = dict(order.data.get("tax_details", []))
self.add_tax_details(
tax_details,
tax,
order.shipping_cost,
order.shipping_discount,
order.shipping_tax,
)
order.data["tax_details"] = list(tax_details.items())
[docs]class ApplyRemainingDiscountToShippingProcessor(ProcessorBase):
"""
Apply the remaining discount to the shipping (if shipping is non-zero
and there are any remaining discounts left)
"""
def process(self, order, items):
raise NotImplementedError(
"ApplyRemainingDiscountToShippingProcessor is not implemented yet"
)
[docs]class OrderSummationProcessor(ProcessorBase):
"""
Sum up order total by adding up items and shipping totals.
"""
[docs] def process(self, order, items):
"""
The value must be quantized here, because otherwise f.e. the payment
modules will be susceptible to rounding errors giving f.e. missing
payments of 0.01 units.
"""
total = sum(self.get_processor_value("total").values(), Decimal("0.00"))
order.total = total.quantize(Decimal("0.00"), rounding=ROUND_HALF_UP)