Add specific shipping choice when 6 bags are in the cart
This commit is contained in:
parent
b9b5eb5254
commit
44e73ca790
@ -2,103 +2,105 @@ from django.conf import settings
|
||||
|
||||
|
||||
class DiscountValueType:
|
||||
FIXED = "fixed"
|
||||
PERCENTAGE = "percentage"
|
||||
FIXED = 'fixed'
|
||||
PERCENTAGE = 'percentage'
|
||||
|
||||
CHOICES = [
|
||||
(FIXED, settings.DEFAULT_CURRENCY),
|
||||
(PERCENTAGE, "%"),
|
||||
(PERCENTAGE, '%'),
|
||||
]
|
||||
|
||||
|
||||
class VoucherType:
|
||||
SHIPPING = "shipping"
|
||||
ENTIRE_ORDER = "entire_order"
|
||||
SPECIFIC_PRODUCT = "specific_product"
|
||||
SHIPPING = 'shipping'
|
||||
ENTIRE_ORDER = 'entire_order'
|
||||
SPECIFIC_PRODUCT = 'specific_product'
|
||||
|
||||
CHOICES = [
|
||||
(ENTIRE_ORDER, "Entire order"),
|
||||
(SHIPPING, "Shipping"),
|
||||
(SPECIFIC_PRODUCT, "Specific products, collections and categories"),
|
||||
(ENTIRE_ORDER, 'Entire order'),
|
||||
(SHIPPING, 'Shipping'),
|
||||
(SPECIFIC_PRODUCT, 'Specific products, collections and categories'),
|
||||
]
|
||||
|
||||
|
||||
class OrderStatus:
|
||||
DRAFT = "draft" # fully editable, not finalized order created by staff users
|
||||
UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled
|
||||
DRAFT = 'draft' # fully editable, not finalized order created by staff users
|
||||
UNFULFILLED = 'unfulfilled' # order with no items marked as fulfilled
|
||||
PARTIALLY_FULFILLED = (
|
||||
"partially_fulfilled" # order with some items marked as fulfilled
|
||||
'partially_fulfilled' # order with some items marked as fulfilled
|
||||
)
|
||||
FULFILLED = "fulfilled" # order with all items marked as fulfilled
|
||||
FULFILLED = 'fulfilled' # order with all items marked as fulfilled
|
||||
|
||||
PARTIALLY_RETURNED = (
|
||||
"partially_returned" # order with some items marked as returned
|
||||
'partially_returned' # order with some items marked as returned
|
||||
)
|
||||
RETURNED = "returned" # order with all items marked as returned
|
||||
CANCELED = "canceled" # permanently canceled order
|
||||
RETURNED = 'returned' # order with all items marked as returned
|
||||
CANCELED = 'canceled' # permanently canceled order
|
||||
|
||||
CHOICES = [
|
||||
(DRAFT, "Draft"),
|
||||
(UNFULFILLED, "Unfulfilled"),
|
||||
(PARTIALLY_FULFILLED, "Partially fulfilled"),
|
||||
(PARTIALLY_RETURNED, "Partially returned"),
|
||||
(RETURNED, "Returned"),
|
||||
(FULFILLED, "Fulfilled"),
|
||||
(CANCELED, "Canceled"),
|
||||
(DRAFT, 'Draft'),
|
||||
(UNFULFILLED, 'Unfulfilled'),
|
||||
(PARTIALLY_FULFILLED, 'Partially fulfilled'),
|
||||
(PARTIALLY_RETURNED, 'Partially returned'),
|
||||
(RETURNED, 'Returned'),
|
||||
(FULFILLED, 'Fulfilled'),
|
||||
(CANCELED, 'Canceled'),
|
||||
]
|
||||
|
||||
|
||||
class TransactionStatus:
|
||||
CREATED = "CREATED" # The order was created with the specified context.
|
||||
SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
|
||||
APPROVED = "APPROVED" # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
|
||||
VOIDED = "VOIDED" # All purchase units in the order are voided.
|
||||
COMPLETED = "COMPLETED" # The payment was authorized or the authorized payment was captured for the order.
|
||||
PAYER_ACTION_REQUIRED = "PAYER_ACTION_REQUIRED" # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order.
|
||||
CREATED = 'CREATED' # The order was created with the specified context.
|
||||
SAVED = 'SAVED' # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
|
||||
APPROVED = 'APPROVED' # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
|
||||
VOIDED = 'VOIDED' # All purchase units in the order are voided.
|
||||
COMPLETED = 'COMPLETED' # The payment was authorized or the authorized payment was captured for the order.
|
||||
PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED' # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the 'rel':'payer-action' HATEOAS link returned as part of the response prior to authorizing or capturing the order.
|
||||
|
||||
CHOICES = [
|
||||
(CREATED, "Created"),
|
||||
(SAVED, "Saved"),
|
||||
(APPROVED, "Approved"),
|
||||
(VOIDED, "Voided"),
|
||||
(COMPLETED, "Completed"),
|
||||
(PAYER_ACTION_REQUIRED, "Payer action required")
|
||||
(CREATED, 'Created'),
|
||||
(SAVED, 'Saved'),
|
||||
(APPROVED, 'Approved'),
|
||||
(VOIDED, 'Voided'),
|
||||
(COMPLETED, 'Completed'),
|
||||
(PAYER_ACTION_REQUIRED, 'Payer action required')
|
||||
]
|
||||
|
||||
|
||||
class ShippingMethodType:
|
||||
PRICE_BASED = "price"
|
||||
WEIGHT_BASED = "weight"
|
||||
PRICE_BASED = 'price'
|
||||
WEIGHT_BASED = 'weight'
|
||||
|
||||
CHOICES = [
|
||||
(PRICE_BASED, "Price based shipping"),
|
||||
(WEIGHT_BASED, "Weight based shipping"),
|
||||
(PRICE_BASED, 'Price based shipping'),
|
||||
(WEIGHT_BASED, 'Weight based shipping'),
|
||||
]
|
||||
|
||||
|
||||
class ShippingService:
|
||||
FIRST_CLASS = "FIRST CLASS"
|
||||
PRIORITY = "PRIORITY"
|
||||
PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL"
|
||||
FIRST_CLASS = 'FIRST CLASS'
|
||||
PRIORITY = 'PRIORITY'
|
||||
PRIORITY_COMMERCIAL = 'PRIORITY COMMERCIAL'
|
||||
|
||||
CHOICES = [
|
||||
(FIRST_CLASS, "First Class"),
|
||||
(PRIORITY, "Priority"),
|
||||
(PRIORITY_COMMERCIAL, "Priority Commercial")
|
||||
(FIRST_CLASS, 'First Class'),
|
||||
(PRIORITY, 'Priority'),
|
||||
(PRIORITY_COMMERCIAL, 'Priority Commercial')
|
||||
]
|
||||
|
||||
|
||||
class ShippingContainer:
|
||||
LG_FLAT_RATE_BOX = "LG FLAT RATE BOX"
|
||||
REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA"
|
||||
REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB"
|
||||
VARIABLE = "VARIABLE"
|
||||
LG_FLAT_RATE_BOX = 'LG FLAT RATE BOX'
|
||||
MD_FLAT_RATE_BOX = 'MD FLAT RATE BOX'
|
||||
REGIONAL_RATE_BOX_A = 'REGIONALRATEBOXA'
|
||||
REGIONAL_RATE_BOX_B = 'REGIONALRATEBOXB'
|
||||
VARIABLE = 'VARIABLE'
|
||||
|
||||
CHOICES = [
|
||||
(LG_FLAT_RATE_BOX, "Flate Rate Box - Large"),
|
||||
(REGIONAL_RATE_BOX_A, "Regional Rate Box A"),
|
||||
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
|
||||
(VARIABLE, "Variable")
|
||||
(LG_FLAT_RATE_BOX, 'Flate Rate Box - Large'),
|
||||
(MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'),
|
||||
(REGIONAL_RATE_BOX_A, 'Regional Rate Box A'),
|
||||
(REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
|
||||
(VARIABLE, 'Variable')
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"name": "Save 10%: Valid",
|
||||
"code": "MAY2022",
|
||||
"valid_from": "2022-05-01T06:00:00Z",
|
||||
"valid_to": "2022-05-31T06:00:00Z",
|
||||
"valid_to": "2054-05-31T06:00:00Z",
|
||||
"discount_value_type": "percentage",
|
||||
"discount_value": "10.00",
|
||||
"products": [],
|
||||
|
||||
@ -43,7 +43,7 @@ class AddressTests(StaticLiveServerTestCase):
|
||||
state_select.select_by_value('UT')
|
||||
postal_code_input = self.browser.find_element_by_name('postal_code')
|
||||
postal_code_input.send_keys('37461')
|
||||
self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click()
|
||||
self.browser.find_element_by_xpath('//input[@value="Continue"]').click()
|
||||
# try:
|
||||
# WebDriverWait(self.browser, 4).until(
|
||||
# EC.presence_of_element_located((By.CLASS_NAME, 'errorlist'))
|
||||
@ -77,7 +77,7 @@ class AddressTests(StaticLiveServerTestCase):
|
||||
state_select.select_by_value('AK')
|
||||
postal_code_input = self.browser.find_element_by_name('postal_code')
|
||||
postal_code_input.send_keys('99801')
|
||||
self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click()
|
||||
self.browser.find_element_by_xpath('//input[@value="Continue"]').click()
|
||||
# try:
|
||||
# WebDriverWait(self.browser, 4).until(
|
||||
# EC.presence_of_element_located((By.CLASS_NAME, 'errorlist'))
|
||||
|
||||
@ -85,7 +85,7 @@ class CouponTests(StaticLiveServerTestCase):
|
||||
postal_code_input = self.browser.find_element_by_name('postal_code')
|
||||
postal_code_input.send_keys('84321')
|
||||
self.browser.find_element_by_xpath(
|
||||
'//input[@value="Continue to Payment"]'
|
||||
'//input[@value="Continue"]'
|
||||
).click()
|
||||
|
||||
self.assertEqual(
|
||||
@ -140,7 +140,7 @@ class CouponTests(StaticLiveServerTestCase):
|
||||
postal_code_input = self.browser.find_element_by_name('postal_code')
|
||||
postal_code_input.send_keys('84321')
|
||||
self.browser.find_element_by_xpath(
|
||||
'//input[@value="Continue to Payment"]'
|
||||
'//input[@value="Continue"]'
|
||||
).click()
|
||||
|
||||
self.assertEqual(
|
||||
|
||||
@ -30,6 +30,7 @@ class Cart:
|
||||
self.request = request
|
||||
self.session = request.session
|
||||
self.coupon_code = self.session.get('coupon_code')
|
||||
self.container = self.session.get('shipping_container')
|
||||
cart = self.session.get(settings.CART_SESSION_ID)
|
||||
if not cart:
|
||||
cart = self.session[settings.CART_SESSION_ID] = {}
|
||||
@ -109,20 +110,26 @@ class Cart:
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_shipping_box(self):
|
||||
def get_shipping_box(self, container=None):
|
||||
if container:
|
||||
return container
|
||||
|
||||
if self.container:
|
||||
return self.container
|
||||
|
||||
if len(self) > 6 and len(self) <= 10:
|
||||
return ShippingContainer.LG_FLAT_RATE_BOX
|
||||
elif len(self) > 2 and len(self) <= 6:
|
||||
elif len(self) > 3 and len(self) <= 6:
|
||||
return ShippingContainer.REGIONAL_RATE_BOX_B
|
||||
elif len(self) <= 2:
|
||||
elif len(self) <= 3:
|
||||
return ShippingContainer.REGIONAL_RATE_BOX_A
|
||||
else:
|
||||
return ShippingContainer.VARIABLE
|
||||
|
||||
def get_shipping_cost(self):
|
||||
def get_shipping_cost(self, container=None):
|
||||
if len(self) > 0 and self.session.get("shipping_address"):
|
||||
try:
|
||||
usps_rate_request = self.build_usps_rate_request()
|
||||
usps_rate_request = self.build_usps_rate_request(container)
|
||||
except TypeError as e:
|
||||
return Decimal('0.00')
|
||||
usps = USPSApi(settings.USPS_USER_ID, test=True)
|
||||
@ -135,7 +142,7 @@ class Cart:
|
||||
)
|
||||
|
||||
logger.info(validation.result)
|
||||
if not 'Error' in validation.result['RateV4Response']['Package']:
|
||||
if 'Error' not in validation.result['RateV4Response']['Package']:
|
||||
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
|
||||
else:
|
||||
logger.error("USPS Rate error")
|
||||
@ -152,7 +159,7 @@ class Cart:
|
||||
pass
|
||||
self.session.modified = True
|
||||
|
||||
def build_usps_rate_request(self):
|
||||
def build_usps_rate_request(self, container=None):
|
||||
return \
|
||||
{
|
||||
'service': ShippingService.PRIORITY_COMMERCIAL,
|
||||
@ -160,7 +167,7 @@ class Cart:
|
||||
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
|
||||
'pounds': '0',
|
||||
'ounces': f'{self.get_total_weight()}',
|
||||
'container': f'{self.get_shipping_box()}',
|
||||
'container': f'{self.get_shipping_box(container)}',
|
||||
'width': '',
|
||||
'length': '',
|
||||
'height': '',
|
||||
@ -168,7 +175,7 @@ class Cart:
|
||||
'machinable': 'TRUE'
|
||||
}
|
||||
|
||||
def build_order_params(self):
|
||||
def build_order_params(self, container=None):
|
||||
return \
|
||||
{
|
||||
'items': self,
|
||||
@ -177,12 +184,14 @@ class Cart:
|
||||
'discount': f'{self.get_discount()}',
|
||||
'shipping_price': f'{self.get_shipping_cost()}',
|
||||
'tax_total': '0',
|
||||
'shipping_method': 'US POSTAL SERVICE',
|
||||
'shipping_method': 'US POSTAL SERVICE ' + (
|
||||
container if container else ''
|
||||
),
|
||||
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
|
||||
}
|
||||
|
||||
def create_order(self):
|
||||
params = self.build_order_params()
|
||||
def create_order(self, container=None):
|
||||
params = self.build_order_params(container)
|
||||
logger.info(f'\nParams: {params}\n')
|
||||
if settings.DEBUG:
|
||||
response = CreateOrder().create_order(params, debug=True)
|
||||
|
||||
@ -11,7 +11,7 @@ from usps import USPSApi, Address
|
||||
from captcha.fields import CaptchaField
|
||||
|
||||
from core.models import Order
|
||||
from core import CoffeeGrind
|
||||
from core import CoffeeGrind, ShippingContainer
|
||||
|
||||
from .tasks import contact_form_email
|
||||
|
||||
@ -106,6 +106,18 @@ class AddressForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class CheckoutShippingForm(forms.Form):
|
||||
SHIPPING_CHOICES = [
|
||||
(ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'),
|
||||
(ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'),
|
||||
]
|
||||
|
||||
shipping_method = forms.ChoiceField(
|
||||
widget=forms.RadioSelect,
|
||||
choices=SHIPPING_CHOICES
|
||||
)
|
||||
|
||||
|
||||
class OrderCreateForm(forms.ModelForm):
|
||||
email = forms.CharField(widget=forms.HiddenInput())
|
||||
first_name = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
{% csrf_token %}
|
||||
{{form.as_p}}
|
||||
<p>
|
||||
<input type="submit" value="Continue to Payment">
|
||||
<input type="submit" value="Continue">
|
||||
</p>
|
||||
</form>
|
||||
<p>We validate addresses with USPS, if you are having issues please contact us at <a href="mailto:support@ptcoffee.com">support@ptcoffee.com</a> or use the contact information found on our <a href="{% url 'storefront:contact' %}">contact</a> page.</p>
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head_title %}Checkout | {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<header>
|
||||
<h1>Checkout</h1>
|
||||
</header>
|
||||
<section>
|
||||
<h3>Shipping Method</h3>
|
||||
<form action="{% url 'storefront:checkout-shipping' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
<fieldset>
|
||||
<legend>{{ form.shipping_method.label }}</legend>
|
||||
{% for radio in form.shipping_method %}
|
||||
<p>
|
||||
<label for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
{% if 'Flate Rate Box - Medium' in radio.choice_label %}
|
||||
<strong>${{ MD_FLAT_RATE_BOX }}</strong>
|
||||
{% elif 'Regional Rate Box B' in radio.choice_label %}
|
||||
<strong>${{ REGIONAL_RATE_BOX_B }}</strong>
|
||||
{% endif %}
|
||||
</label>
|
||||
{{ radio.tag }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<br>
|
||||
<p>
|
||||
<input type="submit" value="Continue to Payment">
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -48,6 +48,11 @@ urlpatterns = [
|
||||
views.CheckoutAddressView.as_view(),
|
||||
name='checkout-address',
|
||||
),
|
||||
path(
|
||||
'checkout/shipping/',
|
||||
views.CheckoutShippingView.as_view(),
|
||||
name='checkout-shipping',
|
||||
),
|
||||
path('checkout/', views.OrderCreateView.as_view(), name='order-create'),
|
||||
path('done/', views.PaymentDoneView.as_view(), name='payment-done'),
|
||||
path(
|
||||
|
||||
@ -32,11 +32,11 @@ from accounts.forms import (
|
||||
)
|
||||
from core.models import Product, Order, Transaction, OrderLine, Coupon
|
||||
from core.forms import ShippingMethodForm
|
||||
from core import OrderStatus
|
||||
from core import OrderStatus, ShippingContainer
|
||||
|
||||
from .forms import (
|
||||
AddToCartForm, UpdateCartItemForm, OrderCreateForm,
|
||||
AddressForm, CouponApplyForm, ContactForm
|
||||
AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm,
|
||||
)
|
||||
from .cart import Cart
|
||||
from .payments import CaptureOrder
|
||||
@ -163,7 +163,7 @@ class ProductDetailView(FormMixin, DetailView):
|
||||
class CheckoutAddressView(FormView):
|
||||
template_name = 'storefront/checkout_address.html'
|
||||
form_class = AddressForm
|
||||
success_url = reverse_lazy('storefront:order-create')
|
||||
success_url = reverse_lazy('storefront:checkout-shipping')
|
||||
|
||||
def get_initial(self):
|
||||
user = self.request.user
|
||||
@ -212,6 +212,47 @@ class CheckoutAddressView(FormView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CheckoutShippingView(FormView):
|
||||
template_name = 'storefront/checkout_shipping_form.html'
|
||||
form_class = CheckoutShippingForm
|
||||
success_url = reverse_lazy('storefront:order-create')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cart = Cart(request)
|
||||
if len(cart) != 6:
|
||||
if 'shipping_container' in self.request.session:
|
||||
del self.request.session['shipping_container']
|
||||
return HttpResponseRedirect(
|
||||
reverse('storefront:order-create')
|
||||
)
|
||||
|
||||
if not self.request.session.get("shipping_address"):
|
||||
messages.warning(request, 'Please add a shipping address.')
|
||||
return HttpResponseRedirect(
|
||||
reverse('storefront:checkout-address')
|
||||
)
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
cart = Cart(self.request)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost(
|
||||
ShippingContainer.MD_FLAT_RATE_BOX
|
||||
)
|
||||
context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost(
|
||||
ShippingContainer.REGIONAL_RATE_BOX_B
|
||||
)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
cleaned_data = form.cleaned_data
|
||||
self.request.session['shipping_container'] = cleaned_data.get(
|
||||
'shipping_method'
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class OrderCreateView(CreateView):
|
||||
model = Order
|
||||
template_name = 'storefront/order_form.html'
|
||||
@ -219,6 +260,10 @@ class OrderCreateView(CreateView):
|
||||
success_url = reverse_lazy('storefront:payment-done')
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cart = Cart(request)
|
||||
if len(cart) != 6 and 'shipping_container' in self.request.session:
|
||||
del self.request.session['shipping_container']
|
||||
|
||||
if not self.request.session.get("shipping_address"):
|
||||
messages.warning(request, 'Please add a shipping address.')
|
||||
return HttpResponseRedirect(
|
||||
@ -271,13 +316,14 @@ class OrderCreateView(CreateView):
|
||||
def form_valid(self, form):
|
||||
cart = Cart(self.request)
|
||||
shipping_address = self.request.session.get('shipping_address')
|
||||
shipping_container = self.request.session.get('shipping_container')
|
||||
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
|
||||
form.instance.status = OrderStatus.DRAFT
|
||||
self.object = form.save()
|
||||
bulk_list = cart.build_bulk_list(self.object)
|
||||
objs = OrderLine.objects.bulk_create(bulk_list)
|
||||
|
||||
response = cart.create_order()
|
||||
response = cart.create_order(shipping_container)
|
||||
data = response.result.__dict__['_dict']
|
||||
|
||||
self.request.session['order_id'] = self.object.pk
|
||||
@ -339,8 +385,8 @@ class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
|
||||
permission_denied_message = 'Not authorized.'
|
||||
raise_exception = True
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['order_list'] = Order.objects.without_drafts().filter(
|
||||
customer=self.object
|
||||
).prefetch_related('lines')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user