Prevent duplicate import
This commit is contained in:
@ -3,22 +3,25 @@ import logging
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Security, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..core.security import get_current_active_user as get_user
|
||||
from ..db.session import SessionDep
|
||||
from ..models.account_base import AccountBase
|
||||
from ..models.journal import Journal
|
||||
from ..models.validations import check_journals_are_valid
|
||||
from ..models.voucher import Voucher
|
||||
from ..models.voucher_type import VoucherType
|
||||
from ..schemas.user import UserToken
|
||||
from . import get_lock_info
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@ -158,7 +161,7 @@ async def preview_import(
|
||||
if mapping.narration_col and mapping.narration_col in row:
|
||||
val = row.get(mapping.narration_col)
|
||||
if not pd.isna(val):
|
||||
narration = str(val)
|
||||
narration = str(val).strip()
|
||||
|
||||
# 4. Amount / Debit / Credit
|
||||
debit = 0.0
|
||||
@ -211,13 +214,18 @@ async def execute_import(
|
||||
raise HTTPException(status_code=400, detail="Primary account not found.")
|
||||
|
||||
created_vouchers = []
|
||||
processed_rows = []
|
||||
|
||||
for row in request.rows:
|
||||
if not row.is_valid or not row.date_ or not row.account_id:
|
||||
processed_rows.append(row)
|
||||
continue
|
||||
|
||||
other_acc = db.execute(select(AccountBase).where(AccountBase.id == row.account_id)).scalar_one_or_none()
|
||||
if not other_acc:
|
||||
row.is_valid = False
|
||||
row.validation_error = "Other account not found"
|
||||
processed_rows.append(row)
|
||||
continue
|
||||
|
||||
voucher = Voucher(
|
||||
@ -227,11 +235,50 @@ async def execute_import(
|
||||
user_id=user.id_,
|
||||
voucher_type=VoucherType.JOURNAL,
|
||||
)
|
||||
db.add(voucher)
|
||||
|
||||
# Check for duplicate
|
||||
existing_vouchers = (
|
||||
db.execute(
|
||||
select(Voucher).where(Voucher.date_ == row.date_, func.trim(Voucher.narration) == row.narration.strip())
|
||||
)
|
||||
.scalars()
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
is_duplicate = False
|
||||
expected_amount = Decimal(str(row.debit if row.debit > 0 else row.credit)).quantize(Decimal("0.01"))
|
||||
expected_p_debit = 1 if row.debit > 0 else -1
|
||||
expected_o_debit = -1 if row.debit > 0 else 1
|
||||
|
||||
for v in existing_vouchers:
|
||||
if len(v.journals) != 2:
|
||||
continue
|
||||
|
||||
p_jnl = next((j for j in v.journals if str(j.account_id) == str(primary_acc.id)), None)
|
||||
o_jnl = next((j for j in v.journals if str(j.account_id) == str(other_acc.id)), None)
|
||||
|
||||
if not p_jnl or not o_jnl:
|
||||
continue
|
||||
|
||||
if (
|
||||
p_jnl.amount.quantize(Decimal("0.01")) == expected_amount
|
||||
and o_jnl.amount.quantize(Decimal("0.01")) == expected_amount
|
||||
and p_jnl.debit == expected_p_debit
|
||||
and o_jnl.debit == expected_o_debit
|
||||
):
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if is_duplicate:
|
||||
row.is_valid = False
|
||||
row.validation_error = "Duplicate"
|
||||
processed_rows.append(row)
|
||||
continue
|
||||
|
||||
# Determine signs for journals
|
||||
amount = expected_amount
|
||||
if row.debit > 0:
|
||||
amount = row.debit
|
||||
# Primary gets Debit (1)
|
||||
j_primary = Journal(
|
||||
amount=amount,
|
||||
@ -247,7 +294,6 @@ async def execute_import(
|
||||
cost_centre_id=other_acc.cost_centre_id,
|
||||
)
|
||||
else:
|
||||
amount = row.credit
|
||||
# Primary gets Credit (-1)
|
||||
j_primary = Journal(
|
||||
amount=amount,
|
||||
@ -265,9 +311,39 @@ async def execute_import(
|
||||
|
||||
voucher.journals.append(j_primary)
|
||||
voucher.journals.append(j_other)
|
||||
|
||||
# Validate Lockout Dates
|
||||
account_types = [primary_acc.type_id, other_acc.type_id]
|
||||
allowed, message = get_lock_info([row.date_], VoucherType.JOURNAL, account_types, db)
|
||||
if not allowed:
|
||||
row.is_valid = False
|
||||
row.validation_error = f"Locked: {message}"
|
||||
processed_rows.append(row)
|
||||
continue
|
||||
|
||||
# Validate Journal integrity
|
||||
try:
|
||||
check_journals_are_valid(voucher)
|
||||
except HTTPException as e:
|
||||
row.is_valid = False
|
||||
row.validation_error = f"Validation Failed: {e.detail}"
|
||||
processed_rows.append(row)
|
||||
continue
|
||||
|
||||
db.add(voucher)
|
||||
db.add(j_primary)
|
||||
db.add(j_other)
|
||||
created_vouchers.append(voucher)
|
||||
|
||||
row.is_valid = True
|
||||
row.validation_error = "Imported"
|
||||
processed_rows.append(row)
|
||||
|
||||
db.commit()
|
||||
return {"message": f"Successfully imported {len(created_vouchers)} entries."}
|
||||
msg = f"Successfully imported {len(created_vouchers)} entries."
|
||||
|
||||
skipped_count = len([r for r in processed_rows if not r.is_valid])
|
||||
if skipped_count > 0:
|
||||
msg += f" Skipped {skipped_count} entries."
|
||||
|
||||
return {"message": msg, "processed_rows": [r.model_dump() for r in processed_rows]}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<h1 mat-dialog-title>{{ data.title }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<div mat-dialog-content style="white-space: pre-wrap">
|
||||
{{ data.content }}
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
|
||||
@ -262,8 +262,9 @@ export class TransactionImportComponent {
|
||||
|
||||
this.importService.execute(req).subscribe({
|
||||
next: (res) => {
|
||||
this.snackBar.open(res.message, 'Close', { duration: 3000 });
|
||||
this.router.navigate(['/daybook']);
|
||||
this.snackBar.open(res.message, 'Close', { duration: 5000 });
|
||||
this.previewRows.set(res.processed_rows || []);
|
||||
this.selection.clear();
|
||||
},
|
||||
error: (err) => {
|
||||
this.snackBar.open('Error importing transactions: ' + err.message, 'Close', { duration: 3000 });
|
||||
|
||||
@ -62,11 +62,12 @@ export class TransactionImportService {
|
||||
}>;
|
||||
}
|
||||
|
||||
execute(request: ImportExecutionRequest): Observable<{ message: string }> {
|
||||
execute(request: ImportExecutionRequest): Observable<{ message: string; processed_rows?: PreviewRow[] }> {
|
||||
return this.http
|
||||
.post<{ message: string }>(`${url}/execute`, request)
|
||||
.post<{ message: string; processed_rows?: PreviewRow[] }>(`${url}/execute`, request)
|
||||
.pipe(catchError(this.log.handleError('TransactionImportService', 'execute'))) as Observable<{
|
||||
message: string;
|
||||
processed_rows?: PreviewRow[];
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user