django-salesman icon indicating copy to clipboard operation
django-salesman copied to clipboard

Stock Handling

Open IncreaseComputers opened this issue 2 years ago • 8 comments

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?

IncreaseComputers avatar Apr 25 '22 14:04 IncreaseComputers

I think the item validator would do the job if it could be raised on item deletes/ basket clears

IncreaseComputers avatar Apr 25 '22 15:04 IncreaseComputers

@IncreaseComputers could you perhaps use Django's post_save and post_delete signals on the BaksetItem model to sync with your stock management?

The basket item validator is used just for validating the item, it is not aware of deletes.

dinoperovic avatar Apr 26 '22 07:04 dinoperovic

Signals wont work as getting the original values from the objects is problematic - Ie in basket save has the quantity changed? what was the old quantity what is the new quantity? - I have implemented a "stock _handler" override similar to the validators and hooked into it at various points in basket and basket item to deal with changes - it passes a operation "delete" / "add" /"update" the original object and the changed fields - it works but it means overriding core code making it more difficult to maintain

IncreaseComputers avatar Apr 27 '22 07:04 IncreaseComputers

I see what you're saying. I suppose you could swap the basket models and add the additional field that keeps track of the "previous quantity" on the basket item. You could do this using the swappable models feature: https://django-salesman.readthedocs.io/en/latest/advanced/swappable_models.html

The thing with stock handling is that it can differ from the shop implementation making it hard to put specific logic into the core project. Maybe having dedicated signals for whenever an item is added/updated/deleted on the basket would solve this.

I would also consider a different approach to stock management, one where it does not tie to the basket directly. I had previously used a system like this:

  • In a payment method, when checkout is called, create a stock reservation object for the basket
  • Add logic to the validate_basket method on payment that first checks if all items are available (taking reservations into account)
  • Once the payment is completed, commit that stock reservation. If payment fails, drop the reservation
  • Optionally add a basket modifier that checks item availability based on reservations so that it can be communicated to the customer before checkout

This way you're not updating the stock on every basket item add/remove, but rather when the intent to buy the items is created. Also, users tend to add items to the basket and leave the shop, with your approach you would have decreased the stock for that item indefinitely -- this can, of course, be handled by implementing an abandoned cart feature, but you see my point.

dinoperovic avatar Apr 27 '22 08:04 dinoperovic

one of the problems (which is specific to my implementation) is that I'm selling memberships - which have a limit - so I have to follow the basket rather than the order - I cant oversell... - I will stick with my overrides for now - I'm planning to also implement the methods into the order to also convert a "reservation" to a completed stock adjustment.

IncreaseComputers avatar Apr 29 '22 08:04 IncreaseComputers

To handle the abandoned item I have added an "expiry" timer to the item which removes it from the cart after a specified time

IncreaseComputers avatar Apr 29 '22 08:04 IncreaseComputers

@IncreaseComputers I believe you would still avoid overselling in my example by raising ValidationError in validate_basket method on the payment if any one of the items is not available.

Either way, here's an example implementation using Django signals without overriding the basket item model:

from django.dispatch import receiver
from django.db.models.signals import post_delete, post_init, post_save

from salesman.core.utils import get_salesman_model

BasketItem = get_salesman_model("BasketItem")


@receiver(post_init, sender=BasketItem)
def post_init_item(sender, instance, **kwargs):
    # Remember current quantity on the instance
    instance._current_quantity = 0 if instance.pk is None else instance.quantity


@receiver(post_save, sender=BasketItem)
def post_save_item(sender, instance, **kwargs):
    quantity_diff = instance.quantity - instance._current_quantity
    if quantity_diff > 0:
        print(f"Decrease {instance.product} stock by {quantity_diff}")
    elif quantity_diff < 0:
        print(f"Increase {instance.product} stock by {quantity_diff * -1}")


@receiver(post_delete, sender=BasketItem)
def post_delete_item(sender, instance, **kwargs):
    print(f"Increase {instance.product} stock for {instance.quantity}")

You can of course override the __init__, save and delete methods on basket item directly using swappable models.

I am considering adding the item_quantity_changed signal that would basically do this by default, but I'm still not sure if it would be helpful for any other cases.

dinoperovic avatar Apr 29 '22 12:04 dinoperovic

By the way, I would appreciate this item_quantity_changed signal ok

Thanks for this amazing job!

Em sex., 29 de abr. de 2022 às 09:06, Dino Perovic @.***> escreveu:

@IncreaseComputers https://github.com/IncreaseComputers I believe you would still avoid overselling in my example by raising ValidationError in validate_basket method on the payment if any one of the items is not available.

Either way, here's an example implementation using Django signals without overriding the basket item model:

from django.db.models.signals import post_delete, post_init, post_save from salesman.core.utils import get_salesman_model BasketItem = get_salesman_model("BasketItem")

@receiver(post_init, sender=BasketItem)def post_init_item(sender, instance, **kwargs): # Remember current quantity on the instance instance._current_quantity = 0 if instance.pk is None else instance.quantity

@receiver(post_save, sender=BasketItem)def post_save_item(sender, instance, **kwargs): quantity_diff = instance.quantity - instance._current_quantity if quantity_diff > 0: print(f"Decrease {instance.product} stock by {quantity_diff}") elif quantity_diff < 0: print(f"Increase {instance.product} stock by {quantity_diff * -1}")

@receiver(post_delete, sender=BasketItem)def post_delete_item(sender, instance, **kwargs): print(f"Increase {instance.product} stock for {instance.quantity}")

You can of course override the init, save and delete methods on basket item directly using swappable models.

I am considering adding the item_quantity_changed signal that would basically do this by default, but I'm still not sure if it would be helpful for any other cases.

— Reply to this email directly, view it on GitHub https://github.com/dinoperovic/django-salesman/issues/18#issuecomment-1113236648, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFQYBTZ3MR4Y55SCWLQVJ5DVHPGFPANCNFSM5UIWS6KQ . You are receiving this because you are subscribed to this thread.Message ID: @.***>

-- ¶ Pablo Valentini Nelson Boulangerie @.***>(43) 3321 3954 comercial (43) 8404 9009 celular

pablondrina avatar Apr 29 '22 12:04 pablondrina