File size: 17,000 Bytes
cd9bca9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional, Union
from datetime import datetime, date
import enum

# Import Enum types from models.py to be used in Pydantic schemas
# This avoids redefining them and ensures consistency.
# We might need to adjust models.py if enums are defined in a way that's hard to import directly
# For now, let's assume we can import them or we'll redefine for Pydantic if necessary.
# For simplicity in this step, I'll redefine them here. If issues arise, we can refactor.

class UserRole(str, enum.Enum):
    ADMIN = "ADMIN"
    USER = "USER"

class TransactionType(str, enum.Enum):
    PURCHASE = "PURCHASE"
    SELL = "SELL"

class TransactionStatus(str, enum.Enum):
    PENDING = "PENDING"
    PROCESSING = "PROCESSING" # Added PROCESSING status
    COMPLETED = "COMPLETED"
    CANCELLED = "CANCELLED"

# CustomerType 和 PaymentCategory 改為字串類型,支援動態值
# PaymentMethod 改為支援動態字串,如 "月結30天", "下收" 等

# Base and Read schemas for Category
class CategoryBase(BaseModel):
    name: str

class CategoryCreate(CategoryBase):
    pass

class Category(CategoryBase): # For Read operations
    id: int

    class Config:
        from_attributes = True

# Base and Read schemas for Product - 根據客戶 Excel 欄位重新設計
class ProductBase(BaseModel):
    productCode: str = Field(..., description="貨品編號,必須唯一")
    productName: str = Field(..., description="貨品名稱")
    unit: str = Field(..., description="單位(箱、盒等)")
    warehouse: Optional[str] = None  # 倉別
    unitWeight: Optional[float] = None  # 單位重量(KG)
    barcode: Optional[str] = None  # 條碼編號
    category_id: int = Field(..., description="類別ID")

class ProductCreate(ProductBase):
    pass

# For Read operations
class Product(ProductBase):
    id: int
    stock: int = Field(default=0, description="庫存數量")  # 添加庫存欄位
    is_deleted: bool = Field(default=False, description="是否已刪除")
    deleted_at: Optional[datetime] = None
    deleted_by: Optional[int] = None
    createdAt: datetime
    updatedAt: Optional[datetime] = None
    category: Optional[Category] = None  # 包含類別詳細資訊

    class Config:
        from_attributes = True

# Schemas for updating a product (all fields optional)
class ProductUpdate(BaseModel):
    productCode: Optional[str] = None
    productName: Optional[str] = None
    unit: Optional[str] = None
    warehouse: Optional[str] = None
    unitWeight: Optional[float] = None
    barcode: Optional[str] = None
    category_id: Optional[int] = None
    stock: Optional[int] = Field(None, ge=0, description="庫存數量,必須大於等於0")  # 添加庫存更新


# Base and Read schemas for Supplier
class SupplierBase(BaseModel):
    name: str
    contactInfo: Optional[str] = None
    address: Optional[str] = None

class SupplierCreate(SupplierBase):
    pass

class Supplier(SupplierBase): # For Read operations
    id: int

    class Config:
        from_attributes = True

# Base and Read schemas for Customer
class CustomerBase(BaseModel):
    customerType: str
    salesPersonId: Optional[str] = None
    salesPersonName: Optional[str] = None
    customerCode: str = Field(..., description="客戶編號,必須唯一")
    customerName: str = Field(..., description="客戶名稱")
    contactPerson: Optional[str] = None
    invoiceTitle: Optional[str] = None
    taxId: Optional[str] = None
    phoneNumber: Optional[str] = None
    faxNumber: Optional[str] = None
    deliveryAddress: Optional[str] = None
    businessHours: Optional[str] = None
    paymentMethod: Optional[str] = None
    paymentCategory: Optional[str] = None
    creditLimit: Optional[float] = 0.0

class CustomerCreate(CustomerBase):
    pass

class CustomerUpdate(BaseModel):
    customerType: Optional[str] = None
    salesPersonId: Optional[str] = None
    salesPersonName: Optional[str] = None
    customerCode: Optional[str] = None
    customerName: Optional[str] = None
    contactPerson: Optional[str] = None
    invoiceTitle: Optional[str] = None
    taxId: Optional[str] = None
    phoneNumber: Optional[str] = None
    faxNumber: Optional[str] = None
    deliveryAddress: Optional[str] = None
    businessHours: Optional[str] = None
    paymentMethod: Optional[str] = None
    paymentCategory: Optional[str] = None
    creditLimit: Optional[float] = None

class Customer(CustomerBase): # For Read operations
    id: int
    createdDate: datetime
    updatedAt: Optional[datetime] = None

    class Config:
        from_attributes = True

# Base and Read schemas for User
class UserBase(BaseModel):
    name: Optional[str] = None
    email: EmailStr
    phoneNumber: Optional[str] = None
    role: Optional[UserRole] = UserRole.USER

class UserCreate(UserBase):
    password: str = Field(..., min_length=4) # Example: make password required on create

class UserUpdate(BaseModel): # For updating user profile
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    phoneNumber: Optional[str] = None
    role: Optional[UserRole] = None
    # Role and password updates might be handled by separate, more secure endpoints or admin functions

class User(UserBase): # For Read operations (e.g., /users/current)
    id: int
    createdAt: datetime
    # Do not include password in responses

    class Config:
        from_attributes = True


# Schemas for TransactionProductAssociation (enhanced with pricing information)
class TransactionProductAssociationBase(BaseModel):
    product_id: int
    quantity: Optional[int] = 1
    unit_price: Optional[float] = None
    line_total: Optional[float] = None
    notes: Optional[str] = None

class TransactionProductAssociationCreate(TransactionProductAssociationBase):
    pass

class TransactionProductAssociation(TransactionProductAssociationBase): # For Read
    # Include product details for complete information
    product: Optional[Product] = None

    class Config:
        from_attributes = True


# Base and Read schemas for Transaction
class TransactionBase(BaseModel):
    totalProducts: int
    totalPrice: float
    transactionType: TransactionType
    transactionStatus: Optional[TransactionStatus] = TransactionStatus.PENDING
    description: Optional[str] = None
    note: Optional[str] = None
    user_id: Optional[int] = None # Assuming user_id is set based on authenticated user
    supplier_id: Optional[int] = None
    customer_id: Optional[int] = None # For sell transactions
    # For creating transactions, the frontend might send a list of products involved
    # This needs to align with how api.service.ts sends data for purchase/sell
    # For example: products_involved: List[TransactionProductAssociationCreate]

class TransactionCreate(TransactionBase):
    # The frontend's purchaseProduct/sellProduct takes a 'body'. We need to match that structure.
    # If 'body' contains a list of product IDs and quantities:
    products_involved: List[TransactionProductAssociationCreate]


class Transaction(TransactionBase): # For Read operations
    id: int
    createdAt: datetime
    updatedAt: Optional[datetime] = None
    user: Optional[User] = None # Nested user details
    supplier: Optional[Supplier] = None # Nested supplier details
    customer: Optional[Customer] = None # Nested customer details for sell transactions
    products: List[TransactionProductAssociation] # List of products involved in the transaction

    class Config:
        from_attributes = True

# Schema for updating transaction status (as per frontend api.service.ts)
class TransactionStatusUpdate(BaseModel):
    status: TransactionStatus # Frontend sends JSON.stringify(status) - need to ensure this matches

# Schemas for Authentication
class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    email: Optional[str] = None

class UserLogin(BaseModel):
    email: EmailStr
    password: str


# 採購單相關的枚舉類型
class PurchaseOrderStatus(str, enum.Enum):
    DRAFT = "DRAFT"           # 草稿
    PENDING = "PENDING"       # 待處理
    CONFIRMED = "CONFIRMED"   # 已確認
    RECEIVED = "RECEIVED"     # 已收貨
    CANCELLED = "CANCELLED"   # 已取消

class PaymentStatus(str, enum.Enum):
    UNPAID = "UNPAID"         # 未付款
    PARTIAL = "PARTIAL"       # 部分付款
    PAID = "PAID"             # 已付款

class TaxType(str, enum.Enum):
    INCLUSIVE = "INCLUSIVE"   # 含稅
    EXCLUSIVE = "EXCLUSIVE"   # 未稅
    ADDITIONAL = "ADDITIONAL" # 外加稅

# 入庫單相關的枚舉類型
class GoodsReceiptStatus(str, enum.Enum):
    DRAFT = "DRAFT"           # 草稿
    PENDING = "PENDING"       # 待處理
    COMPLETED = "COMPLETED"   # 已完成
    CANCELLED = "CANCELLED"   # 已取消

class WarehouseType(str, enum.Enum):
    MAIN = "MAIN"             # 主倉
    RAW_MATERIAL = "RAW_MATERIAL"  # 原料倉
    FINISHED_GOODS = "FINISHED_GOODS"  # 成品倉
    QUARANTINE = "QUARANTINE" # 檢疫倉
    DAMAGED = "DAMAGED"       # 損壞品倉

# 銷售單相關的枚舉類型
class SalesOrderStatus(str, enum.Enum):
    DRAFT = "DRAFT"           # 草稿
    CONFIRMED = "CONFIRMED"   # 已確認
    SHIPPED = "SHIPPED"       # 已出貨
    DELIVERED = "DELIVERED"   # 已送達
    CANCELLED = "CANCELLED"   # 已取消

class PaymentTerm(str, enum.Enum):
    CASH = "CASH"             # 現金
    MONTHLY = "MONTHLY"       # 月結
    TRANSFER = "TRANSFER"     # 轉帳
    CREDIT_CARD = "CREDIT_CARD"  # 信用卡
    CHECK = "CHECK"           # 支票


# 採購明細項目 schemas
class PurchaseOrderItemBase(BaseModel):
    product_id: int = Field(..., description="產品ID")
    quantity: int = Field(..., gt=0, description="數量,必須大於0")
    unit_price: float = Field(..., ge=0, description="單價,必須大於等於0")
    notes: Optional[str] = None

class PurchaseOrderItemCreate(PurchaseOrderItemBase):
    pass

class PurchaseOrderItemUpdate(BaseModel):
    product_id: Optional[int] = None
    quantity: Optional[int] = Field(None, gt=0)
    unit_price: Optional[float] = Field(None, ge=0)
    notes: Optional[str] = None

class PurchaseOrderItem(PurchaseOrderItemBase):
    id: int
    line_total: float  # 小計 (數量 × 單價)
    product: Optional[Product] = None  # 包含產品詳細資訊
    created_at: datetime
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


# 採購單主檔 schemas
class PurchaseOrderBase(BaseModel):
    purchase_date: date = Field(..., description="採購日期")
    expected_delivery_date: Optional[date] = None
    supplier_id: int = Field(..., description="供應商ID")
    notes: Optional[str] = None
    tax_type: TaxType = TaxType.INCLUSIVE
    tax_rate: float = Field(0.05, ge=0, le=1, description="稅率,0-1之間")
    payment_method: Optional[str] = None

class PurchaseOrderCreate(PurchaseOrderBase):
    items: List[PurchaseOrderItemCreate] = Field(..., min_items=1, description="採購明細,至少要有一項")

class PurchaseOrderUpdate(BaseModel):
    purchase_date: Optional[date] = None
    expected_delivery_date: Optional[date] = None
    supplier_id: Optional[int] = None
    status: Optional[PurchaseOrderStatus] = None
    notes: Optional[str] = None
    tax_type: Optional[TaxType] = None
    tax_rate: Optional[float] = Field(None, ge=0, le=1)
    payment_method: Optional[str] = None
    payment_status: Optional[PaymentStatus] = None

class PurchaseOrder(PurchaseOrderBase):
    id: int
    po_number: str  # 採購單號
    purchaser_id: int
    status: PurchaseOrderStatus
    subtotal: float  # 小計
    tax_amount: float  # 稅額
    total_amount: float  # 含稅總額
    payment_status: PaymentStatus
    created_at: datetime
    updated_at: Optional[datetime] = None

    # 關聯資料
    purchaser: Optional[User] = None
    supplier: Optional[Supplier] = None
    items: List[PurchaseOrderItem] = []

    class Config:
        from_attributes = True


# 入庫明細項目 schemas
class GoodsReceiptItemBase(BaseModel):
    purchase_order_item_id: int = Field(..., description="採購明細ID")
    product_id: int = Field(..., description="產品ID")
    ordered_quantity: int = Field(..., gt=0, description="採購數量")
    received_quantity: int = Field(..., ge=0, description="實到數量,必須大於等於0")
    storage_location: Optional[str] = None
    notes: Optional[str] = None

class GoodsReceiptItemCreate(GoodsReceiptItemBase):
    pass

class GoodsReceiptItemUpdate(BaseModel):
    purchase_order_item_id: Optional[int] = None  # 需要用來識別明細項目
    product_id: Optional[int] = None              # 需要用來識別產品
    received_quantity: Optional[int] = Field(None, ge=0)
    storage_location: Optional[str] = None
    notes: Optional[str] = None

class GoodsReceiptItem(GoodsReceiptItemBase):
    id: int
    product: Optional[Product] = None  # 包含產品詳細資訊
    purchase_order_item: Optional[PurchaseOrderItem] = None  # 包含採購明細資訊
    created_at: datetime
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


# 入庫單主檔 schemas
class GoodsReceiptBase(BaseModel):
    receipt_date: date = Field(..., description="入庫日期")
    purchase_order_id: int = Field(..., description="採購單ID")
    warehouse_type: WarehouseType = WarehouseType.MAIN
    warehouse_location: Optional[str] = None
    notes: Optional[str] = None

class GoodsReceiptCreate(GoodsReceiptBase):
    items: List[GoodsReceiptItemCreate] = Field(..., min_items=1, description="入庫明細,至少要有一項")

class GoodsReceiptUpdate(BaseModel):
    receipt_date: Optional[date] = None
    warehouse_type: Optional[WarehouseType] = None
    warehouse_location: Optional[str] = None
    status: Optional[GoodsReceiptStatus] = None
    notes: Optional[str] = None
    items: Optional[List[GoodsReceiptItemUpdate]] = None  # 新增入庫明細更新

class GoodsReceipt(GoodsReceiptBase):
    id: int
    gr_number: str  # 入庫單號
    warehouse_staff_id: int
    status: GoodsReceiptStatus
    created_at: datetime
    updated_at: Optional[datetime] = None

    # 關聯資料
    warehouse_staff: Optional[User] = None
    purchase_order: Optional[PurchaseOrder] = None
    items: List[GoodsReceiptItem] = []

    class Config:
        from_attributes = True


# 銷售明細項目 schemas
class SalesOrderItemBase(BaseModel):
    product_id: int = Field(..., description="產品ID")
    quantity: int = Field(..., gt=0, description="數量,必須大於0")
    unit_price: float = Field(..., ge=0, description="單價,必須大於等於0")
    notes: Optional[str] = None

class SalesOrderItemCreate(SalesOrderItemBase):
    pass

class SalesOrderItemUpdate(BaseModel):
    product_id: Optional[int] = None
    quantity: Optional[int] = Field(None, gt=0)
    unit_price: Optional[float] = Field(None, ge=0)
    notes: Optional[str] = None

class SalesOrderItem(SalesOrderItemBase):
    id: int
    line_total: float  # 小計 (數量 × 單價)
    product: Optional[Product] = None  # 包含產品詳細資訊
    created_at: datetime
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


# 銷售單主檔 schemas
class SalesOrderBase(BaseModel):
    sales_date: date = Field(..., description="銷售日期")
    customer_id: int = Field(..., description="客戶ID")
    payment_term: PaymentTerm = PaymentTerm.CASH
    notes: Optional[str] = None
    tax_type: TaxType = TaxType.INCLUSIVE
    tax_rate: float = Field(0.05, ge=0, le=1, description="稅率,0-1之間")
    discount_rate: float = Field(0.0, ge=0, le=1, description="折扣率,0-1之間")

class SalesOrderCreate(SalesOrderBase):
    items: List[SalesOrderItemCreate] = Field(..., min_items=1, description="銷售明細,至少要有一項")

class SalesOrderUpdate(BaseModel):
    sales_date: Optional[date] = None
    customer_id: Optional[int] = None
    payment_term: Optional[PaymentTerm] = None
    status: Optional[SalesOrderStatus] = None
    notes: Optional[str] = None
    tax_type: Optional[TaxType] = None
    tax_rate: Optional[float] = Field(None, ge=0, le=1)
    discount_rate: Optional[float] = Field(None, ge=0, le=1)
    items: Optional[List[SalesOrderItemUpdate]] = None  # 銷售明細更新

class SalesOrder(SalesOrderBase):
    id: int
    so_number: str  # 銷售單號
    salesperson_id: int
    status: SalesOrderStatus
    subtotal: float  # 小計
    tax_amount: float  # 稅額
    discount_amount: float  # 折扣金額
    total_amount: float  # 實收總額
    created_at: datetime
    updated_at: Optional[datetime] = None

    # 關聯資料
    salesperson: Optional[User] = None
    customer: Optional[Customer] = None
    items: List[SalesOrderItem] = []

    class Config:
        from_attributes = True