From 7fbc2f7d410ea5b6419fa9eaa54a62f75ae4cd39 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 8 May 2026 18:23:16 +0300 Subject: [PATCH] Auto set the price for product when model is set --- backend/app/__pycache__/utils.cpython-314.pyc | Bin 2239 -> 2239 bytes .../__pycache__/product.cpython-314.pyc | Bin 2064 -> 2848 bytes backend/app/models/product.py | 11 +++- .../__pycache__/products.cpython-314.pyc | Bin 12735 -> 15692 bytes backend/app/routers/products.py | 54 +++++++++++++++++- .../__pycache__/product.cpython-314.pyc | Bin 5001 -> 5055 bytes backend/app/schemas/product.py | 4 +- frontend/src/pages/Admin.jsx | 25 +++++++- 8 files changed, 85 insertions(+), 9 deletions(-) diff --git a/backend/app/__pycache__/utils.cpython-314.pyc b/backend/app/__pycache__/utils.cpython-314.pyc index 1f90343d25b0f94dbc375cf05e2abbea3d3a90c9..0a8720d24492f363058179ef72f1683d879a7d79 100644 GIT binary patch delta 22 ccmdllxL=T0n~#@^0SH+C{LS>=$h(OH06$m;EC2ui delta 22 ccmdllxL=T0n~#@^0SM0i{+k)Nk#`dZ07ha4^#A|> diff --git a/backend/app/models/__pycache__/product.cpython-314.pyc b/backend/app/models/__pycache__/product.cpython-314.pyc index 151e4dd59d4314f56a8aeed891d0c0800ea04896..028e46865cbab03e13242199c8413f1aea5aaf40 100644 GIT binary patch delta 1065 zcma)4%}*0S6o1opw}h4oDYSq!t}FrtED|viL}G%*1QIQ4;DD9Qx;up?OV`W}m~d$i z8u20~dw~;h^k}^LHyA!lARF=P=1qeEE*fVRN{Qj%%g&qke)D^8-+ME!s#ju>j!0D{ zFuC{Vcg_jEiO8tF(+8kI5gr0X3Q*rfkosd_fis9QqT*A;YreOuXc0+ca0MxKaWE^C z7}M1>sl;jf7|;$SBtx>^8T4-#bi}gr#f4=CKU=#JQ~jt2V=&?CeF|VOGyx-M3MuG5 zdH`LBCGdUcd{-ox&X9Z(xkA>oG@A6eV%8$l8g;8wRW~);HgY;uRh!eBif($aFEeUm zVrPt5=bdo7;D%LI%jYesQLL(NZJJodj80Q0QJKZswM-opVIP7*{3lndqu#d5aPT_e z1{dB1TjAYL%;0^bIr@*_81E`m;ABrur}Z3-N9Jh)C2o0dJu;16p6+x`S}gNR7M8r< z`wJ!D|3k|qDc()Y4c)<1qElGH*(}y+ZWc?ANlwSoB?;#kPMGTrwbhoaC;6j8i%8R! zQNSicA89sLy_m^TJ;{J8cnE1?kSj1QHr>iHlihj1#%9*$6KWwBIV(Desd`++VMh0C z=}J#Ilrt>M9%iZtUqWXV`d;*Hgf4uH)-TpBH!d}9Mw{2d&D&Kq3*!sp^W%%RmWP&x zwi4aziSCWWjbqwYLu$PtwHfVN3wM2u#z-5>_ZpI&&iTNcK-!t-tOVZ6q=Q**rLGaG z<|v-F-H=5LOb#8A;=Gqayx*-k@pN@;nyQ*aq?sLiz25YCB{j_VRFEHBWm7nK4n?O< z91vD|K7`wxQSn=(IJ_TVPj!#KSJLKO4?eqlnaA-84@Xwf&n7!#;sG+kFg{D0KldMg egwSWmZF%1gwC=#U9ccL_$|yc>>;c=nB!2-!*72+W delta 282 zcmZ1=HbH<-n~#@^0SI<@ean2nx{*(rQHY0$fuVsRmZ5|(h@pfjj=h9=vL|BP3xq?JXG=sR688pQvCo|t*S;?TOH94HUiv1RAK~ZLM>f~$e=Nxab=H{oQ=G@{+ zPAn>k&n!vJExyH_UzCzs1m%dv#}}6*mSiT!Czg~HWhRxDq!!1=7s&%%@)_iu$!l4H zM2ZA~EHJ?iWE871GBC7Fmg2Ny>h_%+%lU|<#ktJ-3LH#a+>|F-6FKkkbZj2o%-xxqN*cbpWR89K; diff --git a/backend/app/models/product.py b/backend/app/models/product.py index bc97490..c36f30a 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -11,7 +11,7 @@ class Product(Base): name = Column(String, index=True) slug = Column(String, unique=True, index=True, nullable=True) description = Column(Text) - price = Column(Float) + price = Column(Float, nullable=True) # Nullable - can inherit from model discount_price = Column(Float, nullable=True) category_id = Column(Integer, ForeignKey("category.id")) model_id = Column(Integer, ForeignKey("model.id", ondelete="SET NULL"), nullable=True) @@ -31,3 +31,12 @@ class Product(Base): model = relationship("Model", back_populates="products") cart_items = relationship("CartItem", back_populates="product") order_items = relationship("OrderItem", back_populates="product") + + def get_effective_price(self) -> float: + """Get the effective price - either product's own price or model's base_price""" + if self.price is not None: + return float(self.price) + elif self.model and self.model.base_price is not None: + return float(self.model.base_price) + else: + return 0.0 # Default fallback diff --git a/backend/app/routers/__pycache__/products.cpython-314.pyc b/backend/app/routers/__pycache__/products.cpython-314.pyc index a852b3760f16af9a6cb8d9ac564e9a90ce804e72..b2fa4c3a4138717d2ee9f91f4b28d1e6a98a9282 100644 GIT binary patch delta 3708 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 delta 1841 zcmbtVZA_C_6u!6ZTl$uMK?}B}#bU9gg6H1{p)W+Ldq zrkm*4L9-d6OEhtbTY`2!O#Gqywa;XznP_7cf0*pYmhB~5_L0SH=M=xT?BCw>p7We@ z?>+aPbDw+rPjhj|YQc??ubohOr=bEHD&zb6hk26JAw|_`W57TAAFf zN{D~fGm~R_{p1=oi!1{(=S{ayHmfpOMrL!)NRwk~Hd&@emYFOI!zlfeJ*t%~8?!E% zPEHQ1Ib`LsY>R3ql;dC~tC~kJpUJj`B|(VDUd^~J)hh4T@&oXN$U% znxc%w%rINv0H4Fk8{jWotqY|SCd}3Lrg6q(R>)!|KMV0RUmmit?>{R#DyMf%&y9_`9*TWv+emeET;1-Dux-+v7je{BHBSCw$!#p7+$ud1_`W z4{toyydW`$M>W9toGal*3RQw)oc+oSR7Mm>`&3dh_Kf;qQGK)*`(P^9t100yo9oXy zz*s_xNuYYYnA~rZB`aD0v;Tvalxn3;6;X)$uUrA_1D|O+)Gtmew zu0mrc!gg%RAnbANgy!G?Z-m3{`fx8f11M;58xJm!xNCN+TGaOqR#iexl(sEoA%en^ z0!<4CTY($PE!*B8f^;L#7QYzWtIac**d`WZZF-pXEH!I8vGpqwPq0`BQ$C5OY`!OoYpBrqXVK4dqfCDS~uLgJ1*+Uwvl+2oYIa# zp(KTqu__`7(POc`UDPXY?^WVkGjU!bJF9PGbT}Co%0mXhyw97n9ecK6Q;wh*XQ~BG z6zw-=yWUxLGdnP+3&53SBfJ$#{Vu$o4gQ)HqcoTZ`!q4`+U%+pnQ`Ayq)MpqY-F37 zMaV#>B+GEo@6|LinDcKeL+*g$qcq+Qr`5Okr_28xe6$b$FW z#e6dLRp>O=4!*@9QCi76;B4t~J_w(cp7ADdTv~3}*B4JD2SU*9-3MynKSmw5e3}Qby zGFk6Ils1nhhDLh(G&NEC1LsE|SyzWw?8CZZX&QS!N!_Yj#!YEsZz55#d!#=;q`-rf zspsg0%{(W9ZfIJU7(tQ|QPSGPXj0H=r1dyDj%1>G3Mzo0y$D)fOvAz|xA8nex+_Wr ze iU+B$wI1Z8O1XF#n74JRyqN)aw8h~}^`}a7d{?6ZG70Rsu diff --git a/backend/app/routers/products.py b/backend/app/routers/products.py index f314829..97c7006 100644 --- a/backend/app/routers/products.py +++ b/backend/app/routers/products.py @@ -60,10 +60,13 @@ def list_products( products = query.offset(skip).limit(limit).all() - # Inherit sizes from model if product doesn't have sizes + # Inherit sizes and price from model if product doesn't have them for product in products: if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: product.sizes = product.model.sizes or [] + # Inherit price from model if not set + if product.price is None and product.model_id and product.model and product.model.base_price: + product.price = float(product.model.base_price) return products @@ -72,10 +75,13 @@ def list_products( def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)): products = search_products(db, q, skip=skip, limit=limit) - # Inherit sizes from model if product doesn't have sizes + # Inherit sizes and price from model if product doesn't have them for product in products: if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: product.sizes = product.model.sizes or [] + # Inherit price from model if not set + if product.price is None and product.model_id and product.model and product.model.base_price: + product.price = float(product.model.base_price) return products @@ -89,6 +95,9 @@ def get_product(product_id: int, db: Session = Depends(get_db)): # If product doesn't have sizes but has a model, inherit sizes from model if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: product.sizes = product.model.sizes or [] + # Inherit price from model if not set + if product.price is None and product.model_id and product.model and product.model.base_price: + product.price = float(product.model.base_price) return product @@ -99,6 +108,24 @@ def create_new_product( db: Session = Depends(get_db), admin: User = Depends(get_current_admin_user), ): + # Validate price: either product has price OR model has base_price + if product.price is None: + if not product.model_id: + raise HTTPException( + status_code=400, + detail="Price is required when no model is assigned" + ) + # Check if model exists and has base_price + from app.models import Model + model = db.query(Model).filter(Model.id == product.model_id).first() + if not model: + raise HTTPException(status_code=404, detail="Model not found") + if model.base_price is None: + raise HTTPException( + status_code=400, + detail="Model must have a base_price set if product price is not provided" + ) + # Auto-generate slug if not provided if not product.slug: base_slug = generate_slug(f"{product.brand} {product.name}") @@ -110,7 +137,18 @@ def create_new_product( counter += 1 product.slug = slug - return create_product(db, product) + created_product = create_product(db, product) + + # Refresh to get the model relationship + db.refresh(created_product) + + # Inherit price and sizes from model if needed + if created_product.price is None and created_product.model_id and created_product.model: + created_product.price = float(created_product.model.base_price) + if (not created_product.sizes or len(created_product.sizes) == 0) and created_product.model_id and created_product.model: + created_product.sizes = created_product.model.sizes or [] + + return created_product @router.put("/{product_id}", response_model=ProductResponse) @@ -123,6 +161,16 @@ def update_existing_product( product = update_product(db, product_id, product_update) if not product: raise HTTPException(status_code=404, detail="Product not found") + + # Refresh to ensure model relationship is loaded + db.refresh(product) + + # Inherit price and sizes from model if needed + if product.price is None and product.model_id and product.model and product.model.base_price: + product.price = float(product.model.base_price) + if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model: + product.sizes = product.model.sizes or [] + return product diff --git a/backend/app/schemas/__pycache__/product.cpython-314.pyc b/backend/app/schemas/__pycache__/product.cpython-314.pyc index d857622f1acc16663460e852b8a6e6d124519833..3b37ae28b1b6c85a3319eaeeac5da8b5a0340678 100644 GIT binary patch delta 208 zcmeBF->=T6&Bx2d00g%G{$|E-Y~*8PWQ>_Cz^H5y#8|?v$56rn#GF7j7m(%-Vk+SQ zVqPGh4~Y507>pT%n8g`#I7$S9SV{!L7)pdDr!(p@9b=rlpIvV97DnUApBY6qzh->M z#3(WOEQ`_P*<4DKWmwNM3QvB+YRxDxS($CJm>1CgAMFec3=gE_8eAU8DmJ*@P}ACc xl1-MK=^n%+Ha`2wfqWv96F4|F2lCl6F&a+p7qH_HndsVUf00FM^96yOOaL#SIFSGV delta 202 zcmdn5-l@)~&Bx2d00bNr-!k8_Z{%ZSWQ>?Bz^H7Z$56r!#2i74C7eK-D~PFt8;E&; zd|n{t3u7>53}O~%$l)mA4`L}12xBM_oSe<5%e0Mg@&k6c$vYU08JRZ!VSK{GC_MQg zixIP6qt9e%)|ZSzlUdoUC%dxs3%djD{n5_Q!0`doiM(|8F o<}+qxwq^=qogB|6J$Wt%=jQo*3QUYTlMe{kfsEYDCb*Lc04K3CfdBvi diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py index 7e2029a..98d9ea4 100644 --- a/backend/app/schemas/product.py +++ b/backend/app/schemas/product.py @@ -8,7 +8,7 @@ class ProductCreate(BaseModel): name: str slug: Optional[str] = None description: str - price: float + price: Optional[float] = None # Optional - inherits from model if not set discount_price: Optional[float] = None category_id: int model_id: Optional[int] = None @@ -49,7 +49,7 @@ class ProductResponse(BaseModel): name: str slug: Optional[str] description: str - price: float + price: Optional[float] # May be None if inherited from model discount_price: Optional[float] category_id: int model_id: Optional[int] diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index cc844ef..4b342ec 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -209,7 +209,7 @@ export default function Admin() { const productData = { ...formData, - price: parseFloat(formData.price), + price: formData.price ? parseFloat(formData.price) : null, // Allow null - inherits from model discount_price: formData.discount_price ? parseFloat(formData.discount_price) : null, category_id: parseInt(formData.category_id), model_id: formData.model_id ? parseInt(formData.model_id) : null, @@ -821,8 +821,27 @@ export default function Admin() {
- - + + m.id === parseInt(formData.model_id))?.base_price} + placeholder={ + formData.model_id && models.find(m => m.id === parseInt(formData.model_id))?.base_price + ? `Model price: ₪${parseFloat(models.find(m => m.id === parseInt(formData.model_id)).base_price).toFixed(2)}` + : 'Enter price' + } + /> + {formData.model_id && models.find(m => m.id === parseInt(formData.model_id))?.base_price && ( + + Leave empty to use model's base price of ₪{parseFloat(models.find(m => m.id === parseInt(formData.model_id)).base_price).toFixed(2)} + + )}