Upload 6 files
Browse files
main.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# main.py
|
| 2 |
"""
|
| 3 |
Mini Invoice/Estimate (Quote) SaaS — single-file FastAPI app
|
| 4 |
SQLite + SQLModel + FastAPI
|
|
@@ -13,8 +12,8 @@ import pathlib
|
|
| 13 |
from fastapi import FastAPI, Depends, HTTPException, Header, Query
|
| 14 |
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
from fastapi.openapi.utils import get_openapi
|
| 16 |
-
from fastapi.responses import FileResponse
|
| 17 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
| 18 |
from pydantic import BaseModel
|
| 19 |
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select, Relationship
|
| 20 |
|
|
@@ -159,8 +158,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):
|
| 166 |
description: str
|
|
@@ -246,7 +244,20 @@ def on_startup():
|
|
| 246 |
SQLModel.metadata.create_all(engine)
|
| 247 |
|
| 248 |
# ---- UI(/app) ----
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
# -------- Customers --------
|
| 252 |
@app.post("/customers", dependencies=[Depends(require_api_key)])
|
|
@@ -264,16 +275,17 @@ def list_customers(
|
|
| 264 |
stmt = select(Customer)
|
| 265 |
if q:
|
| 266 |
like = f"%{q}%"
|
|
|
|
| 267 |
stmt = stmt.where(
|
| 268 |
-
(Customer.name.
|
| 269 |
-
(Customer.email.
|
| 270 |
-
(Customer.phone.
|
| 271 |
)
|
| 272 |
total = session.exec(stmt).count()
|
| 273 |
rows = session.exec(stmt.offset(offset).limit(limit)).all()
|
| 274 |
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
| 275 |
|
| 276 |
-
# -------- Products
|
| 277 |
@app.post("/products", dependencies=[Depends(require_api_key)])
|
| 278 |
def create_product(payload: Product, session: Session = Depends(get_session)):
|
| 279 |
session.add(payload); session.commit(); session.refresh(payload)
|
|
@@ -324,28 +336,27 @@ def add_quote_item(quote_id: int, payload: QuoteItemIn, session: Session = Depen
|
|
| 324 |
# -------- Invoices --------
|
| 325 |
@app.post("/invoices", dependencies=[Depends(require_api_key)])
|
| 326 |
def create_invoice(payload: CreateInvoiceIn, session: Session = Depends(get_session)):
|
| 327 |
-
# 見積→請求のコピーにも対応(quote_id を受け付ける)
|
| 328 |
if not session.get(Customer, payload.customer_id):
|
| 329 |
raise HTTPException(400, "Customer not found")
|
| 330 |
|
| 331 |
inv = Invoice(customer_id=payload.customer_id, due_date=payload.due_date, notes=payload.notes)
|
| 332 |
session.add(inv); session.commit(); session.refresh(inv)
|
| 333 |
|
|
|
|
| 334 |
if payload.quote_id:
|
| 335 |
q = session.get(Quote, payload.quote_id)
|
| 336 |
if not q:
|
| 337 |
raise HTTPException(404, "Quote not found to copy")
|
| 338 |
q_items = session.exec(select(QuoteItem).where(QuoteItem.quote_id == payload.quote_id)).all()
|
| 339 |
for it in q_items:
|
| 340 |
-
|
| 341 |
invoice_id=inv.id,
|
| 342 |
product_id=it.product_id,
|
| 343 |
description=it.description,
|
| 344 |
quantity=it.quantity,
|
| 345 |
unit_price=it.unit_price,
|
| 346 |
tax_rate=it.tax_rate,
|
| 347 |
-
)
|
| 348 |
-
session.add(new_item)
|
| 349 |
session.commit()
|
| 350 |
|
| 351 |
session.refresh(inv)
|
|
@@ -427,9 +438,10 @@ def email_invoice(invoice_id: int, payload: EmailIn, session: Session = Depends(
|
|
| 427 |
)
|
| 428 |
return {"ok": ok, "detail": detail}
|
| 429 |
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
| 433 |
|
| 434 |
# -------- ChatGPT router を最後に組み込む --------
|
| 435 |
from openai_integration import router as ai_router
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
Mini Invoice/Estimate (Quote) SaaS — single-file FastAPI app
|
| 3 |
SQLite + SQLModel + FastAPI
|
|
|
|
| 12 |
from fastapi import FastAPI, Depends, HTTPException, Header, Query
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
from fastapi.openapi.utils import get_openapi
|
|
|
|
| 15 |
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
| 17 |
from pydantic import BaseModel
|
| 18 |
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select, Relationship
|
| 19 |
|
|
|
|
| 158 |
customer_id: int
|
| 159 |
due_date: Optional[date] = None
|
| 160 |
notes: Optional[str] = None
|
| 161 |
+
quote_id: Optional[int] = None # 見積→請求コピー
|
|
|
|
| 162 |
|
| 163 |
class InvoiceItemIn(BaseModel):
|
| 164 |
description: str
|
|
|
|
| 244 |
SQLModel.metadata.create_all(engine)
|
| 245 |
|
| 246 |
# ---- UI(/app) ----
|
| 247 |
+
STATIC_DIR = "static"
|
| 248 |
+
if os.path.isdir(STATIC_DIR):
|
| 249 |
+
app.mount("/app", StaticFiles(directory=STATIC_DIR, html=True), name="app")
|
| 250 |
+
else:
|
| 251 |
+
# フォールバック(staticが無い場合でも起動できるようにする)
|
| 252 |
+
@app.get("/app", response_class=HTMLResponse)
|
| 253 |
+
def app_inline():
|
| 254 |
+
return """
|
| 255 |
+
<!doctype html><meta charset="utf-8">
|
| 256 |
+
<title>Mini Invoice/Estimate SaaS</title>
|
| 257 |
+
<h1>Mini Invoice/Estimate SaaS</h1>
|
| 258 |
+
<p><code>static/app.html</code> が見つかりません。Filesで作成してから再読み込みしてください。</p>
|
| 259 |
+
<p>APIは <a href="/docs">/docs</a> から操作できます。</p>
|
| 260 |
+
"""
|
| 261 |
|
| 262 |
# -------- Customers --------
|
| 263 |
@app.post("/customers", dependencies=[Depends(require_api_key)])
|
|
|
|
| 275 |
stmt = select(Customer)
|
| 276 |
if q:
|
| 277 |
like = f"%{q}%"
|
| 278 |
+
# SQLite は ILIKE 非対応なので LIKE で十分(大文字小文字は基本無視コラテラル)
|
| 279 |
stmt = stmt.where(
|
| 280 |
+
(Customer.name.like(like)) |
|
| 281 |
+
(Customer.email.like(like)) |
|
| 282 |
+
(Customer.phone.like(like))
|
| 283 |
)
|
| 284 |
total = session.exec(stmt).count()
|
| 285 |
rows = session.exec(stmt.offset(offset).limit(limit)).all()
|
| 286 |
return {"data": rows, "pagination": {"total": total, "limit": limit, "offset": offset}}
|
| 287 |
|
| 288 |
+
# -------- Products --------
|
| 289 |
@app.post("/products", dependencies=[Depends(require_api_key)])
|
| 290 |
def create_product(payload: Product, session: Session = Depends(get_session)):
|
| 291 |
session.add(payload); session.commit(); session.refresh(payload)
|
|
|
|
| 336 |
# -------- Invoices --------
|
| 337 |
@app.post("/invoices", dependencies=[Depends(require_api_key)])
|
| 338 |
def create_invoice(payload: CreateInvoiceIn, session: Session = Depends(get_session)):
|
|
|
|
| 339 |
if not session.get(Customer, payload.customer_id):
|
| 340 |
raise HTTPException(400, "Customer not found")
|
| 341 |
|
| 342 |
inv = Invoice(customer_id=payload.customer_id, due_date=payload.due_date, notes=payload.notes)
|
| 343 |
session.add(inv); session.commit(); session.refresh(inv)
|
| 344 |
|
| 345 |
+
# 見積→請求のコピー
|
| 346 |
if payload.quote_id:
|
| 347 |
q = session.get(Quote, payload.quote_id)
|
| 348 |
if not q:
|
| 349 |
raise HTTPException(404, "Quote not found to copy")
|
| 350 |
q_items = session.exec(select(QuoteItem).where(QuoteItem.quote_id == payload.quote_id)).all()
|
| 351 |
for it in q_items:
|
| 352 |
+
session.add(InvoiceItem(
|
| 353 |
invoice_id=inv.id,
|
| 354 |
product_id=it.product_id,
|
| 355 |
description=it.description,
|
| 356 |
quantity=it.quantity,
|
| 357 |
unit_price=it.unit_price,
|
| 358 |
tax_rate=it.tax_rate,
|
| 359 |
+
))
|
|
|
|
| 360 |
session.commit()
|
| 361 |
|
| 362 |
session.refresh(inv)
|
|
|
|
| 438 |
)
|
| 439 |
return {"ok": ok, "detail": detail}
|
| 440 |
|
| 441 |
+
# ---- ルート:UIへリダイレクト(デモしやすさ優先)----
|
| 442 |
+
@app.get("/", include_in_schema=False)
|
| 443 |
+
def root_redirect():
|
| 444 |
+
return RedirectResponse("/app")
|
| 445 |
|
| 446 |
# -------- ChatGPT router を最後に組み込む --------
|
| 447 |
from openai_integration import router as ai_router
|