Django药品库存管理系统:生成销售单后自动更新药品库存值的技术实现问询
Hey there! Let's fix up your inventory management so that stock gets automatically deducted when a sale is created. First, I noticed a small bug in your MedicineSale.save() method that we should address first: you're calculating total_amt1 using price_no_2 instead of price_no_1—that'll throw off your totals! Let's fix that and add the stock deduction logic.
Option 1: Add Stock Deduction to the save() Method (Direct Approach)
We can modify the MedicineSale model's save() method to handle stock updates right when the sale is saved. We'll also use database transactions to make sure that if the sale fails to save, the stock doesn't get incorrectly deducted.
Here's the updated code:
from django.db import transaction, IntegrityError class MedicineSale(models.Model): # ... keep your existing fields ... def save(self, *args, **kwargs): # Fix the total calculation bug first self.total_amt1 = self.qty_no_1 * self.price_no_1 self.total_amt2 = self.qty_no_2 * self.price_no_2 if self.medicine2 else 0 self.total_amt3 = self.qty_no_3 * self.price_no_3 if self.medicine3 else 0 self.total_amt4 = self.qty_no_4 * self.price_no_4 if self.medicine4 else 0 self.total = self.total_amt1 + self.total_amt2 + self.total_amt3 + self.total_amt4 # Handle stock deduction with transaction to avoid partial updates with transaction.atomic(): is_new_sale = self.pk is None original_quantities = {} if not is_new_sale: # Get original quantities from the database before updating original_sale = MedicineSale.objects.get(pk=self.pk) original_quantities = { 'medicine1': original_sale.qty_no_1, 'medicine2': original_sale.qty_no_2 if original_sale.medicine2 else 0, 'medicine3': original_sale.qty_no_3 if original_sale.medicine3 else 0, 'medicine4': original_sale.qty_no_4 if original_sale.medicine4 else 0, } # Process each medicine in the sale medicine_updates = [ (self.medicine1, self.qty_no_1, original_quantities.get('medicine1', 0)), (self.medicine2, self.qty_no_2, original_quantities.get('medicine2', 0)), (self.medicine3, self.qty_no_3, original_quantities.get('medicine3', 0)), (self.medicine4, self.qty_no_4, original_quantities.get('medicine4', 0)), ] for med, new_qty, old_qty in medicine_updates: if not med: continue # Calculate how much the quantity changed (for updates) qty_difference = new_qty - old_qty if qty_difference > 0: # Only deduct if we're adding more quantity than before (or it's a new sale) if med.stock_value < qty_difference: raise IntegrityError(f"Insufficient stock for {med.medicine_name}: Available {med.stock_value}, Required {qty_difference}") med.stock_value -= qty_difference med.save() elif qty_difference < 0: # If quantity was reduced, add the difference back to stock med.stock_value += abs(qty_difference) med.save() # Save the sale only after all stock checks pass super(MedicineSale, self).save(*args, **kwargs)
Key Notes:
- Transaction Safety:
transaction.atomic()ensures that either all stock updates and the sale save succeed, or none do—no partial updates if something goes wrong. - Update Handling: If you edit an existing sale, we adjust stock based on the difference between old and new quantities (e.g., reducing the sold quantity adds stock back).
- Stock Validation: We check for sufficient stock before deducting, and throw an error if there's not enough (you can catch this in your views to show a user-friendly message).
Option 2: Use Django Signals (Decoupled Approach)
If you prefer to keep business logic separate from the model's save method, use Django's post_save signal. This keeps your code modular and reusable.
Add this to your models.py (or a dedicated signals.py file):
from django.db.models.signals import post_save from django.dispatch import receiver from django.db import transaction, IntegrityError @receiver(post_save, sender=MedicineSale) def update_stock_on_sale(sender, instance, created, **kwargs): with transaction.atomic(): if created: # Handle new sale: deduct stock directly sale_items = [ (instance.medicine1, instance.qty_no_1), (instance.medicine2, instance.qty_no_2), (instance.medicine3, instance.qty_no_3), (instance.medicine4, instance.qty_no_4), ] for med, qty in sale_items: if med and qty > 0: if med.stock_value < qty: raise IntegrityError(f"Insufficient stock for {med.medicine_name}") med.stock_value -= qty med.save() else: # Handle sale updates: calculate quantity changes original_sale = MedicineSale.objects.get(pk=instance.pk) qty_changes = [ (instance.medicine1, instance.qty_no_1 - original_sale.qty_no_1), (instance.medicine2, instance.qty_no_2 - original_sale.qty_no_2 if instance.medicine2 else 0), (instance.medicine3, instance.qty_no_3 - original_sale.qty_no_3 if instance.medicine3 else 0), (instance.medicine4, instance.qty_no_4 - original_sale.qty_no_4 if instance.medicine4 else 0), ] for med, change in qty_changes: if med and change != 0: if change > 0 and med.stock_value < change: raise IntegrityError(f"Insufficient stock for {med.medicine_name}") med.stock_value -= change med.save()
If you use a separate signals.py file, register it in your app's apps.py:
from django.apps import AppConfig class YourAppConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'your_app_name' def ready(self): import your_app_name.signals
Bonus: Optimize Your Model Structure
Right now, your MedicineSale model uses hardcoded medicine1 to medicine4 fields, which limits each sale to 4 items max. A better approach is to use a one-to-many relationship with a MedicineSaleItem model—this lets you add any number of items to a sale:
Updated Models:
class Medicine(models.Model): # ... keep your existing fields ... class MedicineSale(models.Model): total = models.FloatField(editable=False, default=0) sale_date = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): # Calculate total from related sale items self.total = sum(item.total for item in self.items.all()) super().save(*args, **kwargs) def __str__(self): return f"SALE #{self.pk} - {self.sale_date}" class MedicineSaleItem(models.Model): sale = models.ForeignKey(MedicineSale, related_name='items', on_delete=models.CASCADE) medicine = models.ForeignKey(Medicine, on_delete=models.CASCADE) quantity = models.FloatField() price = models.FloatField() total = models.FloatField(editable=False, default=0) def save(self, *args, **kwargs): self.total = self.quantity * self.price super().save(*args, **kwargs)
Then adjust your stock deduction logic to loop through instance.items.all() instead of hardcoded fields—this is way more scalable!
Update Admin for the Optimized Model:
Add an inline for MedicineSaleItem so you can add items directly in the sale form:
from django.contrib import admin from .models import Medicine, MedicineSale, MedicineSaleItem class MedicineSaleItemInline(admin.TabularInline): model = MedicineSaleItem extra = 1 class MedicineAdmin(admin.ModelAdmin): list_display = ('medicine_name', 'medicine_type', 'stock_value', 'price', 'expiry_date') list_display_links = ('medicine_name',) search_fields = ('medicine_name', 'medicine_type', 'expiry_date') list_filter = ('medicine_type', 'expiry_date') class MedicineSaleAdmin(admin.ModelAdmin): list_display = ('id', 'total', 'sale_date') inlines = [MedicineSaleItemInline] admin.site.register(Medicine, MedicineAdmin) admin.site.register(MedicineSale, MedicineSaleAdmin)
Testing the Functionality
- Go to your Django Admin, add a new
Medicinewithstock_value > 0. - Create a new
MedicineSale(or add items if you used the optimized model). - After saving, check the
Medicine'sstock_value—it should be reduced by the quantity sold. - Try creating a sale with more quantity than available stock—you should get an error (customize this message for your users in views!).
内容的提问来源于stack exchange,提问作者Ali Raza shakir

