dinoperovic / django-salesman Goto Github PK
View Code? Open in Web Editor NEWHeadless e-commerce framework for Django and Wagtail.
Home Page: https://django-salesman.rtfd.io
License: BSD 3-Clause "New" or "Revised" License
Headless e-commerce framework for Django and Wagtail.
Home Page: https://django-salesman.rtfd.io
License: BSD 3-Clause "New" or "Revised" License
The basket serializer expects an Integer primary key for product_id
.
From an initial search it looks like this is the only spot requiring Integer primary keys. UUID primary keys are common and for consistency it is nice to use them here.
Please consider adding support for UUID (or more generically, non-integer) primary keys.
Thanks!
It would be helpful if there was an endpoint one could use to patch extra key values like:
<form
hx-patch="{% endverbatim %}{% url 'salesman-basket-list' %}{% verbatim %}{{ ref }}/"
hx-swap="none"
>
<input type="number" name="unit_price_override" value="{{ unit_price }}" min="0" step="0.01">
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">Submit</button>
</form>
The result of submitting the form should be that the unit_price_override
key in the basket item extra dict is set to the submitted value without modifying the other keys of the extra data.
Is your feature request related to a problem? Please describe.
User can populate extra
field with any amount of JSON data using the API. It would be useful to allow a custom validator for extra
field on basket and basket item serializer, similar to how address validation works on the checkout serializer.
Describe the solution you'd like
Validate extra
field before it gets saved on basket or basket item using a custom validator function configured in settings.py
.
Describe alternatives you've considered
-
Additional context
-
It would be a helpful shortcut if PUT quantity 0 acted as delete for basket items
We are implementing a multi site Wagtail based store. The store should have the payment methods limited based on the site.
For example for country Germany you should have Stripe but for US we should only allow the PayPal payment method. Currently the payment_methods_pool does not have a setting to override or modify the Pool in any way.
We'd be happy to help build such a feature if an agreement on how this should be done could be described and agreed upon.
Swappable models release indicates it should work for Django 3.1+.
Should it work with Django 3.x? Was hoping to add this to a project that hasn't completed the update to 4+ yet.
Migrations fail for me on Django<4:
ValueError: The field salesmanbasket.BasketItem.basket was declared with a lazy reference to 'shop.basket', but app 'shop' isn't installed.
The field salesmanorders.OrderItem.order was declared with a lazy reference to 'shop.order', but app 'shop' isn't installed.
The field salesmanorders.OrderNote.order was declared with a lazy reference to 'shop.order', but app 'shop' isn't installed.
The field salesmanorders.OrderPayment.order was declared with a lazy reference to 'shop.order', but app 'shop' isn't installed.
If I install Django 4+ then the migration is successful.
-- edit --
It does appear to work on 3.x once the migration is created on 4.x
Probably related to #29 and #30 but mentioning issue so it is tracked.
It would be helpful for situations where a customer genuinely does not have an email address to allow checkout without one.
The Order model does not require an email address but the Checkout serializer does.
Simplest workaround I've come up with for the moment is to submit with an email placeholder and then remove it with a pre_save
signal.
Are we going to get support for wagtail V6?
Waiting on Wagtail-2.13
release that adds support for Django-3.2
.
Currently, when overriding and expanding your Order model with custom fields (let's say I want to store first_name
and last_name
on my order besides the default email
), everything has to go through the extras
object during checkout. I was wondering if there is (or could be in the future) a more robust way of doing this, as it currently feels like twisting the frameworks arm a bit. Let me clarify.
Say I expand my order model with these fields:
from salesman.orders.models import BaseOrder
class Order(BaseOrder):
first_name = TextField(_("First name"), null=True, blank=True)
last_name = TextField(_("Last name"), null=True, blank=True)
And, after creating a basket I initiate a checkout with this JSON:
{
"email": "[email protected]",
"first_name": "Tim",
"last_name": "van der Linden",
"payment_method": "pay-later",
}
To get the extra fields first_name
and last_name
stored on my order I need to override the populate_from_basket()
method on the Order and add in these two fields:
@transaction.atomic
def populate_from_basket(
self,
basket: BaseBasket,
request: HttpRequest,
**kwargs: Any,
) -> None:
"""
Populate order with items from basket.
Args:
basket (Basket): Basket instance
request (HttpRequest): Django request
"""
from salesman.basket.serializers import ExtraRowsField
if not hasattr(basket, "total"):
basket.update(request)
self.user = basket.user
self.email = basket.extra.pop("email", "")
self.first_name = basket.extra.pop("first_name", "") # <- added in
self.last_name = basket.extra.pop("last_name", "") # <- added in
...
As you can see I need to pop them from the extras object of the basket to store them on the final Order.
I understand that the checkout itself is not a model and is more of a process, your Basket simply gets "transformed" into an Order with the above method. But still it feels strange to pull all of our custom data we wish to capture from the extras
object of the basket.
Is this the intended way? Or should the Basket model be overridden as well and contain mirror fields of the data we want to store on the final Order (eg. a custom Basket model which would also contain a first_name
and last_name
field)?
Please i did love to know how long the project will be supported and maintained. I would love to know if i should go ahead and use it in production. How long will it receive updates?
Describe the bug
On Wagtail 2.14 the orders admin page returns the following error:
TemplateDoesNotExist at /cms/salesman/order/
modeladmin/salesman/order/index.html, modeladmin/salesman/index.html, modeladmin/index.html
Request Method: | GET
-- | --
http://localhost:8000/cms/salesman/order/
3.2.6
TemplateDoesNotExist
modeladmin/salesman/order/index.html, modeladmin/salesman/index.html, modeladmin/index.html
/usr/local/lib/python3.8/site-packages/django/template/loader.py, line 47, in select_template
/usr/local/bin/python
3.8.1
['/app', '/usr/local/bin', '/usr/local/lib/python38.zip', '/usr/local/lib/python3.8', '/usr/local/lib/python3.8/lib-dynload', '/usr/local/lib/python3.8/site-packages']
Tue, 17 Aug 2021 19:53:46 +0000
To Reproduce
Steps to reproduce the behavior:
Technical details:
This causes the amount
PriceField in ExtraRowSerializer to not conform to the request.
Add more flexibility to modifiers through additional hooks.
This is somewhat related to #29. Currently it is assumed that addresses (billing and shipping) are given as strings. However, in our case (and I boldly assume in many) addresses are stored as address objects with separate components. To store proper address objects on an Order I override the Order model first:
from salesman.orders.models import BaseOrder
class Order(BaseOrder):
shipping_address = ForeignKey(
"webshop.OrderAddress",
null=True,
blank=True,
on_delete=SET_NULL,
related_name="order_shipping_address",
)
billing_address = ForeignKey(
"webshop.OrderAddress",
null=True,
blank=True,
on_delete=SET_NULL,
related_name="order_billing_address",
)
Making shipping_address
and billing_address
foreign keys to the addresses table. Then on checkout I submit them like this:
{
"email": "[email protected]",
"shipping_address": {
"street": "Shippingstreet",
"house_number": 1,
"postal_code": "1234",
"locality": "Some Locality",
"country": "BE"
},
"billing_address": {
"street": "Billingstreet",
"house_number": 1,
"postal_code": "1234",
"locality": "Some Locality",
"country": "BE"
},
"payment_method": "pay-later",
}
Now to be able for Salesman to handle this I first need to override the Checkout serializer:
from salesman.checkout.serializers import CheckoutSerializer
class CustomCheckoutSerializer(CheckoutSerializer):
shipping_address = OrderAddressSerializer()
billing_address = OrderAddressSerializer()
def validate_shipping_address(self, value: OrderAddressSerializer) -> OrderAddressSerializer:
context = self.context.copy()
context["address"] = "shipping"
return validate_address(value, context=context)
def validate_billing_address(self, value: OrderAddressSerializer) -> OrderAddressSerializer:
context = self.context.copy()
context["address"] = "billing"
return validate_address(value, context=context)
def save(self, **kwargs: Any) -> None:
basket, request = self.context["basket"], self.context["request"]
....
# Add in address fields.
basket.extra["shipping_address"] = self.validated_data["shipping_address"]
basket.extra["billing_address"] = self.validated_data["billing_address"]
The OrderAddressSerializer
is a simple ModelSerializer
which serializes the address objects. I have overridden both the validate_shipping_address()
and validate_billing_address()
functions to accept the object data instead of simple string data. The actual validator function needs to be written as well:
from webshop.serializers.orders.order_address import OrderAddressSerializer
def validate_address(value: OrderAddressSerializer, context: dict[str, Any] = {}) -> str:
if not value:
raise ValidationError(_("Address is required."))
return value
And set this in the config:
SALESMAN_ADDRESS_VALIDATOR = "webshop.utils.validate_address"
And finally I assign the validated data to the extra
object on the basket.
Next the populate_from_basket()
method on the custom Order model needs to be modified:
@transaction.atomic
def populate_from_basket(
self,
basket: BaseBasket,
request: HttpRequest,
**kwargs: Any,
) -> None:
from webshop.models import OrderAddress
from salesman.basket.serializers import ExtraRowsField
if not hasattr(basket, "total"):
basket.update(request)
self.user = basket.user
self.email = basket.extra.pop("email", "")
self.shipping_address = OrderAddress.objects.create(**basket.extra["shipping_address"])
self.billing_address = OrderAddress.objects.create(**basket.extra["billing_address"])
Here the data found on the extra
object is expanded into actual Address objects and assigned to the Order.
To round things up I need to make sure that Salesman will use my CustomCheckoutSerializer
instead of the default. As this is not configurable I am forced to override the routing by defining a new View:
from salesman.checkout.views import CheckoutViewSet
from webshop.serializers.checkout.checkout import CustomCheckoutSerializer
class CustomCheckoutViewSet(SalesmanCheckoutViewSet):
serializer_class = CustomCheckoutSerializer
And then wire this view into my route:
router.register("checkout", CustomCheckoutViewSet, basename="salesman-checkout")
This feels a bit convoluted, or is this the intended way?
I think making the checkout serializer configurable (eg. SALESMAN_CHECKOUT_SERIALIZER
) would at least take care of not having to override the view and routing. But we are still left with the assumption of addresses being strings and a "hacky" way of trying to change this via the extra
object on a basket.
Should be implemented the same way the other "formatter" function are.
SALESMAN_ADMIN_JSON_FORMATTER = 'path.to.function'
I'm looking to implement stock handling for order s/ basket items - handling the stock is fine the problem I'm having is catching basket quantity changes to adjust stock levels, any ideas or suggestions on doing this?
For stores that sell something intangible (courses, images, videos and much more), indicating the delivery and purchase address is not required. I found a workaround and allow empty strings in the address validator, but it doesn't look very nice in the admin panel
validate_basket_item is called twice per item
Currently, a checkout is a transition from a Basket to an Order via a single API call and returns either a redirect URL to a payment method processing page or a custom URL (such as a thank you page).
However, I think there are use cases where a checkout can be multiple steps. For example, during checkout you define the shipping address, yet the shipping cost (which you define as a modifier on your Basket) could be variable depending on your address - it is not always a flat fee. Currently we would have to add the shipping address to the basket to be able to add a variable shipping cost modifier ... and then again to the checkout to store the shipping address on the order. This feels hacky.
Having multiple steps during checkout would allow to set Basket modifiers (or Checkout modifiers in that case?) which would add extra lines to the Basket/Order depending on data that is given during the checkout.
This would probably require a checkout to become its own model which sits between the basket and the final order and to which different data is applied during various steps. I imagine API routes such as:
POST
to /shop/checkout/
to start a checkout, returning a checkout ref (giving your Basket ref).PUT
to /shop/checkout/<checkout-ref>/
to update the checkout with various dataPOST
to /shop/checkout/<checkout-ref>/finalize
to transition the checkout to an orderVia various calls to the PUT
route one could implement multiple steps, adding more data to the checkout as they go along. The final POST
would act similar to the current checkout, as it would return a URL to redirect to (external payment method, thank you page, ...). Checkout models would also contain a configurable status object depicting the various steps such as "define shipping", "define contact data", "define gift wrapping", ... .
Having a checkout being a separate model and giving it its own status could also provide some more statistical insight in the backend, as we would have an admin showing the various checkouts and where the user might have jumped off, aka. on which status a checkout is left.
I think that currently the idea is that the basket is used for all of the above, as in: we patch the basket during "checkout" until we are ready to commit this to an Order. But on the other hand only the checkout route contains crucial data such as the shipping address which is needed to calculate the shipping basket modifier.
Semantically I feel as if a basket is simply your shopping cart, the checkout is its own thing where your patch in various items such as addresses, contact info, gift wrapping, ... and an order is all of that finalized.
Is your feature request related to a problem? Please describe.
Saving extra data on order (eg. "phone number") during the checkout requires an additional POST request to /api/basket/extra/
.
Describe the solution you'd like
When sending a POST request to /api/checkout/
add an ability to send extra
data in a payload directly.
Describe alternatives you've considered
-
Additional context
Validation for extra data should be enforced here as well (#1).
Describe the bug
ERROR: Cannot install -r requirements.in (line 24), django-salesman==1.1.6 and django>=4.2 because these package versions have conflicting dependencies.
Django 4.2 was released and salesman I think would run just fine on the newer version as well but because of the Django version limitations it's not installable from pip.
Could we expand the Django versions it supports?
It would be helpful to the customer if there was a way to display a short label with the order payment. For example, VISA ending in 1111
instead of just the identifier credit-card
or whatever the payment method identifier is.
The payment_method is useful for the api but doesn't really give the customer enough information to remember which statement to check.
This seems generally useful and would probably be a good thing to include. If not, it would be helpful to allow adding additional fields here
I was refactoring a payment method and noticed that when I used a base class to hold common method that the payment method choices were no longer presented.
Here is a simple example using the documented payment methods:
Payment Methods:
from django.core.exceptions import ValidationError
from django.urls import reverse
from salesman.checkout.payment import PaymentMethod
from salesman.core.utils import get_salesman_model
Order = get_salesman_model('Order')
class PayOnDelivery(PaymentMethod):
"""
Payment method that expects payment on delivery.
Copied from https://github.com/dinoperovic/django-salesman/blob/8105e84c0ccca00f483122b91a74175131808399/example/shop/payment/on_delivery.py#L9
"""
identifier = "pay-on-delivery"
label = "Pay on delivery"
def validate_basket(self, basket, request):
"""
Payment only available when purchasing 10 items or less.
"""
super().validate_basket(basket, request)
if basket.quantity > 10:
raise ValidationError("Can't pay for more than 10 items on delivery.")
def basket_payment(self, basket, request):
"""
Create order and mark it as shipped. Order status should be changed
to `COMPLETED` and a new payment should be added manually by the merchant
when the order items are received and paid for by the customer.
"""
order = Order.objects.create_from_basket(basket, request, status="SHIPPED")
basket.delete()
url = reverse("salesman-order-last") + f"?token={order.token}"
return request.build_absolute_uri(url)
class PayOnDelivery2(PayOnDelivery):
identifier = "pay-on-delivery-2"
label = "Pay on delivery 2"
class PayOnDelivery3(PayOnDelivery):
identifier = "pay-on-delivery-3"
label = "Pay on delivery 3"
def basket_payment(self, *args, **kwargs):
return super().basket_payment(*args, **kwargs)
Setting:
SALESMAN_PAYMENT_METHODS = [
'path.to.PayOnDelivery',
'path.to.PayOnDelivery2',
'path.to.PayOnDelivery3',
]
When selected the payment method, PayOnDelivery2 is not presented as a choice.
It is not very clear how to get an overview of Salesman's components, how to manage products, ... within Wagtail.
The SALESMAN_ADMIN_REGISTER
setting seems to imply that it is set to True
by default and gives us all kinds of Admin views, however only Orders
is added to the Wagtail admin menu.
Is the idea that we should build our own Admin views using ModelAdmin?
Or are there default views for all components (Products, Baskets, ...) shipped and need they be enabled somehow?
Describe the bug
iSort dependency prevents poetry resolution
To Reproduce
➜ django-salesman (master) ✔ poetry install -E example
The currently activated Python version 2.7.16 is not supported by the project (^3.6).
Trying to find and use a compatible version.
Using python3 (3.8.5)
Creating virtualenv django-salesman-J2JSvch3-py3.8 in /Users/raymaun/Library/Caches/pypoetry/virtualenvs
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'PosixPath' is not defined
Updating dependencies
Resolving dependencies... (468.6s)
Writing lock file
[SolverProblemError]
The current project's Python requirement (2.7.16) is not compatible with some of the required packages Python requirement:
- isort requires Python >=3.6,<4.0
Because isort (5.5.1) requires Python >=3.6,<4.0
and no versions of isort match >=5.4,<5.5.1 || >5.5.1,<6.0, isort is forbidden.
So, because django-salesman depends on isort (^5.4), version solving failed.
➜ django-salesman (master) ✔
Expected behavior
That poetry install -E example
would resolve the dependencies.
Technical details:
When adding/updating/deleting/etc. items, additional GET request on /api/basket/ is needed to return a fresh basket.
When ?basket
is apended to the url (/api/basket/?basket, /api/basket/ref-1/?basket) the response should return a new basket after the desiered operation.
Are there any plans to support Wagtail 4.x?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.