-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to create order from dashboard #1971
Conversation
{% if order.is_draft %} | ||
<li> | ||
<a href="#base-modal" data-href="{% url "dashboard:draft-order-delete" order_pk=order.pk %}" class="modal-trigger-custom"> | ||
{% trans "Remove order" context "Order detail action" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would "Delete draft" be more descriptive?
{% endblock %} | ||
|
||
{% block title %} | ||
{% trans "Remove order" context "Modal remove order title" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. Maybe "Delete draft order".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, it will be better - I will change it. But I will leave Remove
word, as we use it more widely in dashboard.
Codecov Report
@@ Coverage Diff @@
## master #1971 +/- ##
==========================================
+ Coverage 85.01% 85.08% +0.06%
==========================================
Files 166 167 +1
Lines 6974 7420 +446
Branches 697 772 +75
==========================================
+ Hits 5929 6313 +384
- Misses 853 884 +31
- Partials 192 223 +31
Continue to review full report at Codecov.
|
By any chance, would you have some time to put a task list/ todo list on your WiP PR? That would really help to know what bugs, what missing things and issues are known. Since I also worked on my own implementation of this feature, it would be quite nice to work on yours! Discount: fixed or percentageWhat about allowing the staff user to chose between percentage or fixed discount? It would really be useful and needed in real use for the staff member. I do acknowledge it would require to put a flag for that, probably in the We could calculate the fixed value in Optional shipping method fieldWhat about IRL orders? If we don't have to ship an order that have products that require shipping? Maybe we should put the field as optional. Removing selected customerWe can't switch between a selected customer to a guest (can't remove the selected value). Optional shipping addressIf we set (or create) a billing address, for example on a guest, we should default the shipping address to the billing address to avoid the staff user having to enter twice the same address. Can't set order as paid (capture amount)We can't mark an order as paid or capture amount on order (especially if the customer is a guest, as the users can't pay the order from their account). |
saleor/order/__init__.py
Outdated
UNFULFILLED = 'unfulfilled' | ||
PARTIALLY_FULFILLED = 'partially fulfilled' | ||
FULFILLED = 'fulfilled' | ||
CANCELED = 'canceled' | ||
|
||
CHOICES = [ | ||
(DRAFT, pgettext_lazy('order status', 'Draft')), | ||
(UNFULFILLED, pgettext_lazy('order status', 'Unfulfilled')), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we profit from the fact that we are editing the order process, to make contexts more descriptive? To explain to the translators what trigger each status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a clever idea, maybe the statuses' contexts could be more descriptive.
I can list some TODO's, but I will provide an exact list when we decide what exactly should be done in this PR. I listed some problems that I met in related issue #1125, as you saw. Percentage discounts are a really good idea, but there are few problems when it comes to handle them properly. One thing that I plan to do for now is to recalculate an order total value every time the order state changes (e. g. products, shipping or a discount). So with percentage discount we should hold this information in an order. I would move this feature to separate PR. Optional shipping method and removing customer (this one will be added for purpose of changing between an existing and unregistered user) are related to IRL orders and we have not supported this until now. We must decide if we want to. I think that I could provide optional/default shipping address in some way in this PR. That's a good idea. Marking order as paid is one of the features we plan to add in the future. Thank you for your support with this feature 🙂 Your feedback is really helpful. |
Sounds great for me! Thanks for the answer! |
ce1f6f5
to
94baaf5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests missing
saleor/checkout/utils.py
Outdated
"""Calculate discount value for a voucher of product or category type.""" | ||
if voucher.type == VoucherType.PRODUCT: | ||
prices = [ | ||
item[1] for item in get_product_variants_and_prices( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we unpack here? I've seen that get_product_variants_and_prices
returns 2 el tuple, variant
and variant_price
Q(email__icontains=search_query)) | ||
|
||
users = [ | ||
{'id': user.pk, 'text': user.ajax_label} for user in queryset] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you may use queryset.values()
here, instead of putting dict manually
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good idea, but ajax_label
is property, not database field.
saleor/dashboard/order/forms.py
Outdated
|
||
|
||
class CreateOrderFromDraftForm(forms.ModelForm): | ||
"""Save draft order as a ready to fulfill.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can mark
as ready to fulfill, not save
, also an article before ready
is not necessary
saleor/dashboard/order/forms.py
Outdated
shipping_address = self.instance.shipping_address | ||
if ( | ||
method and shipping_address and | ||
method.country_code != ANY_COUNTRY and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think introducing variable for this would improve readability, that's lots of ands for simple if
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'll move it to variable 🙂
model = Order | ||
fields = [] | ||
|
||
def __init__(self, *args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed
saleor/dashboard/order/forms.py
Outdated
|
||
if customer: | ||
if ( | ||
customer.default_billing_address == |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we doing this? Maybe some comment would be helpful, for one going back to this after some time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cause if we set billing and shipping address in an order to default user addresses before, we want to unset this addresses when removing customer. But if custom changes were made, we leave it, as it might have been intended. I'll add a comment or maybe move whole logic of removing customer to separate function, marked with docstring.
saleor/order/models.py
Outdated
return self.status != OrderStatus.CANCELED | ||
return self.status not in {OrderStatus.CANCELED, OrderStatus.DRAFT} | ||
|
||
def can_mark_as_paid(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will trigger new query when fired, I've seen its used only in template once, can we pass it from the view instead? I think payments should be already prefetched on order detail view
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, such helpers are very convenient, but those that perform database queries should be rather avoided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll fix it.
saleor/order/utils.py
Outdated
"""Recalculate and assign total price of order. | ||
|
||
Total price is a sum of items in order and order shipping price minus | ||
discount amount. | ||
""" | ||
prices = [line.get_total() for line in order] | ||
total = sum(prices, order.shipping_price) | ||
# discount amount can't be greater than order total | ||
order.discount_amount = min( | ||
discount_amount or order.discount_amount or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd assign it to some sort of variable and then pass it to min()
function, it would be more readable
docs/architecture/orders.rst
Outdated
@@ -34,3 +34,5 @@ Order status is based on statuses of its fulfillments. There are four possible s | |||
|
|||
- ``CANCELED`` | |||
Order has been canceled. Every fulfillment (if there is any) has ``CANCELED`` status. Order doesn't require further actions by a shop operator. | |||
|
|||
There is also ``DRAFT`` status, used for orders created from dashboard. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not exactly for all orders created from the dashboard, but only for those which were not published.
def drafts(self): | ||
return self.filter(status=OrderStatus.DRAFT) | ||
|
||
def to_ship(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we've started moving logic out of Object Managers in favour of utils
helpers
eg. product/utils.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used to have heavy functions in managers with a lot of logic. But when you only have to prepare a specific queryset, then this is a good place.
tests/conftest.py
Outdated
@@ -52,8 +52,26 @@ def cart(db): # pylint: disable=W0613 | |||
|
|||
|
|||
@pytest.fixture | |||
def customer_user(db): # pylint: disable=W0613 | |||
return User.objects.create_user('test@example.com', 'password') | |||
def default_billing_address(db): # pylint: disable=W0613 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be used as any address, not exactly default billing, for the purpose of creating customer_user
, and as I'm aware - it is.
I think that address
is a better fit, as and advantage we won't end up with several:
address = default_billing_address
in tests, to fit everything within 80 chars
3b9d79a
to
844170d
Compare
tests/dashboard/test_order.py
Outdated
@@ -1,15 +1,20 @@ | |||
from decimal import Decimal | |||
from unittest.mock import patch | |||
from unittest.mock import MagicMock, Mock, patch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think MagicMock
is unused
saleor/order/utils.py
Outdated
@@ -59,6 +59,10 @@ def recalculate_order(order, **kwargs): | |||
Voucher discount amount is recalculated by default. To avoid this, pass | |||
update_voucher_discount argument set to False. | |||
""" | |||
# do not use prefetched order lines, cause they might have changed | |||
if hasattr(order, '_prefetched_objects_cache'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite uncommon, wouldn't order.refresh_from_db()
do the trick (if it fetches related objects which I'm not sure of)? Anyway, it's very intriguing that we have to remove prefetched data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unhopefully, refresh_from_db
doesn't work. Don't have any other ideas right now.
I want to merge this change because it adds an ability to create order from dashboard. Those orders have
DRAFT
status and can be edited until confirmed by staff users. Draft status allows to add new products, change shipping, modify discount, assign customer or user email and edit shipping and billing address. Confirmed draft order changes status toUNFULFILLED
and customer can be notified (optionally) about it.Closes #1125
TODO's
shipping_address
,billing_address
,shipping_method
)Same as shipping address
in billing address details if equaldiscount_amount
value in order set to 0Pull Request Checklist
pycodestyle
,pydocstyle
,pylint
.eslint
.[Edit]
Draft order details dashboard page.
Order header for draft orders is
Order draft #<number>
. You can edit shipping, discount and discount amount by clicking on their names/values. You can edit customer and billing/shipping address by clickingEdit
button next to them. Billing address containsSame as shipping address
note if not differs from shipping address (but you can still edit it).Create order
button is highlighted as the most important action.Edit voucher modal.
You can choose any voucher to apply to an order without validation if it is correct for current state - it will be done while saving draft order. If some voucher is already applied, additional
Remove voucher from order
action appears in modal.Edit customer modal.
You can set any existing user as a customer in a draft order (order is invisible until created) or just set an email, but only one option is accepted. If some customer is already set, additional
Remove customer from order
action appears in modal. Order can have no customer set - then customer is simple aGuest
.