# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. 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
from django.conf import settings
from django.db import models
from django.db.transaction import atomic
from django.utils.crypto import get_random_string
from django.utils.encoding import python_2_unicode_compatible
from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumIntegerField
from shuup.core.fields import InternalIdentifierField, MeasurementField, QuantityField
from shuup.core.models import ShuupModel
from shuup.core.signals import shipment_deleted, shipment_sent
from shuup.core.utils.units import get_shuup_volume_unit
from shuup.utils.analog import define_log_model
__all__ = ("Shipment", "ShipmentProduct")
class ShipmentStatus(Enum):
NOT_SENT = 0
SENT = 1
RECEIVED = 2 # if the customer deigns to tell us
ERROR = 10
DELETED = 20
class Labels:
NOT_SENT = _("Not sent")
SENT = _("Sent")
RECEIVED = _("Received")
ERROR = _("Error")
DELETED = _("Deleted")
class ShipmentType(Enum):
OUT = 0
IN = 1
class Labels:
OUT = _("outgoing")
IN = _("incoming")
class ShipmentQueryset(models.QuerySet):
def all_except_deleted(self, language=None, shop=None):
return self.exclude(status=ShipmentStatus.DELETED)
def sent(self):
return self.filter(status=ShipmentStatus.SENT)
class Shipment(ShuupModel):
order = models.ForeignKey(
"Order", blank=True, null=True, related_name="shipments", on_delete=models.PROTECT, verbose_name=_("order")
)
supplier = models.ForeignKey(
"Supplier", related_name="shipments", on_delete=models.PROTECT, verbose_name=_("supplier")
)
created_on = models.DateTimeField(auto_now_add=True, verbose_name=_("created on"))
status = EnumIntegerField(ShipmentStatus, default=ShipmentStatus.NOT_SENT, verbose_name=_("status"))
tracking_code = models.CharField(max_length=64, blank=True, verbose_name=_("tracking code"))
tracking_url = models.URLField(blank=True, verbose_name=_("tracking url"))
description = models.CharField(max_length=255, blank=True, verbose_name=_("description"))
volume = MeasurementField(
unit=get_shuup_volume_unit(), verbose_name=format_lazy(_("volume ({})"), get_shuup_volume_unit())
)
weight = MeasurementField(
unit=settings.SHUUP_MASS_UNIT, verbose_name=format_lazy(_("weight ({})"), settings.SHUUP_MASS_UNIT)
)
identifier = InternalIdentifierField(unique=True)
type = EnumIntegerField(ShipmentType, default=ShipmentType.OUT, verbose_name=_("type"))
objects = ShipmentQueryset.as_manager()
class Meta:
verbose_name = _("shipment")
verbose_name_plural = _("shipments")
def __init__(self, *args, **kwargs):
super(Shipment, self).__init__(*args, **kwargs)
if not self.identifier:
if self.order and self.order.pk:
prefix = "%s/%s/" % (self.order.pk, self.order.shipments.count())
else:
prefix = ""
self.identifier = prefix + get_random_string(32)
def __repr__(self): # pragma: no cover
return "<Shipment %s (tracking %r, created %s)>" % (self.pk, self.tracking_code, self.created_on)
[docs] def save(self, *args, **kwargs):
super(Shipment, self).save(*args, **kwargs)
for product_id in self.products.values_list("product_id", flat=True):
self.supplier.update_stock(product_id=product_id)
[docs] def delete(self, using=None):
raise NotImplementedError("Error! Not implemented: `Shipment` -> `delete()`. Use `soft_delete()` instead.")
@atomic
[docs] def soft_delete(self, user=None):
if self.status == ShipmentStatus.DELETED:
return
self.status = ShipmentStatus.DELETED
self.save(update_fields=["status"])
for product_id in self.products.values_list("product_id", flat=True):
self.supplier.update_stock(product_id=product_id)
if self.order:
self.order.update_shipping_status()
shipment_deleted.send(sender=type(self), shipment=self)
[docs] def is_deleted(self):
return bool(self.status == ShipmentStatus.DELETED)
[docs] def is_sent(self):
return bool(self.status == ShipmentStatus.SENT)
[docs] def cache_values(self):
"""
(Re)cache `.volume` and `.weight` for this Shipment from within the ShipmentProducts.
"""
total_volume = 0
total_weight = 0
for quantity, volume, weight in self.products.values_list("quantity", "unit_volume", "unit_weight"):
total_volume += quantity * volume
total_weight += quantity * weight
self.volume = total_volume
self.weight = total_weight
@property
def total_products(self):
return self.products.aggregate(quantity=models.Sum("quantity"))["quantity"] or 0
[docs] def set_sent(self):
"""
Mark the shipment as sent.
"""
if self.status == ShipmentStatus.SENT:
return
self.status = ShipmentStatus.SENT
self.save()
shipment_sent.send(sender=type(self), order=self.order, shipment=self)
[docs] def set_received(self, purchase_prices=None, created_by=None):
"""
Mark the shipment as received.
In case shipment is incoming, add stock adjustment for each
shipment product in this shipment.
:param purchase_prices: a dict mapping product ids to purchase prices
:type purchase_prices: dict[shuup.shop.models.Product, decimal.Decimal]
:param created_by: user who set this shipment received
:type created_by: settings.AUTH_USER_MODEL
"""
self.status = ShipmentStatus.RECEIVED
self.save()
if self.type == ShipmentType.IN:
for product_id, quantity in self.products.values_list("product_id", "quantity"):
purchase_price = purchase_prices.get(product_id, None) if purchase_prices else None
self.supplier.adjust_stock(
product_id=product_id, delta=quantity, purchase_price=purchase_price or 0, created_by=created_by
)
@python_2_unicode_compatible
class ShipmentProduct(ShuupModel):
shipment = models.ForeignKey(
Shipment, related_name="products", on_delete=models.PROTECT, verbose_name=_("shipment")
)
product = models.ForeignKey(
"Product", related_name="shipments", on_delete=models.CASCADE, verbose_name=_("product")
)
quantity = QuantityField(verbose_name=_("quantity"))
unit_volume = MeasurementField(
unit=get_shuup_volume_unit(), verbose_name=format_lazy(_("unit volume ({})"), get_shuup_volume_unit())
)
unit_weight = MeasurementField(
unit=settings.SHUUP_MASS_UNIT, verbose_name=format_lazy(_("unit weight ({})"), settings.SHUUP_MASS_UNIT)
)
class Meta:
verbose_name = _("sent product")
verbose_name_plural = _("sent products")
def __str__(self): # pragma: no cover
return "%(quantity)s of '%(product)s' in Shipment #%(shipment_pk)s" % {
"product": self.product,
"quantity": self.quantity,
"shipment_pk": self.shipment_id,
}
[docs] def cache_values(self):
prod = self.product
self.unit_volume = prod.width * prod.height * prod.depth
self.unit_weight = prod.gross_weight
ShipmentLogEntry = define_log_model(Shipment)
ShipmentProductLogEntry = define_log_model(ShipmentProduct)