270 lines
8.1 KiB
Go
270 lines
8.1 KiB
Go
package loan
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// UploadPDF reçoit un PDF, tente de le parser via Python si disponible,
|
|
// sinon crée le prêt avec saisie manuelle
|
|
func (h *Handler) UploadPDF(w http.ResponseWriter, r *http.Request) {
|
|
r.ParseMultipartForm(20 << 20)
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
http.Error(w, "fichier PDF requis", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
propertyID := r.FormValue("property_id")
|
|
if propertyID == "" {
|
|
http.Error(w, "property_id requis", http.StatusBadRequest)
|
|
return
|
|
}
|
|
labelInput := r.FormValue("label")
|
|
|
|
var buf bytes.Buffer
|
|
buf.ReadFrom(file)
|
|
pdfBytes := buf.Bytes()
|
|
|
|
// Tenter l'extraction Python (optionnel)
|
|
ref, initialAmount, monthly := extractInfoFallback(pdfBytes)
|
|
|
|
label := labelInput
|
|
if label == "" {
|
|
if ref != "" {
|
|
label = fmt.Sprintf("Prêt %s", ref)
|
|
} else {
|
|
label = "Prêt immobilier"
|
|
}
|
|
}
|
|
|
|
loan := &Loan{
|
|
PropertyID: propertyID,
|
|
Label: label,
|
|
Reference: ref,
|
|
InitialAmount: initialAmount,
|
|
MonthlyPayment: monthly,
|
|
}
|
|
if err := h.store.CreateLoan(loan); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Tenter le parsing des lignes via Python
|
|
lines, parseErr := parseLinesWithPython(pdfBytes)
|
|
linesImported := 0
|
|
|
|
if parseErr == nil && len(lines) > 0 {
|
|
h.store.InsertLines(loan.ID, lines)
|
|
linesImported = len(lines)
|
|
} else {
|
|
// Pas de Python : essayer les données embarquées selon la référence
|
|
switch {
|
|
case strings.Contains(ref, "781495"):
|
|
lines = GetLoan781495Lines()
|
|
case strings.Contains(ref, "781728"):
|
|
lines = GetLoan781728Lines()
|
|
}
|
|
if len(lines) > 0 {
|
|
h.store.InsertLines(loan.ID, lines)
|
|
linesImported = len(lines)
|
|
}
|
|
}
|
|
|
|
respond(w, map[string]any{
|
|
"loan": loan,
|
|
"lines_imported": linesImported,
|
|
"python_used": parseErr == nil,
|
|
})
|
|
}
|
|
|
|
// extractInfoFallback tente d'extraire les infos via Python, retourne des zéros si indisponible
|
|
func extractInfoFallback(pdfBytes []byte) (ref string, initialAmount, monthly float64) {
|
|
script := `
|
|
import sys, json, pdfplumber, io, re
|
|
data = sys.stdin.buffer.read()
|
|
result = {'reference': '', 'initial_amount': 0, 'monthly': 0}
|
|
with pdfplumber.open(io.BytesIO(data)) as pdf:
|
|
text = pdf.pages[0].extract_text() or ''
|
|
m = re.search(r'cr.dit\s*:\s*([\w]+)', text)
|
|
if m: result['reference'] = m.group(1).strip()
|
|
m = re.search(r'Montant du pr.t\s*:\s*([\d\s,\.]+)\s*EUR', text)
|
|
if m:
|
|
try: result['initial_amount'] = float(m.group(1).strip().replace(' ','').replace('\u202f','').replace(',','.'))
|
|
except: pass
|
|
for page in pdf.pages:
|
|
for table in (page.extract_tables() or []):
|
|
if not table: continue
|
|
for row in table[1:]:
|
|
if not row or not row[2]: continue
|
|
vals=[v.strip() for v in str(row[2]).split('\n') if v.strip()]
|
|
if len(vals)>=3 and len(set(vals[:3]))==1:
|
|
try:
|
|
result['monthly']=float(vals[0].replace(' ','').replace('\u202f','').replace(',','.'))
|
|
break
|
|
except: pass
|
|
if result['monthly']: break
|
|
if result['monthly']: break
|
|
print(json.dumps(result))
|
|
`
|
|
py := pythonBin()
|
|
if py == "" {
|
|
return
|
|
}
|
|
cmd := exec.Command(py, "-c", script)
|
|
cmd.Stdin = bytes.NewReader(pdfBytes)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
if cmd.Run() != nil {
|
|
return
|
|
}
|
|
var info struct {
|
|
Reference string `json:"reference"`
|
|
InitialAmount float64 `json:"initial_amount"`
|
|
Monthly float64 `json:"monthly"`
|
|
}
|
|
if json.Unmarshal(out.Bytes(), &info) != nil {
|
|
return
|
|
}
|
|
r := info.Reference
|
|
if idx := strings.Index(r, "/"); idx > 0 {
|
|
r = strings.TrimSpace(r[:idx])
|
|
}
|
|
return r, info.InitialAmount, info.Monthly
|
|
}
|
|
|
|
// parseLinesWithPython tente le parsing complet via pdfplumber
|
|
func parseLinesWithPython(pdfBytes []byte) ([]LoanLine, error) {
|
|
py := pythonBin()
|
|
if py == "" {
|
|
return nil, fmt.Errorf("python non disponible")
|
|
}
|
|
script := `
|
|
import sys,json,pdfplumber,io
|
|
def pa(s):
|
|
if not s or not s.strip(): return 0.0
|
|
try: return float(s.strip().replace(' ','').replace('\u202f','').replace('\xa0','').replace(',','.'))
|
|
except: return 0.0
|
|
def pd(s):
|
|
s=s.strip()
|
|
if '/' in s:
|
|
p=s.split('/')
|
|
if len(p)==3: return f"{p[2]}-{p[1]}-{p[0]}"
|
|
return s
|
|
lines=[]
|
|
with pdfplumber.open(io.BytesIO(sys.stdin.buffer.read())) as pdf:
|
|
for page in pdf.pages:
|
|
for table in (page.extract_tables() or []):
|
|
if not table or len(table)<2: continue
|
|
if not table[0] or 'RANG' not in str(table[0][0]): continue
|
|
for row in table[1:]:
|
|
if not row or not row[0]: continue
|
|
ranks=str(row[0]).split('\n'); dates=str(row[1]).split('\n') if row[1] else []
|
|
tots=str(row[2]).split('\n') if row[2] else []; caps=str(row[3]).split('\n') if row[3] else []
|
|
ints=str(row[4]).split('\n') if row[4] else []; rems=str(row[5]).split('\n') if row[5] else []
|
|
for i,rs in enumerate(ranks):
|
|
rs=rs.strip()
|
|
if not rs or not rs.isdigit(): continue
|
|
c=pa(caps[i] if i<len(caps) else '0')
|
|
if c==0: continue
|
|
lines.append({'rank':int(rs),'due_date':pd(dates[i].strip() if i<len(dates) else ''),
|
|
'total_amount':pa(tots[i] if i<len(tots) else '0'),'capital':c,
|
|
'interest':pa(ints[i] if i<len(ints) else '0'),
|
|
'remaining_capital':pa(rems[i] if i<len(rems) else '0')})
|
|
print(json.dumps(lines))
|
|
`
|
|
cmd := exec.Command(py, "-c", script)
|
|
cmd.Stdin = bytes.NewReader(pdfBytes)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, err
|
|
}
|
|
var raw []struct {
|
|
Rank int `json:"rank"`
|
|
DueDate string `json:"due_date"`
|
|
TotalAmount float64 `json:"total_amount"`
|
|
Capital float64 `json:"capital"`
|
|
Interest float64 `json:"interest"`
|
|
RemainingCapital float64 `json:"remaining_capital"`
|
|
}
|
|
if err := json.Unmarshal(out.Bytes(), &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
lines := make([]LoanLine, len(raw))
|
|
for i, r := range raw {
|
|
lines[i] = LoanLine{Rank: r.Rank, DueDate: r.DueDate, TotalAmount: r.TotalAmount,
|
|
Capital: r.Capital, Interest: r.Interest, RemainingCapital: r.RemainingCapital}
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
func pythonBin() string {
|
|
for _, name := range []string{"python3", "python"} {
|
|
if _, err := exec.LookPath(name); err == nil {
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func respondPDF(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// CreateLoanManual crée un prêt depuis un formulaire JSON
|
|
func (h *Handler) CreateLoanManual(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
PropertyID string `json:"property_id"`
|
|
Label string `json:"label"`
|
|
Reference string `json:"reference"`
|
|
InitialAmount float64 `json:"initial_amount"`
|
|
MonthlyPayment float64 `json:"monthly_payment"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
http.Error(w, "invalid body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
loan := &Loan{
|
|
PropertyID: payload.PropertyID,
|
|
Label: payload.Label,
|
|
Reference: payload.Reference,
|
|
InitialAmount: payload.InitialAmount,
|
|
MonthlyPayment: payload.MonthlyPayment,
|
|
}
|
|
if err := h.store.CreateLoan(loan); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Charger les données embarquées selon la référence
|
|
var lines []LoanLine
|
|
switch {
|
|
case strings.Contains(payload.Reference, "781495"):
|
|
lines = GetLoan781495Lines()
|
|
case strings.Contains(payload.Reference, "781728"):
|
|
lines = GetLoan781728Lines()
|
|
}
|
|
linesImported := 0
|
|
if len(lines) > 0 {
|
|
h.store.InsertLines(loan.ID, lines)
|
|
linesImported = len(lines)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"loan": loan,
|
|
"lines_imported": linesImported,
|
|
})
|
|
}
|
|
|
|
var _ = mux.Vars // éviter unused import
|