# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2017, Shoop Commerce Ltd. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals, with_statement
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from jsonfield import JSONField
from shuup.core.fields import MoneyValueField, QuantityField, UnsavedForeignKey
from shuup.core.pricing import Priceful
from shuup.core.taxing import LineTax
from shuup.core.utils.line_unit_mixin import LineWithUnit
from shuup.utils.analog import define_log_model
from shuup.utils.money import Money
from shuup.utils.properties import MoneyProperty, MoneyPropped, PriceProperty
from ._base import ShuupModel
from ._shipments import ShipmentProduct
class OrderLineType(Enum):
PRODUCT = 1
SHIPPING = 2
PAYMENT = 3
DISCOUNT = 4
OTHER = 5
REFUND = 6
ROUNDING = 7
class Labels:
PRODUCT = _('product')
SHIPPING = _('shipping')
PAYMENT = _('payment')
DISCOUNT = _('discount')
OTHER = _('other')
REFUND = _('refund')
ROUNDING = _('rounding')
class OrderLineManager(models.Manager):
def products(self): # pragma: no cover
return self.filter(type=OrderLineType.PRODUCT)
def shipping(self): # pragma: no cover
return self.filter(type=OrderLineType.SHIPPING)
def payment(self): # pragma: no cover
return self.filter(type=OrderLineType.PAYMENT)
def discounts(self):
return self.filter(type=OrderLineType.DISCOUNT)
def refunds(self):
return self.filter(type=OrderLineType.REFUND)
def other(self): # pragma: no cover
return self.filter(type=OrderLineType.OTHER)
@python_2_unicode_compatible
class AbstractOrderLine(MoneyPropped, models.Model, Priceful):
product = UnsavedForeignKey(
"shuup.Product", blank=True, null=True, related_name="order_lines",
on_delete=models.PROTECT, verbose_name=_('product')
)
supplier = UnsavedForeignKey(
"shuup.Supplier", blank=True, null=True, related_name="order_lines",
on_delete=models.PROTECT, verbose_name=_('supplier')
)
parent_line = UnsavedForeignKey(
"self", related_name="child_lines", blank=True, null=True,
on_delete=models.PROTECT, verbose_name=_('parent line')
)
ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
type = EnumIntegerField(OrderLineType, default=OrderLineType.PRODUCT, verbose_name=_('line type'))
sku = models.CharField(max_length=48, blank=True, verbose_name=_('line SKU'))
text = models.CharField(max_length=256, verbose_name=_('line text'))
accounting_identifier = models.CharField(max_length=32, blank=True, verbose_name=_('accounting identifier'))
require_verification = models.BooleanField(default=False, verbose_name=_('require verification'))
verified = models.BooleanField(default=False, verbose_name=_('verified'))
extra_data = JSONField(blank=True, null=True, verbose_name=_('extra data'))
# The following fields govern calculation of the prices
quantity = QuantityField(verbose_name=_('quantity'), default=1)
base_unit_price = PriceProperty('base_unit_price_value', 'order.currency', 'order.prices_include_tax')
discount_amount = PriceProperty('discount_amount_value', 'order.currency', 'order.prices_include_tax')
base_unit_price_value = MoneyValueField(verbose_name=_('unit price amount (undiscounted)'), default=0)
discount_amount_value = MoneyValueField(verbose_name=_('total amount of discount'), default=0)
objects = OrderLineManager()
def __str__(self):
return "%dx %s (%s)" % (self.quantity, self.text, self.get_type_display())
@property
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)
@property
def max_refundable_amount(self):
"""
:rtype: shuup.utils.money.Money
"""
refunds = self.child_lines.refunds().filter(parent_line=self)
refund_total_value = sum(refund.taxful_price.amount.value for refund in refunds)
return (self.taxful_price.amount + Money(refund_total_value, self.order.currency))
@property
def max_refundable_quantity(self):
if self.type != OrderLineType.PRODUCT:
return 0
return self.quantity - self.refunded_quantity
@property
def refunded_quantity(self):
return (
self.child_lines.filter(type=OrderLineType.REFUND).aggregate(total=Sum("quantity"))["total"] or 0
)
@property
def shipped_quantity(self):
if not self.product:
return 0
return ShipmentProduct.objects.filter(
shipment__supplier=self.supplier.id,
product_id=self.product.id,
shipment__order=self.order
).aggregate(total=Sum("quantity"))["total"] or 0
[docs] def save(self, *args, **kwargs):
if not self.sku:
self.sku = u""
if self.type == OrderLineType.PRODUCT and not self.product_id:
raise ValidationError("Product-type order line can not be saved without a set product")
if self.product_id and self.type != OrderLineType.PRODUCT:
raise ValidationError("Order line has product but is not of Product type")
if self.product_id and not self.supplier_id:
raise ValidationError("Order line has product but no supplier")
super(AbstractOrderLine, self).save(*args, **kwargs)
if self.product_id:
self.supplier.module.update_stock(self.product_id)
class OrderLine(LineWithUnit, AbstractOrderLine):
order = UnsavedForeignKey("Order", related_name='lines', on_delete=models.PROTECT, verbose_name=_('order'))
# TODO: Store the display and sales unit to OrderLine
@property
def shop(self):
return self.order.shop
@python_2_unicode_compatible
class OrderLineTax(MoneyPropped, ShuupModel, LineTax):
order_line = models.ForeignKey(
OrderLine, related_name='taxes', on_delete=models.PROTECT,
verbose_name=_('order line'))
tax = models.ForeignKey(
"Tax", related_name="order_line_taxes",
on_delete=models.PROTECT, verbose_name=_('tax'))
name = models.CharField(max_length=200, verbose_name=_('tax name'))
amount = MoneyProperty('amount_value', 'order_line.order.currency')
base_amount = MoneyProperty('base_amount_value', 'order_line.order.currency')
amount_value = MoneyValueField(verbose_name=_('tax amount'))
base_amount_value = MoneyValueField(
verbose_name=_('base amount'),
help_text=_('Amount that this tax is calculated from'))
ordering = models.IntegerField(default=0, verbose_name=_('ordering'))
class Meta:
ordering = ["ordering"]
def __str__(self):
return "%s: %s on %s" % (self.name, self.amount, self.base_amount)
OrderLineLogEntry = define_log_model(OrderLine)
OrderLineTaxLogEntry = define_log_model(OrderLineTax)