feat: Add client extraction, amount cross-validation, and workflow fixes
OCR improvements: - Extract client data (name, CUI, address) from B2B receipts - Cross-validate amounts using payment methods and TVA entries - OCR-tolerant patterns for "TOTAL LEI" with common OCR errors - Better BON FISCAL vs CHITANTA detection Backend workflow fixes: - Fix SQLAlchemy deleted instance error in resubmit/submit workflow - Add session.refresh() after deleting accounting entries - Add unapprove endpoint (APPROVED → PENDING_REVIEW) - Add direction filter for receipt listing Frontend improvements: - Fix Vue v-else-if chain broken by Menu component - Unified OCR Preview layout with values table - Receipt list filter by direction (plati/incasari) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,9 @@ class ReceiptCRUD:
|
|||||||
if filters.status:
|
if filters.status:
|
||||||
query = query.where(Receipt.status == filters.status)
|
query = query.where(Receipt.status == filters.status)
|
||||||
|
|
||||||
|
if filters.direction:
|
||||||
|
query = query.where(Receipt.direction == filters.direction)
|
||||||
|
|
||||||
if filters.company_id:
|
if filters.company_id:
|
||||||
query = query.where(Receipt.company_id == filters.company_id)
|
query = query.where(Receipt.company_id == filters.company_id)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from app.schemas.receipt import (
|
|||||||
CashRegisterOption,
|
CashRegisterOption,
|
||||||
ExpenseTypeOption,
|
ExpenseTypeOption,
|
||||||
)
|
)
|
||||||
from app.db.models.receipt import ReceiptStatus
|
from app.db.models.receipt import ReceiptStatus, ReceiptDirection
|
||||||
|
|
||||||
# Auth integration
|
# Auth integration
|
||||||
from auth.dependencies import get_current_user
|
from auth.dependencies import get_current_user
|
||||||
@@ -121,6 +121,7 @@ async def create_receipt(
|
|||||||
@router.get("/", response_model=ReceiptListResponse)
|
@router.get("/", response_model=ReceiptListResponse)
|
||||||
async def list_receipts(
|
async def list_receipts(
|
||||||
status: Optional[ReceiptStatus] = None,
|
status: Optional[ReceiptStatus] = None,
|
||||||
|
direction: Optional[ReceiptDirection] = None,
|
||||||
company_id: Optional[int] = None,
|
company_id: Optional[int] = None,
|
||||||
created_by: Optional[str] = None,
|
created_by: Optional[str] = None,
|
||||||
date_from: Optional[str] = None,
|
date_from: Optional[str] = None,
|
||||||
@@ -136,6 +137,7 @@ async def list_receipts(
|
|||||||
|
|
||||||
filters = ReceiptFilter(
|
filters = ReceiptFilter(
|
||||||
status=status,
|
status=status,
|
||||||
|
direction=direction,
|
||||||
company_id=company_id or selected_company,
|
company_id=company_id or selected_company,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
date_from=date_type.fromisoformat(date_from) if date_from else None,
|
date_from=date_type.fromisoformat(date_from) if date_from else None,
|
||||||
@@ -301,6 +303,24 @@ async def resubmit_receipt(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{receipt_id}/unapprove", response_model=WorkflowAction)
|
||||||
|
async def unapprove_receipt(
|
||||||
|
receipt_id: int,
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
current_user: CurrentUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Unapprove receipt (APPROVED → PENDING_REVIEW). Returns to pending for corrections."""
|
||||||
|
success, message, receipt = await ReceiptService.unapprove_receipt(
|
||||||
|
session, receipt_id, current_user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
return WorkflowAction(
|
||||||
|
success=success,
|
||||||
|
message=message,
|
||||||
|
receipt=ReceiptResponse.model_validate(receipt) if receipt else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============ Accounting Entries Endpoints ============
|
# ============ Accounting Entries Endpoints ============
|
||||||
|
|
||||||
@router.get("/{receipt_id}/entries", response_model=List[AccountingEntryResponse])
|
@router.get("/{receipt_id}/entries", response_model=List[AccountingEntryResponse])
|
||||||
|
|||||||
@@ -42,9 +42,15 @@ class ExtractionData(BaseModel):
|
|||||||
payment_methods: List[PaymentMethod] = Field(default=[], description="Payment methods from receipt (CARD, NUMERAR)")
|
payment_methods: List[PaymentMethod] = Field(default=[], description="Payment methods from receipt (CARD, NUMERAR)")
|
||||||
suggested_payment_mode: Optional[str] = Field(default=None, description="Auto-suggested payment mode based on OCR (casa/banca)")
|
suggested_payment_mode: Optional[str] = Field(default=None, description="Auto-suggested payment mode based on OCR (casa/banca)")
|
||||||
|
|
||||||
|
# Client data (for B2B receipts - buyer information)
|
||||||
|
client_name: Optional[str] = Field(default=None, description="Client/customer company name")
|
||||||
|
client_cui: Optional[str] = Field(default=None, description="Client CUI/CIF fiscal code")
|
||||||
|
client_address: Optional[str] = Field(default=None, description="Client address")
|
||||||
|
|
||||||
confidence_amount: float = Field(default=0.0, ge=0, le=1, description="Amount extraction confidence")
|
confidence_amount: float = Field(default=0.0, ge=0, le=1, description="Amount extraction confidence")
|
||||||
confidence_date: float = Field(default=0.0, ge=0, le=1, description="Date extraction confidence")
|
confidence_date: float = Field(default=0.0, ge=0, le=1, description="Date extraction confidence")
|
||||||
confidence_vendor: float = Field(default=0.0, ge=0, le=1, description="Vendor extraction confidence")
|
confidence_vendor: float = Field(default=0.0, ge=0, le=1, description="Vendor extraction confidence")
|
||||||
|
confidence_client: float = Field(default=0.0, ge=0, le=1, description="Client extraction confidence")
|
||||||
overall_confidence: float = Field(default=0.0, ge=0, le=1, description="Overall confidence score")
|
overall_confidence: float = Field(default=0.0, ge=0, le=1, description="Overall confidence score")
|
||||||
raw_text: str = Field(default="", description="Raw OCR text")
|
raw_text: str = Field(default="", description="Raw OCR text")
|
||||||
ocr_engine: str = Field(default="", description="OCR engine used: paddleocr or tesseract")
|
ocr_engine: str = Field(default="", description="OCR engine used: paddleocr or tesseract")
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ class ReceiptResponse(ReceiptBase):
|
|||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
|
# Override amount to allow zero values in response (validation is on input, not output)
|
||||||
|
amount: Decimal
|
||||||
status: ReceiptStatus
|
status: ReceiptStatus
|
||||||
created_by: str
|
created_by: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -206,6 +208,7 @@ class ReceiptListResponse(BaseModel):
|
|||||||
class ReceiptFilter(BaseModel):
|
class ReceiptFilter(BaseModel):
|
||||||
"""Schema for filtering receipts."""
|
"""Schema for filtering receipts."""
|
||||||
status: Optional[ReceiptStatus] = None
|
status: Optional[ReceiptStatus] = None
|
||||||
|
direction: Optional[ReceiptDirection] = None
|
||||||
company_id: Optional[int] = None
|
company_id: Optional[int] = None
|
||||||
created_by: Optional[str] = None
|
created_by: Optional[str] = None
|
||||||
date_from: Optional[date] = None
|
date_from: Optional[date] = None
|
||||||
|
|||||||
@@ -25,9 +25,15 @@ class ExtractionResult:
|
|||||||
items_count: Optional[int] = None
|
items_count: Optional[int] = None
|
||||||
payment_methods: List[dict] = field(default_factory=list) # [{"method":"CARD","amount":Decimal}]
|
payment_methods: List[dict] = field(default_factory=list) # [{"method":"CARD","amount":Decimal}]
|
||||||
|
|
||||||
|
# Client data (for B2B receipts - buyer information)
|
||||||
|
client_name: Optional[str] = None
|
||||||
|
client_cui: Optional[str] = None
|
||||||
|
client_address: Optional[str] = None
|
||||||
|
|
||||||
confidence_amount: float = 0.0
|
confidence_amount: float = 0.0
|
||||||
confidence_date: float = 0.0
|
confidence_date: float = 0.0
|
||||||
confidence_vendor: float = 0.0
|
confidence_vendor: float = 0.0
|
||||||
|
confidence_client: float = 0.0
|
||||||
raw_text: str = ""
|
raw_text: str = ""
|
||||||
ocr_engine: str = "" # OCR engine used: paddleocr or tesseract
|
ocr_engine: str = "" # OCR engine used: paddleocr or tesseract
|
||||||
processing_time_ms: int = 0 # Processing time in milliseconds
|
processing_time_ms: int = 0 # Processing time in milliseconds
|
||||||
@@ -51,8 +57,10 @@ class ReceiptExtractor:
|
|||||||
# Romanian receipts use various formats: TOTAL LEI, TOTAL:, TOTAL RON, etc.
|
# Romanian receipts use various formats: TOTAL LEI, TOTAL:, TOTAL RON, etc.
|
||||||
# OCR often produces errors, so patterns must be tolerant
|
# OCR often produces errors, so patterns must be tolerant
|
||||||
TOTAL_PATTERNS = [
|
TOTAL_PATTERNS = [
|
||||||
# Most common: TOTAL LEI followed by amount
|
# Most common: TOTAL LEI followed by amount (with OCR-tolerant variations)
|
||||||
(r'TOTAL\s+LEI\s*([\d\s.,]+)', 0.98),
|
# Handles: TOTAL LEI, TOTAL. LE!, T0TAL LEI, TOTAL LE1, etc.
|
||||||
|
(r'T[O0]TAL[.\s]+L[E3][I1!]\s*:?\s*([\d\s.,]+)', 0.98), # OCR-tolerant: TOTAL. LE!, T0TAL LEI
|
||||||
|
(r'TOTAL\s+LEI\s*([\d\s.,]+)', 0.98), # Standard clean pattern
|
||||||
(r'[OT]?OTAL\s+LEI\s*([\d\s.,]+)', 0.95), # OCR may miss first letter
|
(r'[OT]?OTAL\s+LEI\s*([\d\s.,]+)', 0.95), # OCR may miss first letter
|
||||||
# Standard patterns
|
# Standard patterns
|
||||||
(r'TOTAL\s*:?\s*([\d\s.,]+)\s*(?:RON|LEI)?', 0.95),
|
(r'TOTAL\s*:?\s*([\d\s.,]+)\s*(?:RON|LEI)?', 0.95),
|
||||||
@@ -228,6 +236,41 @@ class ReceiptExtractor:
|
|||||||
(r'(JUD\.?\s+[A-Z]+,?\s*(?:MUN\.?|OR\.?|COM\.?)?\s*[A-Z]+)', 0.85),
|
(r'(JUD\.?\s+[A-Z]+,?\s*(?:MUN\.?|OR\.?|COM\.?)?\s*[A-Z]+)', 0.85),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Client/Buyer patterns (for B2B receipts)
|
||||||
|
# CLIENT, CUMPARATOR, BENEFICIAR sections
|
||||||
|
CLIENT_SECTION_MARKERS = [
|
||||||
|
r'C\.?\s*I\.?\s*F\.?\s+CLIENT\s*:', # CIF CLIENT: (reversed format)
|
||||||
|
r'C\.?\s*U\.?\s*I\.?\s+CLIENT\s*:', # CUI CLIENT: (reversed format)
|
||||||
|
r'CLIENT\s*:',
|
||||||
|
r'CUMPARATOR\s*:',
|
||||||
|
r'BENEFICIAR\s*:',
|
||||||
|
r'CUMP[AĂ]R[AĂ]TOR\s*:',
|
||||||
|
r'DATE\s+CLIENT',
|
||||||
|
r'LIENT\s*:', # OCR truncation
|
||||||
|
]
|
||||||
|
|
||||||
|
# Client CUI patterns (explicitly after CLIENT marker)
|
||||||
|
CLIENT_CUI_PATTERNS = [
|
||||||
|
# CIF CLIENT: R01879856 (reversed format - CIF before CLIENT)
|
||||||
|
(r'C\.?\s*I\.?\s*F\.?\s+CLIENT\s*:?\s*(R[O0]?\d{6,10})', 0.98),
|
||||||
|
(r'C\.?\s*U\.?\s*I\.?\s+CLIENT\s*:?\s*(R[O0]?\d{6,10})', 0.98),
|
||||||
|
(r'C\.?\s*I\.?\s*F\.?\s+CLIENT\s*:?\s*(?:R[O0])?(\d{6,10})', 0.98),
|
||||||
|
(r'C\.?\s*U\.?\s*I\.?\s+CLIENT\s*:?\s*(?:R[O0])?(\d{6,10})', 0.98),
|
||||||
|
# CLIENT C.U.I./ C.I.F. :R01879855 (slash variant with both labels)
|
||||||
|
(r'CLIENT\s+C\.\s*U\.\s*I\.?\s*/\s*C\.\s*[I1]\.\s*F\.?\s*:?\s*(R[O0]?\d{6,10})', 0.97),
|
||||||
|
(r'CLIENT\s+C\.?\s*U\.?\s*I\.?(?:\s*/\s*C\.?\s*[I1]\.?\s*F\.?)?\s*:?\s*(R[O0]?\d{6,10})', 0.96),
|
||||||
|
# CLIENT C.U.I. or CLIENT CUI or CLIENT CIF
|
||||||
|
(r'CLIENT\s+C\.?\s*U\.?\s*I\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.98),
|
||||||
|
(r'CLIENT\s+C\.?\s*I\.?\s*F\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.98),
|
||||||
|
(r'CUMPARATOR\s+C\.?\s*U\.?\s*I\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.95),
|
||||||
|
(r'CUMPARATOR\s+C\.?\s*I\.?\s*F\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.95),
|
||||||
|
# CUI/CIF on line immediately after CLIENT marker
|
||||||
|
(r'CLIENT\s*:\s*\n\s*C\.?\s*U\.?\s*I\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.95),
|
||||||
|
(r'CLIENT\s*:\s*\n\s*C\.?\s*I\.?\s*F\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.95),
|
||||||
|
# CUI after client name: "CLIENT: COMPANY SRL\nCUI: 12345678"
|
||||||
|
(r'CLIENT\s*:.*\n.*C\.?\s*U\.?\s*I\.?\s*:?\s*(?:R[O0])?(\d{6,10})', 0.90),
|
||||||
|
]
|
||||||
|
|
||||||
# Vendor name indicators (lines containing these are likely vendor names)
|
# Vendor name indicators (lines containing these are likely vendor names)
|
||||||
# These should be company type suffixes, not generic words
|
# These should be company type suffixes, not generic words
|
||||||
# Patterns must handle OCR spaces: "S. R. L." as well as "S.R.L."
|
# Patterns must handle OCR spaces: "S. R. L." as well as "S.R.L."
|
||||||
@@ -282,6 +325,13 @@ class ReceiptExtractor:
|
|||||||
result.address = self._extract_address(text_upper)
|
result.address = self._extract_address(text_upper)
|
||||||
result.payment_methods = self._extract_payment_methods(text_upper)
|
result.payment_methods = self._extract_payment_methods(text_upper)
|
||||||
|
|
||||||
|
# Extract client data (B2B receipts)
|
||||||
|
client_name, client_cui, client_address, confidence_client = self._extract_client_data(text_upper, text)
|
||||||
|
result.client_name = client_name
|
||||||
|
result.client_cui = client_cui
|
||||||
|
result.client_address = client_address
|
||||||
|
result.confidence_client = confidence_client
|
||||||
|
|
||||||
# Detect receipt type
|
# Detect receipt type
|
||||||
result.receipt_type = self._detect_receipt_type(text_upper)
|
result.receipt_type = self._detect_receipt_type(text_upper)
|
||||||
|
|
||||||
@@ -291,6 +341,19 @@ class ReceiptExtractor:
|
|||||||
if not is_valid:
|
if not is_valid:
|
||||||
print(f"[TVA Reverse Validation] {msg}", flush=True)
|
print(f"[TVA Reverse Validation] {msg}", flush=True)
|
||||||
|
|
||||||
|
# Cross-validate amount using payment methods and TVA
|
||||||
|
validated_amount, validated_confidence, source = self._cross_validate_and_calculate_amount(
|
||||||
|
result.amount,
|
||||||
|
result.confidence_amount,
|
||||||
|
result.payment_methods,
|
||||||
|
result.tva_entries,
|
||||||
|
result.tva_total
|
||||||
|
)
|
||||||
|
if validated_amount != result.amount:
|
||||||
|
print(f"[Cross-Validation] Amount updated: {result.amount} -> {validated_amount} (source: {source})", flush=True)
|
||||||
|
result.amount = validated_amount
|
||||||
|
result.confidence_amount = validated_confidence
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _extract_amount(self, text: str) -> Tuple[Optional[Decimal], float]:
|
def _extract_amount(self, text: str) -> Tuple[Optional[Decimal], float]:
|
||||||
@@ -402,6 +465,97 @@ class ReceiptExtractor:
|
|||||||
|
|
||||||
return num_str
|
return num_str
|
||||||
|
|
||||||
|
def _cross_validate_and_calculate_amount(
|
||||||
|
self,
|
||||||
|
amount: Optional[Decimal],
|
||||||
|
confidence_amount: float,
|
||||||
|
payment_methods: List[dict],
|
||||||
|
tva_entries: List[dict],
|
||||||
|
tva_total: Optional[Decimal]
|
||||||
|
) -> Tuple[Optional[Decimal], float, str]:
|
||||||
|
"""
|
||||||
|
Cross-validate and potentially calculate total from payment methods and TVA.
|
||||||
|
|
||||||
|
Returns: (amount, confidence, source_description)
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
1. If amount is valid (>0) with high confidence (>=0.8), use it directly
|
||||||
|
2. Calculate payment_sum = CARD + NUMERAR + other methods
|
||||||
|
3. Calculate tva_implied_total = tva_total * (100 + rate) / rate
|
||||||
|
4. Cross-validate: if payment_sum matches extracted amount, boost confidence
|
||||||
|
5. If amount is 0/None, use payment_sum as total
|
||||||
|
6. If payment_sum is 0, try to calculate from TVA
|
||||||
|
"""
|
||||||
|
# Calculate payment methods sum
|
||||||
|
payment_sum = Decimal('0')
|
||||||
|
if payment_methods:
|
||||||
|
for pm in payment_methods:
|
||||||
|
try:
|
||||||
|
pm_amount = pm.get('amount')
|
||||||
|
if pm_amount:
|
||||||
|
payment_sum += Decimal(str(pm_amount))
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate TVA-implied total: total = tva * (100 + rate) / rate
|
||||||
|
tva_implied_total = None
|
||||||
|
if tva_entries:
|
||||||
|
# Use the main TVA entry (typically the largest or first one)
|
||||||
|
main_entry = tva_entries[0]
|
||||||
|
rate = main_entry.get('percent', 19)
|
||||||
|
tva_amount = main_entry.get('amount')
|
||||||
|
if tva_amount and rate > 0:
|
||||||
|
try:
|
||||||
|
tva_dec = Decimal(str(tva_amount))
|
||||||
|
# total = tva * (100 + rate) / rate
|
||||||
|
tva_implied_total = (tva_dec * Decimal(100 + rate) / Decimal(rate)).quantize(Decimal('0.01'))
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Case 1: Amount is valid with high confidence - just validate
|
||||||
|
if amount and amount > 0 and confidence_amount >= 0.8:
|
||||||
|
# Cross-validate: check if it matches payment methods
|
||||||
|
if payment_sum > 0 and abs(amount - payment_sum) <= Decimal('0.02'):
|
||||||
|
# Perfect match - boost confidence
|
||||||
|
return amount, min(0.98, confidence_amount + 0.05), "extracted (validated by payment methods)"
|
||||||
|
return amount, confidence_amount, "extracted"
|
||||||
|
|
||||||
|
# Case 2: Amount exists but low confidence - try to validate/correct
|
||||||
|
if amount and amount > 0:
|
||||||
|
# Check if payment methods sum matches
|
||||||
|
if payment_sum > 0:
|
||||||
|
if abs(amount - payment_sum) <= Decimal('0.02'):
|
||||||
|
# Match - boost confidence
|
||||||
|
return amount, 0.90, "extracted (validated by payment methods)"
|
||||||
|
else:
|
||||||
|
# Mismatch - prefer payment_sum as it's more reliable
|
||||||
|
print(f"[Cross-Validation] Amount mismatch: extracted={amount}, payments={payment_sum}", flush=True)
|
||||||
|
return payment_sum, 0.85, "calculated from payment methods"
|
||||||
|
|
||||||
|
# Check TVA-implied total
|
||||||
|
if tva_implied_total:
|
||||||
|
if abs(amount - tva_implied_total) <= Decimal('0.50'):
|
||||||
|
# Close match - use extracted amount
|
||||||
|
return amount, 0.80, "extracted (validated by TVA)"
|
||||||
|
else:
|
||||||
|
print(f"[Cross-Validation] TVA mismatch: extracted={amount}, tva_implied={tva_implied_total}", flush=True)
|
||||||
|
|
||||||
|
# No validation possible - return as-is
|
||||||
|
return amount, confidence_amount, "extracted (unvalidated)"
|
||||||
|
|
||||||
|
# Case 3: Amount is 0 or None - calculate from payment methods
|
||||||
|
if payment_sum > 0:
|
||||||
|
print(f"[Cross-Validation] Amount not found, using payment sum: {payment_sum}", flush=True)
|
||||||
|
return payment_sum, 0.85, "calculated from payment methods"
|
||||||
|
|
||||||
|
# Case 4: Try TVA-implied total as last resort
|
||||||
|
if tva_implied_total and tva_implied_total > 0:
|
||||||
|
print(f"[Cross-Validation] Amount not found, using TVA-implied total: {tva_implied_total}", flush=True)
|
||||||
|
return tva_implied_total, 0.70, "calculated from TVA"
|
||||||
|
|
||||||
|
# Nothing worked - return original
|
||||||
|
return amount, confidence_amount, "not found"
|
||||||
|
|
||||||
def _extract_date(self, text: str) -> Tuple[Optional[date], float]:
|
def _extract_date(self, text: str) -> Tuple[Optional[date], float]:
|
||||||
"""Extract receipt date from text."""
|
"""Extract receipt date from text."""
|
||||||
# First try standard patterns (clean dates)
|
# First try standard patterns (clean dates)
|
||||||
@@ -650,9 +804,17 @@ class ReceiptExtractor:
|
|||||||
return None, 0.0
|
return None, 0.0
|
||||||
|
|
||||||
def _detect_receipt_type(self, text: str) -> str:
|
def _detect_receipt_type(self, text: str) -> str:
|
||||||
"""Detect receipt type from text content."""
|
"""Detect receipt type from text content.
|
||||||
|
|
||||||
|
BON FISCAL variants: "BON FISCAL", "BON FISCAL.", "BON FISCAL"
|
||||||
|
CHITANTA variants: "CHITANTA", "CHITANȚĂ"
|
||||||
|
"""
|
||||||
|
# Check for explicit BON FISCAL first (handles OCR spacing variations)
|
||||||
|
if re.search(r'BON\s+FISCAL', text):
|
||||||
|
return 'bon_fiscal'
|
||||||
if 'CHITANTA' in text or 'CHITANȚĂ' in text:
|
if 'CHITANTA' in text or 'CHITANȚĂ' in text:
|
||||||
return 'chitanta'
|
return 'chitanta'
|
||||||
|
# Default to bon_fiscal if neither found
|
||||||
return 'bon_fiscal'
|
return 'bon_fiscal'
|
||||||
|
|
||||||
def _extract_tva_entries(self, text: str) -> Tuple[List[dict], Optional[Decimal]]:
|
def _extract_tva_entries(self, text: str) -> Tuple[List[dict], Optional[Decimal]]:
|
||||||
@@ -1237,3 +1399,103 @@ class ReceiptExtractor:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
return payment_methods
|
return payment_methods
|
||||||
|
|
||||||
|
def _extract_client_data(
|
||||||
|
self, text_upper: str, original_text: str
|
||||||
|
) -> Tuple[Optional[str], Optional[str], Optional[str], float]:
|
||||||
|
"""
|
||||||
|
Extract client/buyer data from B2B receipts.
|
||||||
|
|
||||||
|
Returns (client_name, client_cui, client_address, confidence)
|
||||||
|
"""
|
||||||
|
client_name = None
|
||||||
|
client_cui = None
|
||||||
|
client_address = None
|
||||||
|
confidence = 0.0
|
||||||
|
|
||||||
|
# Step 1: Find CLIENT section marker
|
||||||
|
client_section_start = None
|
||||||
|
for marker in self.CLIENT_SECTION_MARKERS:
|
||||||
|
match = re.search(marker, text_upper, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
client_section_start = match.start()
|
||||||
|
break
|
||||||
|
|
||||||
|
if client_section_start is None:
|
||||||
|
# No client section found
|
||||||
|
return None, None, None, 0.0
|
||||||
|
|
||||||
|
# Step 2: Extract client CUI
|
||||||
|
for pattern, conf in self.CLIENT_CUI_PATTERNS:
|
||||||
|
match = re.search(pattern, text_upper, re.IGNORECASE | re.MULTILINE)
|
||||||
|
if match:
|
||||||
|
cui = match.group(1)
|
||||||
|
if 6 <= len(cui) <= 10:
|
||||||
|
client_cui = cui
|
||||||
|
confidence = max(confidence, conf)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 3: Extract client name from CLIENT section
|
||||||
|
# Look for company name after CLIENT: marker
|
||||||
|
lines = original_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
line_upper = line.upper().strip()
|
||||||
|
|
||||||
|
# Check if this line contains CLIENT marker
|
||||||
|
if any(re.search(marker, line_upper) for marker in self.CLIENT_SECTION_MARKERS):
|
||||||
|
# Check if name is on same line after ":"
|
||||||
|
if ':' in line:
|
||||||
|
name_part = line.split(':', 1)[1].strip()
|
||||||
|
if name_part and len(name_part) >= 3:
|
||||||
|
# Skip if it looks like a CUI (R/RO followed by digits)
|
||||||
|
if re.match(r'^R[O0]?\d{6,10}$', name_part.upper()):
|
||||||
|
# This is a CUI, not a name - extract it if not already found
|
||||||
|
if not client_cui:
|
||||||
|
cui_digits = re.sub(r'[^0-9]', '', name_part)
|
||||||
|
if 6 <= len(cui_digits) <= 10:
|
||||||
|
client_cui = cui_digits
|
||||||
|
confidence = max(confidence, 0.90)
|
||||||
|
continue
|
||||||
|
# Check for company indicators
|
||||||
|
if any(re.search(ind, name_part.upper()) for ind in self.VENDOR_INDICATORS):
|
||||||
|
client_name = self._clean_vendor_name(name_part)
|
||||||
|
confidence = max(confidence, 0.95)
|
||||||
|
break
|
||||||
|
elif len(name_part) >= 5 and not name_part.isdigit():
|
||||||
|
client_name = self._clean_vendor_name(name_part)
|
||||||
|
confidence = max(confidence, 0.80)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check next line for company name
|
||||||
|
if i + 1 < len(lines):
|
||||||
|
next_line = lines[i + 1].strip()
|
||||||
|
next_upper = next_line.upper()
|
||||||
|
|
||||||
|
# Skip if it's a CUI/CIF line
|
||||||
|
if not re.search(r'C\.?\s*[UI]\.?\s*[IF]\.?', next_upper):
|
||||||
|
if any(re.search(ind, next_upper) for ind in self.VENDOR_INDICATORS):
|
||||||
|
client_name = self._clean_vendor_name(next_line)
|
||||||
|
confidence = max(confidence, 0.90)
|
||||||
|
break
|
||||||
|
elif len(next_line) >= 5 and not next_line.isdigit():
|
||||||
|
# Check if it looks like a company name
|
||||||
|
if not any(kw in next_upper for kw in ['CUI', 'CIF', 'COD', 'FISCAL']):
|
||||||
|
client_name = self._clean_vendor_name(next_line)
|
||||||
|
confidence = max(confidence, 0.75)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Step 4: Extract client address (if present after client section)
|
||||||
|
if client_section_start:
|
||||||
|
# Look for address patterns after client section
|
||||||
|
client_region = text_upper[client_section_start:client_section_start + 500]
|
||||||
|
for pattern, _ in self.ADDRESS_PATTERNS:
|
||||||
|
match = re.search(pattern, client_region)
|
||||||
|
if match:
|
||||||
|
client_address = match.group(1).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Log extraction result
|
||||||
|
if client_cui or client_name:
|
||||||
|
print(f"[Client Extraction] Found: name={client_name}, cui={client_cui}, conf={confidence}", flush=True)
|
||||||
|
|
||||||
|
return client_name, client_cui, client_address, confidence
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ class ReceiptService:
|
|||||||
account_code=expense_type.account_code,
|
account_code=expense_type.account_code,
|
||||||
account_name=expense_type.account_name,
|
account_name=expense_type.account_name,
|
||||||
amount=net_amount,
|
amount=net_amount,
|
||||||
partner_id=receipt.partner_id,
|
|
||||||
))
|
))
|
||||||
|
|
||||||
# Debit: VAT deductible
|
# Debit: VAT deductible
|
||||||
@@ -156,7 +155,6 @@ class ReceiptService:
|
|||||||
account_code=expense_type.account_code,
|
account_code=expense_type.account_code,
|
||||||
account_name=expense_type.account_name,
|
account_name=expense_type.account_name,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
partner_id=receipt.partner_id,
|
|
||||||
))
|
))
|
||||||
|
|
||||||
# Credit entry - based on payment_mode (new) or cash_register (legacy)
|
# Credit entry - based on payment_mode (new) or cash_register (legacy)
|
||||||
@@ -245,6 +243,9 @@ class ReceiptService:
|
|||||||
await AccountingEntryCRUD.delete_all_for_receipt(session, receipt_id)
|
await AccountingEntryCRUD.delete_all_for_receipt(session, receipt_id)
|
||||||
await AccountingEntryCRUD.create_bulk(session, receipt_id, entries, is_auto_generated=True)
|
await AccountingEntryCRUD.create_bulk(session, receipt_id, entries, is_auto_generated=True)
|
||||||
|
|
||||||
|
# Refresh receipt to clear stale relationship references after entry deletion
|
||||||
|
await session.refresh(receipt)
|
||||||
|
|
||||||
# Update status
|
# Update status
|
||||||
updated = await ReceiptCRUD.update_status(
|
updated = await ReceiptCRUD.update_status(
|
||||||
session, receipt, ReceiptStatus.PENDING_REVIEW
|
session, receipt, ReceiptStatus.PENDING_REVIEW
|
||||||
@@ -288,6 +289,31 @@ class ReceiptService:
|
|||||||
|
|
||||||
return True, "Receipt approved", updated
|
return True, "Receipt approved", updated
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def unapprove_receipt(
|
||||||
|
session: AsyncSession,
|
||||||
|
receipt_id: int,
|
||||||
|
username: str,
|
||||||
|
) -> Tuple[bool, str, Optional[Receipt]]:
|
||||||
|
"""
|
||||||
|
Unapprove receipt (APPROVED → PENDING_REVIEW).
|
||||||
|
Returns receipt to pending review for corrections.
|
||||||
|
"""
|
||||||
|
receipt = await ReceiptCRUD.get_by_id(session, receipt_id)
|
||||||
|
|
||||||
|
if not receipt:
|
||||||
|
return False, "Receipt not found", None
|
||||||
|
|
||||||
|
if receipt.status != ReceiptStatus.APPROVED:
|
||||||
|
return False, "Receipt is not approved", None
|
||||||
|
|
||||||
|
# Update status back to pending review
|
||||||
|
updated = await ReceiptCRUD.update_status(
|
||||||
|
session, receipt, ReceiptStatus.PENDING_REVIEW
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, "Receipt returned to pending review", updated
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def reject_receipt(
|
async def reject_receipt(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
@@ -342,6 +368,9 @@ class ReceiptService:
|
|||||||
await AccountingEntryCRUD.delete_all_for_receipt(session, receipt_id)
|
await AccountingEntryCRUD.delete_all_for_receipt(session, receipt_id)
|
||||||
await AccountingEntryCRUD.create_bulk(session, receipt_id, entries, is_auto_generated=True)
|
await AccountingEntryCRUD.create_bulk(session, receipt_id, entries, is_auto_generated=True)
|
||||||
|
|
||||||
|
# Refresh receipt to clear stale relationship references after entry deletion
|
||||||
|
await session.refresh(receipt)
|
||||||
|
|
||||||
# Update status
|
# Update status
|
||||||
updated = await ReceiptCRUD.update_status(
|
updated = await ReceiptCRUD.update_status(
|
||||||
session, receipt, ReceiptStatus.PENDING_REVIEW
|
session, receipt, ReceiptStatus.PENDING_REVIEW
|
||||||
|
|||||||
@@ -39,6 +39,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION: CLIENT (always visible) -->
|
||||||
|
<div class="ocr-section">
|
||||||
|
<div class="ocr-section-title">CLIENT</div>
|
||||||
|
<div class="ocr-section-content">
|
||||||
|
<template v-if="data.client_name || data.client_cui || data.client_address">
|
||||||
|
<div class="client-name" v-if="data.client_name">
|
||||||
|
{{ data.client_name }}
|
||||||
|
<OCRConfidenceIndicator v-if="data.confidence_client" :confidence="data.confidence_client" size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="client-cui" v-if="data.client_cui">CUI: {{ data.client_cui }}</div>
|
||||||
|
<div class="client-address" v-if="data.client_address">{{ data.client_address }}</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="no-data">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SECTION: DOCUMENT -->
|
<!-- SECTION: DOCUMENT -->
|
||||||
<div class="ocr-section" v-if="data.receipt_type || data.receipt_number || data.receipt_date">
|
<div class="ocr-section" v-if="data.receipt_type || data.receipt_number || data.receipt_date">
|
||||||
<div class="ocr-section-title">DOCUMENT</div>
|
<div class="ocr-section-title">DOCUMENT</div>
|
||||||
@@ -61,46 +77,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SECTION: TOTAL + PLATA (combined) -->
|
<!-- SECTION: TOTAL + PLATA + TVA (unified flat layout) -->
|
||||||
<div class="ocr-section ocr-section-total" v-if="data.amount">
|
<div class="ocr-section" v-if="data.amount || data.tva_entries?.length > 0 || paymentSum > 0">
|
||||||
<div class="ocr-section-title">TOTAL</div>
|
<div class="ocr-values-table">
|
||||||
<div class="ocr-section-content">
|
<!-- TOTAL row - when extracted -->
|
||||||
<div class="ocr-total-box">
|
<div class="value-row" v-if="data.amount">
|
||||||
<div class="total-main">
|
<span class="value-label">TOTAL</span>
|
||||||
<span class="total-amount">{{ formatAmount(data.amount) }} LEI</span>
|
<span class="value-amount">
|
||||||
<OCRConfidenceIndicator :confidence="data.confidence_amount" size="small" />
|
{{ formatAmount(data.amount) }} LEI
|
||||||
</div>
|
<OCRConfidenceIndicator :confidence="data.confidence_amount" size="small" class="confidence-inline" />
|
||||||
<!-- Payment methods integrated into total box -->
|
</span>
|
||||||
<div v-if="data.payment_methods?.length > 0" class="payment-methods-inline">
|
|
||||||
<Tag
|
|
||||||
v-for="(pm, idx) in data.payment_methods"
|
|
||||||
:key="idx"
|
|
||||||
:severity="pm.method === 'CARD' ? 'info' : 'success'"
|
|
||||||
:value="`${pm.method}: ${formatAmount(pm.amount)} LEI`"
|
|
||||||
class="payment-tag"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data.items_count" class="items-count">
|
|
||||||
{{ data.items_count }} articole
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SECTION: TVA -->
|
<!-- TOTAL row - warning when not found but can be calculated from payments -->
|
||||||
<div class="ocr-section" v-if="data.tva_entries?.length > 0 || data.tva_total">
|
<div class="value-row warning-row" v-else-if="paymentSum > 0">
|
||||||
<div class="ocr-section-title">TVA</div>
|
<span class="value-label">
|
||||||
<div class="ocr-section-content">
|
<i class="pi pi-exclamation-triangle warning-icon"></i>
|
||||||
<div class="ocr-tva-table">
|
TOTAL (calculat)
|
||||||
<div v-for="(entry, idx) in data.tva_entries" :key="idx" class="tva-row">
|
</span>
|
||||||
<span class="tva-code" v-if="entry.code">{{ entry.code }}</span>
|
<span class="value-amount calculated">
|
||||||
<span class="tva-percent">({{ entry.percent }}%)</span>
|
{{ formatAmount(paymentSum) }} LEI
|
||||||
<span class="tva-amount">{{ formatAmount(entry.amount) }} LEI</span>
|
<span class="hint">(din plati)</span>
|
||||||
</div>
|
</span>
|
||||||
<div v-if="computedTvaTotal > 0" class="tva-row tva-total-row">
|
</div>
|
||||||
<span class="tva-label">Total TVA:</span>
|
|
||||||
<span class="tva-amount">{{ formatAmount(computedTvaTotal) }} LEI</span>
|
<!-- Validation warning: total ≠ payment sum -->
|
||||||
</div>
|
<div class="validation-warning" v-if="totalMismatchPayment">
|
||||||
|
<i class="pi pi-exclamation-triangle"></i>
|
||||||
|
Total ({{ formatAmount(data.amount) }}) ≠ Suma plati ({{ formatAmount(paymentSum) }})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Validation info: TVA-implied total mismatch -->
|
||||||
|
<div class="validation-info" v-if="totalMismatchTva">
|
||||||
|
<i class="pi pi-info-circle"></i>
|
||||||
|
Total din TVA: {{ formatAmount(tvaImpliedTotal) }} LEI
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment methods as plain text rows -->
|
||||||
|
<div class="value-row" v-for="(pm, idx) in data.payment_methods" :key="'pm-'+idx">
|
||||||
|
<span class="value-label">{{ pm.method }}</span>
|
||||||
|
<span class="value-amount">{{ formatAmount(pm.amount) }} LEI</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TVA entries -->
|
||||||
|
<div class="value-row" v-for="(entry, idx) in data.tva_entries" :key="'tva-'+idx">
|
||||||
|
<span class="value-label">TVA {{ entry.code }} ({{ entry.percent }}%)</span>
|
||||||
|
<span class="value-amount">{{ formatAmount(entry.amount) }} LEI</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TVA Total -->
|
||||||
|
<div class="value-row total-row" v-if="computedTvaTotal > 0">
|
||||||
|
<span class="value-label">Total TVA</span>
|
||||||
|
<span class="value-amount">{{ formatAmount(computedTvaTotal) }} LEI</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items count -->
|
||||||
|
<div v-if="data.items_count" class="items-count-inline">
|
||||||
|
{{ data.items_count }} articole
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,6 +207,34 @@ const computedTvaTotal = computed(() => {
|
|||||||
return props.data.tva_entries.reduce((sum, e) => sum + parseFloat(e.amount || 0), 0)
|
return props.data.tva_entries.reduce((sum, e) => sum + parseFloat(e.amount || 0), 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cross-validation computed properties
|
||||||
|
const paymentSum = computed(() => {
|
||||||
|
if (!props.data.payment_methods?.length) return 0
|
||||||
|
return props.data.payment_methods.reduce((sum, pm) => sum + parseFloat(pm.amount || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tvaImpliedTotal = computed(() => {
|
||||||
|
// Calculate total from TVA: total = tva * (100 + rate) / rate
|
||||||
|
if (!props.data.tva_entries?.length) return 0
|
||||||
|
const mainEntry = props.data.tva_entries[0]
|
||||||
|
const rate = mainEntry.percent || 19
|
||||||
|
const tvaAmount = parseFloat(mainEntry.amount || 0)
|
||||||
|
if (tvaAmount === 0 || rate === 0) return 0
|
||||||
|
return tvaAmount * (100 + rate) / rate
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalMismatchPayment = computed(() => {
|
||||||
|
if (!props.data.amount || paymentSum.value === 0) return false
|
||||||
|
const total = parseFloat(props.data.amount)
|
||||||
|
return Math.abs(total - paymentSum.value) > 0.02 // Tolerance 2 bani
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalMismatchTva = computed(() => {
|
||||||
|
if (!props.data.amount || tvaImpliedTotal.value === 0) return false
|
||||||
|
const total = parseFloat(props.data.amount)
|
||||||
|
return Math.abs(total - tvaImpliedTotal.value) > 0.50 // Tolerance 50 bani (TVA calc has rounding)
|
||||||
|
})
|
||||||
|
|
||||||
const getSuggestedPaymentLabel = (mode) => {
|
const getSuggestedPaymentLabel = (mode) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
'casa': 'Casa (numerar firma)',
|
'casa': 'Casa (numerar firma)',
|
||||||
@@ -320,12 +381,6 @@ const formatProcessingTime = (ms) => {
|
|||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOTAL section - prominent styling */
|
|
||||||
.ocr-section-total {
|
|
||||||
background: linear-gradient(135deg, rgba(220, 252, 231, 0.8) 0%, rgba(187, 247, 208, 0.6) 100%);
|
|
||||||
border: 2px solid #86efac;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FURNIZOR section */
|
/* FURNIZOR section */
|
||||||
.vendor-name {
|
.vendor-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -347,6 +402,33 @@ const formatProcessingTime = (ms) => {
|
|||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CLIENT section */
|
||||||
|
.client-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-cui {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #475569;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-address {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder when no data extracted */
|
||||||
|
.no-data {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* DOCUMENT section */
|
/* DOCUMENT section */
|
||||||
.document-row {
|
.document-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -373,92 +455,117 @@ const formatProcessingTime = (ms) => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOTAL section - prominent */
|
/* Unified values table (TOTAL + Payment + TVA) */
|
||||||
.ocr-total-box {
|
.ocr-values-table {
|
||||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
|
||||||
border: 2px solid #86efac;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-amount {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: auto;
|
||||||
|
color: #1f2937;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-main {
|
.value-row.total-row {
|
||||||
display: flex;
|
margin-top: 0.35rem;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total-amount {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Payment methods inside total box */
|
|
||||||
.payment-methods-inline {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
border-top: 1px dashed #86efac;
|
border-top: 1px dashed #86efac;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-tag {
|
/* Items count - subtle, at bottom of values section */
|
||||||
font-size: 0.8rem;
|
.items-count-inline {
|
||||||
}
|
|
||||||
|
|
||||||
.items-count {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin-top: 0.35rem;
|
text-align: right;
|
||||||
|
padding-top: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TVA section */
|
/* Confidence indicator inline */
|
||||||
.ocr-tva-table {
|
.confidence-inline {
|
||||||
display: flex;
|
margin-left: 0.25rem;
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-row {
|
/* Warning row for calculated total */
|
||||||
|
.value-row.warning-row {
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-row.warning-row .value-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.85rem;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-code {
|
.warning-icon {
|
||||||
font-weight: 600;
|
color: #f59e0b;
|
||||||
color: #475569;
|
font-size: 0.9rem;
|
||||||
min-width: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-percent {
|
.value-amount.calculated {
|
||||||
color: #64748b;
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-amount .hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #a3a3a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation warnings */
|
||||||
|
.validation-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
background: #fee2e2;
|
||||||
|
border-radius: 6px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-amount {
|
.validation-warning i {
|
||||||
font-weight: 500;
|
color: #dc2626;
|
||||||
margin-left: auto;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-total-row {
|
/* Validation info */
|
||||||
margin-top: 0.25rem;
|
.validation-info {
|
||||||
padding-top: 0.25rem;
|
display: flex;
|
||||||
border-top: 1px dashed #86efac;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
background: #dbeafe;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #1e40af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tva-label {
|
.validation-info i {
|
||||||
font-weight: 600;
|
color: #2563eb;
|
||||||
color: #166534;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.raw-text-section {
|
.raw-text-section {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const useReceiptsStore = defineStore('receipts', {
|
|||||||
filters: {
|
filters: {
|
||||||
status: null,
|
status: null,
|
||||||
search: '',
|
search: '',
|
||||||
|
direction: null,
|
||||||
dateFrom: null,
|
dateFrom: null,
|
||||||
dateTo: null,
|
dateTo: null,
|
||||||
},
|
},
|
||||||
@@ -60,6 +61,9 @@ export const useReceiptsStore = defineStore('receipts', {
|
|||||||
if (this.filters.search) {
|
if (this.filters.search) {
|
||||||
params.search = this.filters.search
|
params.search = this.filters.search
|
||||||
}
|
}
|
||||||
|
if (this.filters.direction) {
|
||||||
|
params.direction = this.filters.direction
|
||||||
|
}
|
||||||
if (this.filters.dateFrom) {
|
if (this.filters.dateFrom) {
|
||||||
params.date_from = this.filters.dateFrom
|
params.date_from = this.filters.dateFrom
|
||||||
}
|
}
|
||||||
@@ -193,6 +197,20 @@ export const useReceiptsStore = defineStore('receipts', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async unapproveReceipt(id) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const response = await api.post(`/${id}/unapprove`)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error.response?.data?.detail || 'Failed to unapprove receipt'
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ============ Pending Receipts ============
|
// ============ Pending Receipts ============
|
||||||
|
|
||||||
async fetchPendingReceipts() {
|
async fetchPendingReceipts() {
|
||||||
@@ -409,6 +427,7 @@ export const useReceiptsStore = defineStore('receipts', {
|
|||||||
this.filters = {
|
this.filters = {
|
||||||
status: null,
|
status: null,
|
||||||
search: '',
|
search: '',
|
||||||
|
direction: null,
|
||||||
dateFrom: null,
|
dateFrom: null,
|
||||||
dateTo: null,
|
dateTo: null,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,10 @@ BLUE='\033[0;34m'
|
|||||||
MAGENTA='\033[0;35m'
|
MAGENTA='\033[0;35m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
print_message() { echo -e "${BLUE}[DATA-ENTRY-PROD]${NC} $1"; }
|
print_message() { echo -e "${BLUE}[DATA-ENTRY-PROD]${NC} $1"; }
|
||||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
@@ -25,39 +29,191 @@ check_port() {
|
|||||||
lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1
|
lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Individual service functions
|
||||||
|
stop_frontend() {
|
||||||
|
if check_port 3010; then
|
||||||
|
print_message "Stopping frontend..."
|
||||||
|
lsof -ti:3010 | xargs kill -TERM 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
lsof -ti:3010 | xargs kill -KILL 2>/dev/null || true
|
||||||
|
pkill -f "vite.*3010" 2>/dev/null || true
|
||||||
|
print_success "Frontend stopped"
|
||||||
|
else
|
||||||
|
print_warning "Frontend not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_backend() {
|
||||||
|
if check_port 8003; then
|
||||||
|
print_message "Stopping backend..."
|
||||||
|
lsof -ti:8003 | xargs kill -TERM 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
lsof -ti:8003 | xargs kill -KILL 2>/dev/null || true
|
||||||
|
pkill -f "uvicorn.*8003" 2>/dev/null || true
|
||||||
|
print_success "Backend stopped"
|
||||||
|
else
|
||||||
|
print_warning "Backend not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_frontend() {
|
||||||
|
if check_port 3010; then
|
||||||
|
print_warning "Frontend already running on port 3010"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message "Starting Frontend (Vue.js)..."
|
||||||
|
cd "$SCRIPT_DIR/data-entry-app/frontend/"
|
||||||
|
|
||||||
|
if [ ! -d "node_modules" ] || [ -f "node_modules/.bin/vite.cmd" ]; then
|
||||||
|
print_message "Installing frontend dependencies..."
|
||||||
|
rm -rf node_modules package-lock.json 2>/dev/null
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clear Vite cache for clean start
|
||||||
|
rm -rf node_modules/.vite 2>/dev/null || true
|
||||||
|
|
||||||
|
npm run dev -- --port 3010 > /tmp/data_entry_frontend.log 2>&1 &
|
||||||
|
|
||||||
|
for i in {1..15}; do
|
||||||
|
if check_port 3010; then
|
||||||
|
print_success "Frontend started on http://localhost:3010"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_error "Frontend failed to start"
|
||||||
|
cat /tmp/data_entry_frontend.log
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_backend() {
|
||||||
|
if check_port 8003; then
|
||||||
|
print_warning "Backend already running on port 8003"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message "Starting Backend (FastAPI)..."
|
||||||
|
cd "$SCRIPT_DIR/data-entry-app/backend/"
|
||||||
|
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
print_message "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
if ! python -c "import fastapi, uvicorn, sqlmodel" 2>/dev/null; then
|
||||||
|
print_message "Installing backend dependencies..."
|
||||||
|
pip install --upgrade pip > /dev/null 2>&1
|
||||||
|
pip install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p data/uploads
|
||||||
|
|
||||||
|
print_message "Running migrations..."
|
||||||
|
alembic upgrade head 2>/dev/null || print_warning "Migrations may already be applied"
|
||||||
|
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8003 --reload > /tmp/data_entry_backend.log 2>&1 &
|
||||||
|
|
||||||
|
for i in {1..20}; do
|
||||||
|
if check_port 8003; then
|
||||||
|
print_success "Backend started on http://localhost:8003"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_error "Backend failed to start"
|
||||||
|
cat /tmp/data_entry_backend.log
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
print_message "Stopping services..."
|
print_message "Stopping services..."
|
||||||
[[ -n $BACKEND_PID ]] && kill $BACKEND_PID 2>/dev/null || true
|
stop_frontend
|
||||||
[[ -n $FRONTEND_PID ]] && kill $FRONTEND_PID 2>/dev/null || true
|
stop_backend
|
||||||
./ssh_tunnel.sh stop 2>/dev/null || true
|
./ssh_tunnel.sh stop 2>/dev/null || true
|
||||||
print_success "All services stopped."
|
print_success "All services stopped."
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_services() {
|
stop_services() {
|
||||||
print_message "Stopping all Data Entry PRODUCTION services..."
|
local target=${1:-all}
|
||||||
|
|
||||||
if check_port 8003; then
|
case $target in
|
||||||
lsof -ti:8003 | xargs kill -TERM 2>/dev/null || true
|
frontend|fe|f)
|
||||||
sleep 2
|
stop_frontend
|
||||||
lsof -ti:8003 | xargs kill -KILL 2>/dev/null || true
|
;;
|
||||||
print_success "Backend stopped"
|
backend|be|b)
|
||||||
fi
|
stop_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
print_message "Stopping all Data Entry PRODUCTION services..."
|
||||||
|
stop_frontend
|
||||||
|
stop_backend
|
||||||
|
./ssh_tunnel.sh stop 2>/dev/null || true
|
||||||
|
print_success "All Data Entry PRODUCTION services stopped!"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
if check_port 3010; then
|
start_services() {
|
||||||
lsof -ti:3010 | xargs kill -TERM 2>/dev/null || true
|
local target=${1:-all}
|
||||||
sleep 2
|
|
||||||
lsof -ti:3010 | xargs kill -KILL 2>/dev/null || true
|
|
||||||
print_success "Frontend stopped"
|
|
||||||
fi
|
|
||||||
|
|
||||||
./ssh_tunnel.sh stop 2>/dev/null || true
|
case $target in
|
||||||
|
frontend|fe|f)
|
||||||
|
start_frontend
|
||||||
|
;;
|
||||||
|
backend|be|b)
|
||||||
|
# Ensure .env is correct
|
||||||
|
cp "$SCRIPT_DIR/data-entry-app/backend/.env.prod" "$SCRIPT_DIR/data-entry-app/backend/.env" 2>/dev/null || true
|
||||||
|
start_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
pkill -f "uvicorn.*data-entry" 2>/dev/null || true
|
restart_services() {
|
||||||
pkill -f "vite.*3010" 2>/dev/null || true
|
local target=${1:-all}
|
||||||
|
|
||||||
print_success "All Data Entry PRODUCTION services stopped!"
|
case $target in
|
||||||
exit 0
|
frontend|fe|f)
|
||||||
|
stop_frontend
|
||||||
|
sleep 1
|
||||||
|
start_frontend
|
||||||
|
;;
|
||||||
|
backend|be|b)
|
||||||
|
stop_backend
|
||||||
|
sleep 1
|
||||||
|
start_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
stop_services all
|
||||||
|
sleep 2
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
show_status() {
|
show_status() {
|
||||||
@@ -97,9 +253,21 @@ show_usage() {
|
|||||||
echo -e "${MAGENTA}Data Entry App - PRODUCTION Starter${NC}"
|
echo -e "${MAGENTA}Data Entry App - PRODUCTION Starter${NC}"
|
||||||
echo
|
echo
|
||||||
echo "Usage:"
|
echo "Usage:"
|
||||||
echo " ./start-data-entry-dev.sh Start all services"
|
echo " ./start-data-entry-dev.sh Start all services"
|
||||||
echo " ./start-data-entry-dev.sh stop Stop all services"
|
echo " ./start-data-entry-dev.sh stop [target] Stop services"
|
||||||
echo " ./start-data-entry-dev.sh status Show status"
|
echo " ./start-data-entry-dev.sh start [target] Start services"
|
||||||
|
echo " ./start-data-entry-dev.sh restart [target] Restart services"
|
||||||
|
echo " ./start-data-entry-dev.sh status Show status"
|
||||||
|
echo
|
||||||
|
echo "Targets:"
|
||||||
|
echo " frontend, fe, f Frontend only (Vue.js on port 3010)"
|
||||||
|
echo " backend, be, b Backend only (FastAPI on port 8003)"
|
||||||
|
echo " all (default) All services"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo " ./start-data-entry-dev.sh restart frontend Restart only frontend"
|
||||||
|
echo " ./start-data-entry-dev.sh stop backend Stop only backend"
|
||||||
|
echo " ./start-data-entry-dev.sh start fe Start frontend (short)"
|
||||||
echo
|
echo
|
||||||
echo "Environment:"
|
echo "Environment:"
|
||||||
echo " Oracle Server: 10.0.20.36 (PRODUCTION)"
|
echo " Oracle Server: 10.0.20.36 (PRODUCTION)"
|
||||||
@@ -109,116 +277,95 @@ show_usage() {
|
|||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_all() {
|
||||||
|
trap cleanup SIGINT SIGTERM
|
||||||
|
|
||||||
|
echo -e "${MAGENTA}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${MAGENTA} Data Entry App - PRODUCTION Environment${NC}"
|
||||||
|
echo -e "${MAGENTA} Oracle: 10.0.20.36 | DB: receipts_prod.db${NC}"
|
||||||
|
echo -e "${MAGENTA}═══════════════════════════════════════════${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Step 1: Stop any TEST tunnel and start PRODUCTION tunnel
|
||||||
|
print_message "1. Setting up SSH Tunnel (PRODUCTION)..."
|
||||||
|
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ./ssh_tunnel.sh start; then
|
||||||
|
print_success "SSH Tunnel to PRODUCTION (10.0.20.36) started"
|
||||||
|
else
|
||||||
|
print_error "Failed to start SSH tunnel"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Step 2: Copy PRODUCTION .env
|
||||||
|
print_message "2. Loading PRODUCTION environment..."
|
||||||
|
cp data-entry-app/backend/.env.prod data-entry-app/backend/.env
|
||||||
|
print_success "Loaded .env.prod (receipts_prod.db)"
|
||||||
|
|
||||||
|
# Step 3: Start Frontend
|
||||||
|
print_message "3. Starting Frontend..."
|
||||||
|
start_frontend
|
||||||
|
|
||||||
|
# Step 4: Start Backend
|
||||||
|
print_message "4. Starting Backend..."
|
||||||
|
start_backend
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN} Data Entry PRODUCTION Environment Ready!${NC}"
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo
|
||||||
|
echo -e "${BLUE}Services:${NC}"
|
||||||
|
echo " • SSH Tunnel: 10.0.20.36 (PRODUCTION)"
|
||||||
|
echo " • Backend: http://localhost:8003"
|
||||||
|
echo " • Frontend: http://localhost:3010"
|
||||||
|
echo " • API Docs: http://localhost:8003/docs"
|
||||||
|
echo
|
||||||
|
echo -e "${BLUE}Database:${NC}"
|
||||||
|
echo " • SQLite: data/receipts_prod.db"
|
||||||
|
echo " • Test Company: ROMFAST (company_id=114)"
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
case $1 in
|
case $1 in
|
||||||
stop) stop_services ;;
|
stop)
|
||||||
status) show_status; exit 0 ;;
|
stop_services "$2"
|
||||||
help|--help|-h) show_usage; exit 0 ;;
|
exit 0
|
||||||
"") ;; # Continue to start
|
;;
|
||||||
*) print_error "Unknown: $1"; show_usage; exit 1 ;;
|
start)
|
||||||
|
if [ -z "$2" ] || [ "$2" = "all" ]; then
|
||||||
|
start_all
|
||||||
|
else
|
||||||
|
start_services "$2"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_services "$2"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
show_status
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown command: $1"
|
||||||
|
show_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
trap cleanup SIGINT SIGTERM
|
|
||||||
|
|
||||||
echo -e "${MAGENTA}═══════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${MAGENTA} Data Entry App - PRODUCTION Environment${NC}"
|
|
||||||
echo -e "${MAGENTA} Oracle: 10.0.20.36 | DB: receipts_prod.db${NC}"
|
|
||||||
echo -e "${MAGENTA}═══════════════════════════════════════════${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Step 1: Stop any TEST tunnel and start PRODUCTION tunnel
|
|
||||||
print_message "1. Setting up SSH Tunnel (PRODUCTION)..."
|
|
||||||
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
if ./ssh_tunnel.sh start; then
|
|
||||||
print_success "SSH Tunnel to PRODUCTION (10.0.20.36) started"
|
|
||||||
else
|
|
||||||
print_error "Failed to start SSH tunnel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Step 2: Copy PRODUCTION .env
|
|
||||||
print_message "2. Loading PRODUCTION environment..."
|
|
||||||
cp data-entry-app/backend/.env.prod data-entry-app/backend/.env
|
|
||||||
print_success "Loaded .env.prod (receipts_prod.db)"
|
|
||||||
|
|
||||||
# Step 3: Start Frontend
|
|
||||||
print_message "3. Starting Frontend (Vue.js)..."
|
|
||||||
|
|
||||||
cd data-entry-app/frontend/
|
|
||||||
|
|
||||||
if [ ! -d "node_modules" ] || [ -f "node_modules/.bin/vite.cmd" ]; then
|
|
||||||
print_message "Installing frontend dependencies..."
|
|
||||||
rm -rf node_modules package-lock.json 2>/dev/null
|
|
||||||
npm install
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm run dev -- --port 3010 > /tmp/data_entry_frontend.log 2>&1 &
|
|
||||||
FRONTEND_PID=$!
|
|
||||||
|
|
||||||
for i in {1..15}; do
|
|
||||||
if check_port 3010; then
|
|
||||||
print_success "Frontend started on http://localhost:3010"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
[ $i -eq 15 ] && { print_error "Frontend failed"; cat /tmp/data_entry_frontend.log; cleanup; }
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Step 4: Start Backend
|
|
||||||
print_message "4. Starting Backend (FastAPI)..."
|
|
||||||
|
|
||||||
cd ../backend/
|
|
||||||
|
|
||||||
if [ ! -d "venv" ]; then
|
|
||||||
print_message "Creating virtual environment..."
|
|
||||||
python3 -m venv venv
|
|
||||||
fi
|
|
||||||
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
if ! python -c "import fastapi, uvicorn, sqlmodel" 2>/dev/null; then
|
|
||||||
print_message "Installing backend dependencies..."
|
|
||||||
pip install --upgrade pip > /dev/null 2>&1
|
|
||||||
pip install -r requirements.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p data/uploads
|
|
||||||
|
|
||||||
print_message "Running migrations..."
|
|
||||||
alembic upgrade head 2>/dev/null || print_warning "Migrations may already be applied"
|
|
||||||
|
|
||||||
uvicorn app.main:app --host 0.0.0.0 --port 8003 --reload > /tmp/data_entry_backend.log 2>&1 &
|
|
||||||
BACKEND_PID=$!
|
|
||||||
|
|
||||||
for i in {1..20}; do
|
|
||||||
if check_port 8003; then
|
|
||||||
print_success "Backend started on http://localhost:8003"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
[ $i -eq 20 ] && { print_error "Backend failed"; cat /tmp/data_entry_backend.log; cleanup; }
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo
|
|
||||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${GREEN} Data Entry PRODUCTION Environment Ready!${NC}"
|
|
||||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo
|
|
||||||
echo -e "${BLUE}Services:${NC}"
|
|
||||||
echo " • SSH Tunnel: 10.0.20.36 (PRODUCTION)"
|
|
||||||
echo " • Backend: http://localhost:8003"
|
|
||||||
echo " • Frontend: http://localhost:3010"
|
|
||||||
echo " • API Docs: http://localhost:8003/docs"
|
|
||||||
echo
|
|
||||||
echo -e "${BLUE}Database:${NC}"
|
|
||||||
echo " • SQLite: data/receipts_prod.db"
|
|
||||||
echo " • Test Company: ROMFAST (company_id=114)"
|
|
||||||
echo
|
|
||||||
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
wait
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ BLUE='\033[0;34m'
|
|||||||
CYAN='\033[0;36m'
|
CYAN='\033[0;36m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
print_message() { echo -e "${CYAN}[DATA-ENTRY-TEST]${NC} $1"; }
|
print_message() { echo -e "${CYAN}[DATA-ENTRY-TEST]${NC} $1"; }
|
||||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
@@ -25,39 +29,191 @@ check_port() {
|
|||||||
lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1
|
lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Individual service functions
|
||||||
|
stop_frontend() {
|
||||||
|
if check_port 3010; then
|
||||||
|
print_message "Stopping frontend..."
|
||||||
|
lsof -ti:3010 | xargs kill -TERM 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
lsof -ti:3010 | xargs kill -KILL 2>/dev/null || true
|
||||||
|
pkill -f "vite.*3010" 2>/dev/null || true
|
||||||
|
print_success "Frontend stopped"
|
||||||
|
else
|
||||||
|
print_warning "Frontend not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_backend() {
|
||||||
|
if check_port 8003; then
|
||||||
|
print_message "Stopping backend..."
|
||||||
|
lsof -ti:8003 | xargs kill -TERM 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
lsof -ti:8003 | xargs kill -KILL 2>/dev/null || true
|
||||||
|
pkill -f "uvicorn.*8003" 2>/dev/null || true
|
||||||
|
print_success "Backend stopped"
|
||||||
|
else
|
||||||
|
print_warning "Backend not running"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_frontend() {
|
||||||
|
if check_port 3010; then
|
||||||
|
print_warning "Frontend already running on port 3010"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message "Starting Frontend (Vue.js)..."
|
||||||
|
cd "$SCRIPT_DIR/data-entry-app/frontend/"
|
||||||
|
|
||||||
|
if [ ! -d "node_modules" ] || [ -f "node_modules/.bin/vite.cmd" ]; then
|
||||||
|
print_message "Installing frontend dependencies..."
|
||||||
|
rm -rf node_modules package-lock.json 2>/dev/null
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clear Vite cache for clean start
|
||||||
|
rm -rf node_modules/.vite 2>/dev/null || true
|
||||||
|
|
||||||
|
npm run dev -- --port 3010 > /tmp/data_entry_frontend.log 2>&1 &
|
||||||
|
|
||||||
|
for i in {1..15}; do
|
||||||
|
if check_port 3010; then
|
||||||
|
print_success "Frontend started on http://localhost:3010"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_error "Frontend failed to start"
|
||||||
|
cat /tmp/data_entry_frontend.log
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_backend() {
|
||||||
|
if check_port 8003; then
|
||||||
|
print_warning "Backend already running on port 8003"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message "Starting Backend (FastAPI)..."
|
||||||
|
cd "$SCRIPT_DIR/data-entry-app/backend/"
|
||||||
|
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
print_message "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
if ! python -c "import fastapi, uvicorn, sqlmodel" 2>/dev/null; then
|
||||||
|
print_message "Installing backend dependencies..."
|
||||||
|
pip install --upgrade pip > /dev/null 2>&1
|
||||||
|
pip install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p data/uploads
|
||||||
|
|
||||||
|
print_message "Running migrations..."
|
||||||
|
alembic upgrade head 2>/dev/null || print_warning "Migrations may already be applied"
|
||||||
|
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8003 --reload > /tmp/data_entry_backend.log 2>&1 &
|
||||||
|
|
||||||
|
for i in {1..20}; do
|
||||||
|
if check_port 8003; then
|
||||||
|
print_success "Backend started on http://localhost:8003"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
print_error "Backend failed to start"
|
||||||
|
cat /tmp/data_entry_backend.log
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
print_message "Stopping services..."
|
print_message "Stopping services..."
|
||||||
[[ -n $BACKEND_PID ]] && kill $BACKEND_PID 2>/dev/null || true
|
stop_frontend
|
||||||
[[ -n $FRONTEND_PID ]] && kill $FRONTEND_PID 2>/dev/null || true
|
stop_backend
|
||||||
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
||||||
print_success "All services stopped."
|
print_success "All services stopped."
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_services() {
|
stop_services() {
|
||||||
print_message "Stopping all Data Entry TEST services..."
|
local target=${1:-all}
|
||||||
|
|
||||||
if check_port 8003; then
|
case $target in
|
||||||
lsof -ti:8003 | xargs kill -TERM 2>/dev/null || true
|
frontend|fe|f)
|
||||||
sleep 2
|
stop_frontend
|
||||||
lsof -ti:8003 | xargs kill -KILL 2>/dev/null || true
|
;;
|
||||||
print_success "Backend stopped"
|
backend|be|b)
|
||||||
fi
|
stop_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
print_message "Stopping all Data Entry TEST services..."
|
||||||
|
stop_frontend
|
||||||
|
stop_backend
|
||||||
|
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
||||||
|
print_success "All Data Entry TEST services stopped!"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
if check_port 3010; then
|
start_services() {
|
||||||
lsof -ti:3010 | xargs kill -TERM 2>/dev/null || true
|
local target=${1:-all}
|
||||||
sleep 2
|
|
||||||
lsof -ti:3010 | xargs kill -KILL 2>/dev/null || true
|
|
||||||
print_success "Frontend stopped"
|
|
||||||
fi
|
|
||||||
|
|
||||||
./ssh-tunnel-test.sh stop 2>/dev/null || true
|
case $target in
|
||||||
|
frontend|fe|f)
|
||||||
|
start_frontend
|
||||||
|
;;
|
||||||
|
backend|be|b)
|
||||||
|
# Ensure .env is correct
|
||||||
|
cp "$SCRIPT_DIR/data-entry-app/backend/.env.test" "$SCRIPT_DIR/data-entry-app/backend/.env" 2>/dev/null || true
|
||||||
|
start_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
pkill -f "uvicorn.*data-entry" 2>/dev/null || true
|
restart_services() {
|
||||||
pkill -f "vite.*3010" 2>/dev/null || true
|
local target=${1:-all}
|
||||||
|
|
||||||
print_success "All Data Entry TEST services stopped!"
|
case $target in
|
||||||
exit 0
|
frontend|fe|f)
|
||||||
|
stop_frontend
|
||||||
|
sleep 1
|
||||||
|
start_frontend
|
||||||
|
;;
|
||||||
|
backend|be|b)
|
||||||
|
stop_backend
|
||||||
|
sleep 1
|
||||||
|
start_backend
|
||||||
|
;;
|
||||||
|
all|"")
|
||||||
|
stop_services all
|
||||||
|
sleep 2
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown target: $target (use: frontend, backend, all)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
show_status() {
|
show_status() {
|
||||||
@@ -97,9 +253,21 @@ show_usage() {
|
|||||||
echo -e "${CYAN}Data Entry App - TEST Starter${NC}"
|
echo -e "${CYAN}Data Entry App - TEST Starter${NC}"
|
||||||
echo
|
echo
|
||||||
echo "Usage:"
|
echo "Usage:"
|
||||||
echo " ./start-data-entry-test.sh Start all services"
|
echo " ./start-data-entry-test.sh Start all services"
|
||||||
echo " ./start-data-entry-test.sh stop Stop all services"
|
echo " ./start-data-entry-test.sh stop [target] Stop services"
|
||||||
echo " ./start-data-entry-test.sh status Show status"
|
echo " ./start-data-entry-test.sh start [target] Start services"
|
||||||
|
echo " ./start-data-entry-test.sh restart [target] Restart services"
|
||||||
|
echo " ./start-data-entry-test.sh status Show status"
|
||||||
|
echo
|
||||||
|
echo "Targets:"
|
||||||
|
echo " frontend, fe, f Frontend only (Vue.js on port 3010)"
|
||||||
|
echo " backend, be, b Backend only (FastAPI on port 8003)"
|
||||||
|
echo " all (default) All services"
|
||||||
|
echo
|
||||||
|
echo "Examples:"
|
||||||
|
echo " ./start-data-entry-test.sh restart frontend Restart only frontend"
|
||||||
|
echo " ./start-data-entry-test.sh stop backend Stop only backend"
|
||||||
|
echo " ./start-data-entry-test.sh start fe Start frontend (short)"
|
||||||
echo
|
echo
|
||||||
echo "Environment:"
|
echo "Environment:"
|
||||||
echo " Oracle Server: 10.0.20.121 (TEST)"
|
echo " Oracle Server: 10.0.20.121 (TEST)"
|
||||||
@@ -109,116 +277,95 @@ show_usage() {
|
|||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start_all() {
|
||||||
|
trap cleanup SIGINT SIGTERM
|
||||||
|
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${CYAN} Data Entry App - TEST Environment${NC}"
|
||||||
|
echo -e "${CYAN} Oracle: 10.0.20.121 | DB: receipts_test.db${NC}"
|
||||||
|
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Step 1: Stop any PRODUCTION tunnel and start TEST tunnel
|
||||||
|
print_message "1. Setting up SSH Tunnel (TEST)..."
|
||||||
|
./ssh_tunnel.sh stop 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ./ssh-tunnel-test.sh start; then
|
||||||
|
print_success "SSH Tunnel to TEST (10.0.20.121) started"
|
||||||
|
else
|
||||||
|
print_error "Failed to start SSH tunnel"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Step 2: Copy TEST .env
|
||||||
|
print_message "2. Loading TEST environment..."
|
||||||
|
cp data-entry-app/backend/.env.test data-entry-app/backend/.env
|
||||||
|
print_success "Loaded .env.test (receipts_test.db)"
|
||||||
|
|
||||||
|
# Step 3: Start Frontend
|
||||||
|
print_message "3. Starting Frontend..."
|
||||||
|
start_frontend
|
||||||
|
|
||||||
|
# Step 4: Start Backend
|
||||||
|
print_message "4. Starting Backend..."
|
||||||
|
start_backend
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN} Data Entry TEST Environment Ready!${NC}"
|
||||||
|
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||||
|
echo
|
||||||
|
echo -e "${CYAN}Services:${NC}"
|
||||||
|
echo " • SSH Tunnel: 10.0.20.121 (TEST)"
|
||||||
|
echo " • Backend: http://localhost:8003"
|
||||||
|
echo " • Frontend: http://localhost:3010"
|
||||||
|
echo " • API Docs: http://localhost:8003/docs"
|
||||||
|
echo
|
||||||
|
echo -e "${CYAN}Database:${NC}"
|
||||||
|
echo " • SQLite: data/receipts_test.db"
|
||||||
|
echo " • Test Company: MARIUSM_AUTO (company_id=110)"
|
||||||
|
echo
|
||||||
|
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
case $1 in
|
case $1 in
|
||||||
stop) stop_services ;;
|
stop)
|
||||||
status) show_status; exit 0 ;;
|
stop_services "$2"
|
||||||
help|--help|-h) show_usage; exit 0 ;;
|
exit 0
|
||||||
"") ;; # Continue to start
|
;;
|
||||||
*) print_error "Unknown: $1"; show_usage; exit 1 ;;
|
start)
|
||||||
|
if [ -z "$2" ] || [ "$2" = "all" ]; then
|
||||||
|
start_all
|
||||||
|
else
|
||||||
|
start_services "$2"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_services "$2"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
show_status
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
start_all
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown command: $1"
|
||||||
|
show_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
trap cleanup SIGINT SIGTERM
|
|
||||||
|
|
||||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${CYAN} Data Entry App - TEST Environment${NC}"
|
|
||||||
echo -e "${CYAN} Oracle: 10.0.20.121 | DB: receipts_test.db${NC}"
|
|
||||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Step 1: Stop any PRODUCTION tunnel and start TEST tunnel
|
|
||||||
print_message "1. Setting up SSH Tunnel (TEST)..."
|
|
||||||
./ssh_tunnel.sh stop 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
if ./ssh-tunnel-test.sh start; then
|
|
||||||
print_success "SSH Tunnel to TEST (10.0.20.121) started"
|
|
||||||
else
|
|
||||||
print_error "Failed to start SSH tunnel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Step 2: Copy TEST .env
|
|
||||||
print_message "2. Loading TEST environment..."
|
|
||||||
cp data-entry-app/backend/.env.test data-entry-app/backend/.env
|
|
||||||
print_success "Loaded .env.test (receipts_test.db)"
|
|
||||||
|
|
||||||
# Step 3: Start Frontend
|
|
||||||
print_message "3. Starting Frontend (Vue.js)..."
|
|
||||||
|
|
||||||
cd data-entry-app/frontend/
|
|
||||||
|
|
||||||
if [ ! -d "node_modules" ] || [ -f "node_modules/.bin/vite.cmd" ]; then
|
|
||||||
print_message "Installing frontend dependencies..."
|
|
||||||
rm -rf node_modules package-lock.json 2>/dev/null
|
|
||||||
npm install
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm run dev -- --port 3010 > /tmp/data_entry_frontend.log 2>&1 &
|
|
||||||
FRONTEND_PID=$!
|
|
||||||
|
|
||||||
for i in {1..15}; do
|
|
||||||
if check_port 3010; then
|
|
||||||
print_success "Frontend started on http://localhost:3010"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
[ $i -eq 15 ] && { print_error "Frontend failed"; cat /tmp/data_entry_frontend.log; cleanup; }
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Step 4: Start Backend
|
|
||||||
print_message "4. Starting Backend (FastAPI)..."
|
|
||||||
|
|
||||||
cd ../backend/
|
|
||||||
|
|
||||||
if [ ! -d "venv" ]; then
|
|
||||||
print_message "Creating virtual environment..."
|
|
||||||
python3 -m venv venv
|
|
||||||
fi
|
|
||||||
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
if ! python -c "import fastapi, uvicorn, sqlmodel" 2>/dev/null; then
|
|
||||||
print_message "Installing backend dependencies..."
|
|
||||||
pip install --upgrade pip > /dev/null 2>&1
|
|
||||||
pip install -r requirements.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p data/uploads
|
|
||||||
|
|
||||||
print_message "Running migrations..."
|
|
||||||
alembic upgrade head 2>/dev/null || print_warning "Migrations may already be applied"
|
|
||||||
|
|
||||||
uvicorn app.main:app --host 0.0.0.0 --port 8003 --reload > /tmp/data_entry_backend.log 2>&1 &
|
|
||||||
BACKEND_PID=$!
|
|
||||||
|
|
||||||
for i in {1..20}; do
|
|
||||||
if check_port 8003; then
|
|
||||||
print_success "Backend started on http://localhost:8003"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
[ $i -eq 20 ] && { print_error "Backend failed"; cat /tmp/data_entry_backend.log; cleanup; }
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo
|
|
||||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo -e "${GREEN} Data Entry TEST Environment Ready!${NC}"
|
|
||||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
|
||||||
echo
|
|
||||||
echo -e "${CYAN}Services:${NC}"
|
|
||||||
echo " • SSH Tunnel: 10.0.20.121 (TEST)"
|
|
||||||
echo " • Backend: http://localhost:8003"
|
|
||||||
echo " • Frontend: http://localhost:3010"
|
|
||||||
echo " • API Docs: http://localhost:8003/docs"
|
|
||||||
echo
|
|
||||||
echo -e "${CYAN}Database:${NC}"
|
|
||||||
echo " • SQLite: data/receipts_test.db"
|
|
||||||
echo " • Test Company: MARIUSM_AUTO (company_id=110)"
|
|
||||||
echo
|
|
||||||
echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
wait
|
|
||||||
|
|||||||
Reference in New Issue
Block a user