diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f2f4be10..7202cbaa49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 List all changes after the last release here (newer on top). Each change on a separate bullet point line +### Added + +- Front: create shipment sent notify event +- Core: add shipment tracking url to shipment model +- Admin: add shipment action to mark a shipment as sent + ## [2.3.17] - 2021-02-23 ### Fixed diff --git a/shuup/admin/base.py b/shuup/admin/base.py index a336427f8c..730b6fd995 100644 --- a/shuup/admin/base.py +++ b/shuup/admin/base.py @@ -298,7 +298,7 @@ class Section(object): order = 0 @classmethod - def visible_for_object(cls, obj, request=None): + def visible_for_object(cls, obj, request): """ Returns whether this sections must be visible for the provided object (e.g. `order`). @@ -310,7 +310,7 @@ def visible_for_object(cls, obj, request=None): return False @classmethod - def get_context_data(cls, obj, request=None): + def get_context_data(cls, obj, request): """ Returns additional information to be used in the template. @@ -319,7 +319,6 @@ def get_context_data(cls, obj, request=None): e.g. `context[admin_order_section.identifier] = admin_order_section.get_context_data(self.object)` - :type object: e.g. shuup.core.models.Order :type request: HttpRequest :return additional context data diff --git a/shuup/admin/modules/orders/__init__.py b/shuup/admin/modules/orders/__init__.py index 52f20ebb35..92b8e8a649 100644 --- a/shuup/admin/modules/orders/__init__.py +++ b/shuup/admin/modules/orders/__init__.py @@ -37,6 +37,11 @@ def get_urls(self): "shuup.admin.modules.orders.views.ShipmentDeleteView", name="order.delete-shipment" ), + admin_url( + r"^shipments/(?P\d+)/set-sent/$", + "shuup.admin.modules.orders.views.ShipmentSetSentView", + name="order.set-shipment-sent" + ), admin_url( r"^orders/(?P\d+)/create-payment/$", "shuup.admin.modules.orders.views.OrderCreatePaymentView", diff --git a/shuup/admin/modules/orders/sections.py b/shuup/admin/modules/orders/sections.py index 697f572db5..cab31d0a85 100644 --- a/shuup/admin/modules/orders/sections.py +++ b/shuup/admin/modules/orders/sections.py @@ -79,23 +79,31 @@ def get_context_data(order, request=None): suppliers = Supplier.objects.filter(order_lines__order=order).distinct() create_permission = "order.create-shipment" delete_permission = "order.delete-shipment" - missing_permissions = get_missing_permissions(request.user, [create_permission, delete_permission]) + set_sent_permission = "order.set-shipment-sent" + missing_permissions = get_missing_permissions( + request.user, + [create_permission, delete_permission, set_sent_permission] + ) create_urls = {} + delete_urls = {} + set_sent_urls = {} + if create_permission not in missing_permissions: for supplier in suppliers: create_urls[supplier.pk] = reverse( "shuup_admin:order.create-shipment", kwargs={"pk": order.pk, "supplier_pk": supplier.pk}) - delete_urls = {} - if delete_permission not in missing_permissions: - for shipment_id in order.shipments.all_except_deleted().values_list("id", flat=True): - delete_urls[shipment_id] = reverse( - "shuup_admin:order.delete-shipment", kwargs={"pk": shipment_id}) + for shipment_id in order.shipments.all_except_deleted().values_list("id", flat=True): + if delete_permission not in missing_permissions: + delete_urls[shipment_id] = reverse("shuup_admin:order.delete-shipment", kwargs={"pk": shipment_id}) + if set_sent_permission not in missing_permissions: + set_sent_urls[shipment_id] = reverse("shuup_admin:order.set-shipment-sent", kwargs={"pk": shipment_id}) return { "suppliers": suppliers, "create_urls": create_urls, - "delete_urls": delete_urls + "delete_urls": delete_urls, + "set_sent_urls": set_sent_urls, } diff --git a/shuup/admin/modules/orders/views/__init__.py b/shuup/admin/modules/orders/views/__init__.py index fb66629a3a..b05f183e84 100644 --- a/shuup/admin/modules/orders/views/__init__.py +++ b/shuup/admin/modules/orders/views/__init__.py @@ -15,7 +15,9 @@ OrderCreatePaymentView, OrderDeletePaymentView, OrderSetPaidView ) from .refund import OrderCreateFullRefundView, OrderCreateRefundView -from .shipment import OrderCreateShipmentView, ShipmentDeleteView +from .shipment import ( + OrderCreateShipmentView, ShipmentDeleteView, ShipmentSetSentView +) from .status import OrderStatusEditView, OrderStatusListView __all__ = [ @@ -23,5 +25,5 @@ "OrderListView", "OrderCreatePaymentView", "OrderCreateFullRefundView", "OrderCreateRefundView", "OrderCreateShipmentView", "OrderSetPaidView", "OrderSetStatusView", "OrderStatusEditView", "OrderStatusListView", "ShipmentDeleteView", - "UpdateAdminCommentView", "OrderDeletePaymentView" + "UpdateAdminCommentView", "OrderDeletePaymentView", "ShipmentSetSentView" ] diff --git a/shuup/admin/modules/orders/views/shipment.py b/shuup/admin/modules/orders/views/shipment.py index 2f16a76a21..694becdd98 100644 --- a/shuup/admin/modules/orders/views/shipment.py +++ b/shuup/admin/modules/orders/views/shipment.py @@ -30,6 +30,7 @@ class ShipmentForm(ModifiableFormMixin, forms.Form): description = forms.CharField(required=False) tracking_code = forms.CharField(required=False) + tracking_url = forms.URLField(required=False, label=_("Tracking URL")) class OrderCreateShipmentView(ModifiableViewMixin, UpdateView): @@ -146,6 +147,7 @@ def form_valid(self, form): order=order, supplier_id=self._get_supplier_id(), tracking_code=form.cleaned_data.get("tracking_code"), + tracking_url=form.cleaned_data.get("tracking_url"), description=form.cleaned_data.get("description")) has_extension_errors = self.form_valid_hook(form, unsaved_shipment) @@ -191,3 +193,24 @@ def post(self, request, *args, **kwargs): shipment.soft_delete() messages.success(request, _("Shipment %s has been deleted.") % shipment.pk) return HttpResponseRedirect(self.get_success_url()) + + +class ShipmentSetSentView(DetailView): + model = Shipment + context_object_name = "shipment" + + def get_queryset(self): + shop_ids = Shop.objects.get_for_user(self.request.user).values_list("id", flat=True) + return Shipment.objects.filter(order__shop_id__in=shop_ids) + + def get_success_url(self): + return get_model_url(self.get_object().order) + + def get(self, request, *args, **kwargs): + return HttpResponseRedirect(self.get_success_url()) + + def post(self, request, *args, **kwargs): + shipment = self.get_object() + shipment.set_sent() + messages.success(request, _("Shipment has been marked as sent.")) + return HttpResponseRedirect(self.get_success_url()) diff --git a/shuup/admin/static_src/base/scss/shuup/shipments.scss b/shuup/admin/static_src/base/scss/shuup/shipments.scss index 418be08f53..97f0c5219c 100644 --- a/shuup/admin/static_src/base/scss/shuup/shipments.scss +++ b/shuup/admin/static_src/base/scss/shuup/shipments.scss @@ -1,6 +1,7 @@ #order-shipment-info-for-supplier { margin-left: 20px; margin-bottom: 40px; + padding-right: 20px; a.btn { margin-bottom: 20px; diff --git a/shuup/admin/templates/shuup/admin/orders/_detail_section.jinja b/shuup/admin/templates/shuup/admin/orders/_detail_section.jinja index e2e6f92f9f..865250ec04 100644 --- a/shuup/admin/templates/shuup/admin/orders/_detail_section.jinja +++ b/shuup/admin/templates/shuup/admin/orders/_detail_section.jinja @@ -97,7 +97,7 @@

{% trans %}Billing address{% endtrans %}

{% for line in order.billing_address or [] %} -
{{ line }}
+
{{ line }}
{% else %}

{% trans %}No billing address defined.{% endtrans %}

{% endfor %} @@ -107,7 +107,7 @@

{% trans %}Shipping address{% endtrans %}

{% for line in order.shipping_address or [] %} -
{{ line }}
+
{{ line }}
{% else %}

{% trans %}No shipping address defined.{% endtrans %}

{% endfor %} diff --git a/shuup/admin/templates/shuup/admin/orders/_order_shipments.jinja b/shuup/admin/templates/shuup/admin/orders/_order_shipments.jinja index cffecd6024..b952e38ed4 100644 --- a/shuup/admin/templates/shuup/admin/orders/_order_shipments.jinja +++ b/shuup/admin/templates/shuup/admin/orders/_order_shipments.jinja @@ -6,8 +6,8 @@ {%- if not loop.last %}, {% endif -%} {%- endfor -%} {% endmacro %} - -{% macro render_supplier_info(supplier, create_urls, delete_urls) %} + +{% macro render_supplier_info(supplier, create_urls, delete_urls, set_sent_urls) %} {% set product_summary = order.get_product_summary(supplier) %} {% set ordered_total = product_summary.values()|sum("ordered")|number %} {% set shipped_total = product_summary.values()|sum("shipped")|number %} @@ -59,22 +59,35 @@ {% trans %}Products{% endtrans %} {% trans %}Tracking Code{% endtrans %} + {% trans %}Status{% endtrans %} {% trans %}Description{% endtrans %} {% trans %}Created{% endtrans %} - {% trans %}Delete{% endtrans %} + {% trans %}Actions{% endtrans %} {% for shipment in shipments %} {{ render_products_cell(shipment) }} - {{ shipment.tracking_code }} + + {%- if shipment.tracking_url %} + + {% endif -%} + {{- shipment.tracking_code -}} + {%- if shipment.tracking_url %}{% endif -%} + + {{ shipment.status }} {{ shipment.description }} - {{ shipment.created_on|datetime }} + {{ shipment.created_on|datetime(format="short") }} {% if delete_urls.get(shipment.id) and not shipment.is_deleted() %} - - + + {{ _("Delete") }} + + {% endif %} + {% if set_sent_urls.get(shipment.id) and not shipment.is_sent() %} + + {{ _("Mark as sent") }} {% endif %} @@ -87,14 +100,13 @@
{% endmacro %} -
{% set create_urls = shipments_data.create_urls %} {% set delete_urls = shipments_data.delete_urls %} +{% set set_sent_urls = shipments_data.set_sent_urls %} {% for supplier in shipments_data.suppliers %} - {{ render_supplier_info(supplier, create_urls, delete_urls) }} + {{ render_supplier_info(supplier, create_urls, delete_urls, set_sent_urls) }} {%- if not loop.last %}
{% endif -%} {% endfor %} -
{% block extra_js %} {% endblock %} diff --git a/shuup/core/migrations/0084_tracking_url.py b/shuup/core/migrations/0084_tracking_url.py new file mode 100644 index 0000000000..a1f85d8283 --- /dev/null +++ b/shuup/core/migrations/0084_tracking_url.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.15 on 2021-02-26 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shuup', '0083_make_attribute_name_256_chars'), + ] + + operations = [ + migrations.AddField( + model_name='shipment', + name='tracking_url', + field=models.URLField(blank=True, verbose_name='tracking url'), + ), + ] diff --git a/shuup/core/models/_orders.py b/shuup/core/models/_orders.py index 6984c3d19a..c04c66f818 100644 --- a/shuup/core/models/_orders.py +++ b/shuup/core/models/_orders.py @@ -1201,6 +1201,9 @@ def get_shipping_method_display(self): def get_tracking_codes(self): return [shipment.tracking_code for shipment in self.shipments.all_except_deleted() if shipment.tracking_code] + def get_sent_shipments(self): + return self.shipments.all_except_deleted().sent() + def can_edit(self): return ( settings.SHUUP_ALLOW_EDITING_ORDER diff --git a/shuup/core/models/_shipments.py b/shuup/core/models/_shipments.py index c154d80b3f..3bdfe2e219 100644 --- a/shuup/core/models/_shipments.py +++ b/shuup/core/models/_shipments.py @@ -19,7 +19,7 @@ InternalIdentifierField, MeasurementField, QuantityField ) from shuup.core.models import ShuupModel -from shuup.core.signals import shipment_deleted +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 @@ -34,11 +34,11 @@ class ShipmentStatus(Enum): DELETED = 20 class Labels: - NOT_SENT = _("not sent") - SENT = _("sent") - RECEIVED = _("received") - ERROR = _("error") - DELETED = _("deleted") + NOT_SENT = _("Not sent") + SENT = _("Sent") + RECEIVED = _("Received") + ERROR = _("Error") + DELETED = _("Deleted") class ShipmentType(Enum): @@ -50,11 +50,13 @@ class Labels: IN = _("incoming") -class ShipmentManager(models.Manager): - +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( @@ -66,6 +68,7 @@ class Shipment(ShuupModel): 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(), @@ -77,9 +80,8 @@ class Shipment(ShuupModel): ) identifier = InternalIdentifierField(unique=True) type = EnumIntegerField(ShipmentType, default=ShipmentType.OUT, verbose_name=_("type")) - # TODO: documents = models.ManyToManyField(FilerFile) - objects = ShipmentManager() + objects = ShipmentQueryset.as_manager() class Meta: verbose_name = _('shipment') @@ -122,6 +124,9 @@ def soft_delete(self, user=None): def is_deleted(self): return bool(self.status == ShipmentStatus.DELETED) + def is_sent(self): + return bool(self.status == ShipmentStatus.SENT) + def cache_values(self): """ (Re)cache `.volume` and `.weight` for this Shipment from within the ShipmentProducts. @@ -138,6 +143,17 @@ def cache_values(self): def total_products(self): return (self.products.aggregate(quantity=models.Sum("quantity"))["quantity"] or 0) + 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) + def set_received(self, purchase_prices=None, created_by=None): """ Mark the shipment as received. diff --git a/shuup/core/signals.py b/shuup/core/signals.py index 791170f0a2..224c8daedc 100644 --- a/shuup/core/signals.py +++ b/shuup/core/signals.py @@ -12,6 +12,7 @@ get_orderability_errors = Signal(providing_args=["shop_product", "customer", "supplier", "quantity"], use_caching=True) shipment_created = Signal(providing_args=["order", "shipment"], use_caching=True) shipment_created_and_processed = Signal(providing_args=["order", "shipment"], use_caching=True) +shipment_sent = Signal(providing_args=["order", "shipment"], use_caching=True) refund_created = Signal(providing_args=["order", "refund_lines"], use_caching=True) category_deleted = Signal(providing_args=["category"], use_caching=True) shipment_deleted = Signal(providing_args=["shipment"], use_caching=True) diff --git a/shuup/front/__init__.py b/shuup/front/__init__.py index 6cbe91d541..81bc4a697f 100644 --- a/shuup/front/__init__.py +++ b/shuup/front/__init__.py @@ -33,6 +33,7 @@ class ShuupFrontAppConfig(AppConfig): "shuup.front.notify_events:OrderStatusChanged", "shuup.front.notify_events:ShipmentCreated", "shuup.front.notify_events:ShipmentDeleted", + "shuup.front.notify_events:ShipmentSent", "shuup.front.notify_events:PaymentCreated", "shuup.front.notify_events:RefundCreated", ], diff --git a/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/macros/order_detail.jinja b/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/macros/order_detail.jinja index a7aa062fe9..9cf08b0259 100644 --- a/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/macros/order_detail.jinja +++ b/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/macros/order_detail.jinja @@ -15,17 +15,6 @@ {{ info_row(_("Tax Number"), order.tax_number) }} {{ price_row(_("Total Price"), order.taxful_total_price) }} {{ price_row(_("Total Price (taxless)"), order.taxless_total_price) }} - {% set tracking_codes = order.get_tracking_codes() %} - {% if tracking_codes %} - {{ info_row(_("Tracking codes"), render_objects(tracking_codes)) }} - {% endif %} - - {% endcall %} -{% endmacro %} - -{% macro status() %} - {% call content_block(_("Status")) %} - {{ info_row(_("Order Status"), order.get_status_display()) }} {{ info_row(_("Payment Status"), order.get_payment_status_display()) }} {{ info_row(_("Shipping Status"), order.get_shipping_status_display()) }} @@ -41,6 +30,42 @@ {% endcall %} {% endmacro %} +{% macro render_products_cell(shipment) %} + {%- for shipment_product in shipment.products.all() -%} + {% set product = shipment_product.product %} + {% set unit = product.sales_unit %} + {{- product.name -}} ({{- unit.round(shipment_product.quantity) -}}) + {%- if not loop.last %}, {% endif -%} + {%- endfor -%} +{% endmacro %} + +{% macro sent_shipments(shipments) %} + {% call content_block(_("Shipments")) %} +
+ + + + + + + + {% for shipment in shipments %} + + + + + + {% endfor %} +
{{ _("Identifier") }}{{ _("Products") }}{{ _("Tracking code") }}
{{ shipment.identifier or '' }}{{ render_products_cell(shipment) }} + {%- if shipment.tracking_url %} + + {% endif -%} + {{- shipment.tracking_code -}} + {%- if shipment.tracking_url %}{% endif -%} +
+ {% endcall %} +{% endmacro %} + {% macro billing_address() %} {% call content_block(_("Billing Address")) %} {% for line in order.billing_address %} diff --git a/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/order_detail.jinja b/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/order_detail.jinja index 7a4d62be67..98c3369205 100644 --- a/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/order_detail.jinja +++ b/shuup/front/apps/personal_order_history/templates/shuup/personal_order_history/order_detail.jinja @@ -1,6 +1,6 @@ {% extends "shuup/front/dashboard/dashboard.jinja" %} {% from "shuup/personal_order_history/macros/buttons.jinja" import render_action_buttons with context %} -{% from "shuup/personal_order_history/macros/order_detail.jinja" import basic_info, shipping_address, billing_address, status, order_contents with context %} +{% from "shuup/personal_order_history/macros/order_detail.jinja" import basic_info, shipping_address, billing_address, status, order_contents, sent_shipments with context %} {% block title %}{% trans identifier = order.identifier %}Order {{ identifier }}{% endtrans %}{% endblock %} {% set main_title = _("Details of order %(identifier)s", identifier=order.identifier) %} @@ -18,28 +18,31 @@ {% block dashboard_content %}
-
+
{{ basic_info() }}
{% if order.shipping_address_id and order.billing_address_id %} -
+
{{ shipping_address() }}
-
+
{{ billing_address() }}
{% elif order.shipping_address_id %} -
+
{{ shipping_address() }}
{% elif order.billing_address_id %} -
+
{{ billing_address() }}
{% endif %} -
- {{ status() }} -
+ {% set _sent_shipments = order.get_sent_shipments() %} + {% if _sent_shipments.exists() %} +
+ {{ sent_shipments(_sent_shipments) }} +
+ {% endif %}
{{ order_contents() }}
diff --git a/shuup/front/notify_events.py b/shuup/front/notify_events.py index 542a2d90eb..bd07dc77a9 100644 --- a/shuup/front/notify_events.py +++ b/shuup/front/notify_events.py @@ -12,11 +12,13 @@ from shuup.core.order_creator.signals import order_creator_finished from shuup.core.signals import ( order_status_changed, payment_created, refund_created, - shipment_created_and_processed, shipment_deleted + shipment_created_and_processed, shipment_deleted, shipment_sent ) from shuup.notify.base import Event, Variable from shuup.notify.models import Script -from shuup.notify.typology import Email, Enum, Language, Model, Phone +from shuup.notify.typology import ( + Email, Enum, Language, Model, Phone, Text, URL +) # Common attributes that can be used with orders. ORDER_ATTRIBUTES = ( @@ -68,6 +70,13 @@ class ShipmentCreated(Event): shipment = Variable(_("Shipment"), type=Model("shuup.Shipment")) shipping_status = Variable(_("Order Shipping Status"), type=Enum(ShippingStatus)) shipment_status = Variable(_("Shipment Status"), type=Enum(ShipmentStatus)) + shipment_tracking_code = Variable(_("Shipment Tracking Code"), type=Text, required=False) + shipment_tracking_url = Variable(_("Shipment Tracking URL"), type=URL, required=False) + + +class ShipmentSent(ShipmentCreated): + identifier = "shipment_sent" + name = _("Shipment Sent") class ShipmentDeleted(Event): @@ -137,7 +146,24 @@ def send_shipment_created_notification(order, shipment, **kwargs): language=order.language, shipment=shipment, shipping_status=order.shipping_status, - shipment_status=shipment.status + shipment_status=shipment.status, + shipment_tracking_code=shipment.tracking_code, + shipment_tracking_url=shipment.tracking_url, + ).run(shop=order.shop) + + +@receiver(shipment_sent) +def send_shipment_sent_notification(order, shipment, **kwargs): + ShipmentSent( + order=order, + customer_email=order.email, + customer_phone=order.phone, + language=order.language, + shipment=shipment, + shipping_status=order.shipping_status, + shipment_status=shipment.status, + shipment_tracking_code=shipment.tracking_code, + shipment_tracking_url=shipment.tracking_url, ).run(shop=order.shop) diff --git a/shuup/order_printouts/templates/shuup/order_printouts/admin/macros.jinja b/shuup/order_printouts/templates/shuup/order_printouts/admin/macros.jinja index e58100262c..d582175878 100644 --- a/shuup/order_printouts/templates/shuup/order_printouts/admin/macros.jinja +++ b/shuup/order_printouts/templates/shuup/order_printouts/admin/macros.jinja @@ -62,7 +62,13 @@ {% if shipment.tracking_code %} {% trans %}Tracking code{% endtrans %} - {{ shipment.tracking_code }} + + {%- if shipment.tracking_url %} + + {% endif -%} + {{- shipment.tracking_code -}} + {%- if shipment.tracking_url %}{% endif -%} + {% endif %} {% if extra_fields %}