Upload main.py
Browse files
main.py
CHANGED
|
@@ -17,6 +17,7 @@ from fastapi.staticfiles import StaticFiles
|
|
| 17 |
from pydantic import BaseModel, Field, model_validator
|
| 18 |
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select, Relationship
|
| 19 |
from sqlalchemy import func
|
|
|
|
| 20 |
|
| 21 |
# --------------------------
|
| 22 |
# Auth
|
|
@@ -64,6 +65,7 @@ engine = create_engine(
|
|
| 64 |
echo=False,
|
| 65 |
connect_args={"check_same_thread": False} if DB_URL.startswith("sqlite") else {}
|
| 66 |
)
|
|
|
|
| 67 |
|
| 68 |
def get_session():
|
| 69 |
with Session(engine) as session:
|
|
@@ -122,7 +124,7 @@ class Invoice(SQLModel, table=True):
|
|
| 122 |
due_date: Optional[date] = None
|
| 123 |
notes: Optional[str] = None
|
| 124 |
|
| 125 |
-
#
|
| 126 |
paid_at: Optional[datetime] = None
|
| 127 |
paid_amount: Optional[float] = None
|
| 128 |
payment_method: Optional[str] = None
|
|
@@ -159,7 +161,7 @@ class CreateInvoiceIn(BaseModel):
|
|
| 159 |
customer_id: int
|
| 160 |
due_date: Optional[date] = None
|
| 161 |
notes: Optional[str] = None
|
| 162 |
-
#
|
| 163 |
quote_id: Optional[int] = None
|
| 164 |
|
| 165 |
class InvoiceItemIn(BaseModel):
|
|
@@ -214,7 +216,7 @@ class WizardInvoiceIn(BaseModel):
|
|
| 214 |
return v
|
| 215 |
|
| 216 |
# --------------------------
|
| 217 |
-
# Totals
|
| 218 |
# --------------------------
|
| 219 |
class MoneyBreakdown(BaseModel):
|
| 220 |
subtotal: float
|
|
@@ -251,7 +253,7 @@ app.add_middleware(
|
|
| 251 |
allow_methods=["*"], allow_headers=["*"],
|
| 252 |
)
|
| 253 |
|
| 254 |
-
# Swagger の Authorize
|
| 255 |
def custom_openapi():
|
| 256 |
if app.openapi_schema:
|
| 257 |
return app.openapi_schema
|
|
@@ -285,8 +287,20 @@ app.mount("/app", StaticFiles(directory="static", html=True), name="app")
|
|
| 285 |
# -------- Customers --------
|
| 286 |
@app.post("/customers", dependencies=[Depends(require_api_key)])
|
| 287 |
def create_customer(payload: Customer, session: Session = Depends(get_session)):
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
@app.get("/customers", dependencies=[Depends(require_api_key)])
|
| 292 |
def list_customers(
|
|
@@ -299,23 +313,43 @@ def list_customers(
|
|
| 299 |
count_stmt = select(func.count(Customer.id))
|
| 300 |
|
| 301 |
if q:
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
base = base.where(cond)
|
| 309 |
count_stmt = count_stmt.where(cond)
|
| 310 |
|
| 311 |
total = session.exec(count_stmt).scalar() or 0
|
| 312 |
rows = session.exec(base.offset(offset).limit(limit)).all()
|
| 313 |
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
|
|
|
| 314 |
# -------- Products --------
|
| 315 |
@app.post("/products", dependencies=[Depends(require_api_key)])
|
| 316 |
def create_product(payload: Product, session: Session = Depends(get_session)):
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
@app.get("/products", dependencies=[Depends(require_api_key)])
|
| 321 |
def list_products(
|
|
@@ -369,21 +403,21 @@ def create_invoice(payload: CreateInvoiceIn, session: Session = Depends(get_sess
|
|
| 369 |
inv = Invoice(customer_id=payload.customer_id, due_date=payload.due_date, notes=payload.notes)
|
| 370 |
session.add(inv); session.commit(); session.refresh(inv)
|
| 371 |
|
| 372 |
-
# 見積→請求コピー
|
| 373 |
if payload.quote_id:
|
| 374 |
q = session.get(Quote, payload.quote_id)
|
| 375 |
if not q:
|
| 376 |
raise HTTPException(404, "Quote not found to copy")
|
| 377 |
q_items = session.exec(select(QuoteItem).where(QuoteItem.quote_id == payload.quote_id)).all()
|
| 378 |
for it in q_items:
|
| 379 |
-
|
| 380 |
invoice_id=inv.id,
|
| 381 |
product_id=it.product_id,
|
| 382 |
description=it.description,
|
| 383 |
quantity=it.quantity,
|
| 384 |
unit_price=it.unit_price,
|
| 385 |
tax_rate=it.tax_rate,
|
| 386 |
-
)
|
|
|
|
| 387 |
session.commit()
|
| 388 |
|
| 389 |
session.refresh(inv)
|
|
@@ -514,10 +548,10 @@ def wizard_create_invoice(payload: WizardInvoiceIn, session: Session = Depends(g
|
|
| 514 |
except Exception as e:
|
| 515 |
raise HTTPException(400, f"Wizard failed: {e}")
|
| 516 |
|
| 517 |
-
# ---- ルート(UI
|
| 518 |
-
@app.get("/")
|
| 519 |
-
def
|
| 520 |
-
return
|
| 521 |
|
| 522 |
# -------- ChatGPT router(最後に組み込む) --------
|
| 523 |
from openai_integration import router as ai_router
|
|
|
|
| 17 |
from pydantic import BaseModel, Field, model_validator
|
| 18 |
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select, Relationship
|
| 19 |
from sqlalchemy import func
|
| 20 |
+
from sqlalchemy.exc import SQLAlchemyError
|
| 21 |
|
| 22 |
# --------------------------
|
| 23 |
# Auth
|
|
|
|
| 65 |
echo=False,
|
| 66 |
connect_args={"check_same_thread": False} if DB_URL.startswith("sqlite") else {}
|
| 67 |
)
|
| 68 |
+
IS_SQLITE = DB_URL.startswith("sqlite")
|
| 69 |
|
| 70 |
def get_session():
|
| 71 |
with Session(engine) as session:
|
|
|
|
| 124 |
due_date: Optional[date] = None
|
| 125 |
notes: Optional[str] = None
|
| 126 |
|
| 127 |
+
# 入金関連(任意)
|
| 128 |
paid_at: Optional[datetime] = None
|
| 129 |
paid_amount: Optional[float] = None
|
| 130 |
payment_method: Optional[str] = None
|
|
|
|
| 161 |
customer_id: int
|
| 162 |
due_date: Optional[date] = None
|
| 163 |
notes: Optional[str] = None
|
| 164 |
+
# 見積→請求のコピー用(任意)
|
| 165 |
quote_id: Optional[int] = None
|
| 166 |
|
| 167 |
class InvoiceItemIn(BaseModel):
|
|
|
|
| 216 |
return v
|
| 217 |
|
| 218 |
# --------------------------
|
| 219 |
+
# Totals(堅牢化)
|
| 220 |
# --------------------------
|
| 221 |
class MoneyBreakdown(BaseModel):
|
| 222 |
subtotal: float
|
|
|
|
| 253 |
allow_methods=["*"], allow_headers=["*"],
|
| 254 |
)
|
| 255 |
|
| 256 |
+
# Swagger の Authorize(X-API-Key)を出す
|
| 257 |
def custom_openapi():
|
| 258 |
if app.openapi_schema:
|
| 259 |
return app.openapi_schema
|
|
|
|
| 287 |
# -------- Customers --------
|
| 288 |
@app.post("/customers", dependencies=[Depends(require_api_key)])
|
| 289 |
def create_customer(payload: Customer, session: Session = Depends(get_session)):
|
| 290 |
+
try:
|
| 291 |
+
if not (payload.name and str(payload.name).strip()):
|
| 292 |
+
raise HTTPException(422, "name は必須です")
|
| 293 |
+
session.add(payload)
|
| 294 |
+
session.commit()
|
| 295 |
+
session.refresh(payload)
|
| 296 |
+
return payload
|
| 297 |
+
except HTTPException:
|
| 298 |
+
raise
|
| 299 |
+
except SQLAlchemyError as e:
|
| 300 |
+
session.rollback()
|
| 301 |
+
raise HTTPException(400, f"DBエラー: {e.__class__.__name__}")
|
| 302 |
+
except Exception as e:
|
| 303 |
+
raise HTTPException(400, f"不正なリクエスト: {e}")
|
| 304 |
|
| 305 |
@app.get("/customers", dependencies=[Depends(require_api_key)])
|
| 306 |
def list_customers(
|
|
|
|
| 313 |
count_stmt = select(func.count(Customer.id))
|
| 314 |
|
| 315 |
if q:
|
| 316 |
+
pattern = f"%{q}%"
|
| 317 |
+
if IS_SQLITE:
|
| 318 |
+
cond = (
|
| 319 |
+
Customer.name.like(pattern) |
|
| 320 |
+
Customer.email.like(pattern) |
|
| 321 |
+
Customer.phone.like(pattern)
|
| 322 |
+
)
|
| 323 |
+
else:
|
| 324 |
+
cond = (
|
| 325 |
+
Customer.name.ilike(pattern) |
|
| 326 |
+
Customer.email.ilike(pattern) |
|
| 327 |
+
Customer.phone.ilike(pattern)
|
| 328 |
+
)
|
| 329 |
base = base.where(cond)
|
| 330 |
count_stmt = count_stmt.where(cond)
|
| 331 |
|
| 332 |
total = session.exec(count_stmt).scalar() or 0
|
| 333 |
rows = session.exec(base.offset(offset).limit(limit)).all()
|
| 334 |
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
| 335 |
+
|
| 336 |
# -------- Products --------
|
| 337 |
@app.post("/products", dependencies=[Depends(require_api_key)])
|
| 338 |
def create_product(payload: Product, session: Session = Depends(get_session)):
|
| 339 |
+
try:
|
| 340 |
+
if not (payload.name and str(payload.name).strip()):
|
| 341 |
+
raise HTTPException(422, "name は必須です")
|
| 342 |
+
session.add(payload)
|
| 343 |
+
session.commit()
|
| 344 |
+
session.refresh(payload)
|
| 345 |
+
return payload
|
| 346 |
+
except HTTPException:
|
| 347 |
+
raise
|
| 348 |
+
except SQLAlchemyError as e:
|
| 349 |
+
session.rollback()
|
| 350 |
+
raise HTTPException(400, f"DBエラー: {e.__class__.__name__}")
|
| 351 |
+
except Exception as e:
|
| 352 |
+
raise HTTPException(400, f"不正なリクエスト: {e}")
|
| 353 |
|
| 354 |
@app.get("/products", dependencies=[Depends(require_api_key)])
|
| 355 |
def list_products(
|
|
|
|
| 403 |
inv = Invoice(customer_id=payload.customer_id, due_date=payload.due_date, notes=payload.notes)
|
| 404 |
session.add(inv); session.commit(); session.refresh(inv)
|
| 405 |
|
|
|
|
| 406 |
if payload.quote_id:
|
| 407 |
q = session.get(Quote, payload.quote_id)
|
| 408 |
if not q:
|
| 409 |
raise HTTPException(404, "Quote not found to copy")
|
| 410 |
q_items = session.exec(select(QuoteItem).where(QuoteItem.quote_id == payload.quote_id)).all()
|
| 411 |
for it in q_items:
|
| 412 |
+
new_item = InvoiceItem(
|
| 413 |
invoice_id=inv.id,
|
| 414 |
product_id=it.product_id,
|
| 415 |
description=it.description,
|
| 416 |
quantity=it.quantity,
|
| 417 |
unit_price=it.unit_price,
|
| 418 |
tax_rate=it.tax_rate,
|
| 419 |
+
)
|
| 420 |
+
session.add(new_item)
|
| 421 |
session.commit()
|
| 422 |
|
| 423 |
session.refresh(inv)
|
|
|
|
| 548 |
except Exception as e:
|
| 549 |
raise HTTPException(400, f"Wizard failed: {e}")
|
| 550 |
|
| 551 |
+
# ---- ルート(UI) ----
|
| 552 |
+
@app.get("/", response_class=FileResponse)
|
| 553 |
+
def root_ui():
|
| 554 |
+
return FileResponse("static/app.html")
|
| 555 |
|
| 556 |
# -------- ChatGPT router(最後に組み込む) --------
|
| 557 |
from openai_integration import router as ai_router
|