Files

610 lines
22 KiB
Python
Raw Permalink Normal View History

2026-02-25 10:24:12 +01:00
from flask import Flask, request, render_template, send_file, render_template_string,abort, Response, redirect, url_for
from werkzeug.middleware.proxy_fix import ProxyFix
# from waitress import serve
import os
import json
import io
import re
import base64
import lzma
from io import BytesIO
from barcode import Code39 as BarCodeObj
from barcode.writer import ImageWriter
import qrcode
from datetime import date, timedelta, datetime
from xhtml2pdf import pisa
import zipfile
import datetime
from localconfig import *
import csv
from lib_poslog import process_invoice_request, etree_to_dict, get_trxlink_from_dict
import xml.etree.ElementTree as ET
from models import db, StateOfDate, StateOfInvoice
from pprint import pprint
from lib_poslog import process_invoice_request, etree_to_dict
from collections import deque
MAXZIPSIZE = 100 # max number of files in a zipfile to download
app = Flask(__name__)
app.config["SECRET_KEY"] = "lkjasfuhf83749yow8urehfkuresa"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///state.db"
db.init_app(app)
with app.app_context():
db.create_all()
@app.route('/invoice', methods=['GET'])
def home():
return render_template('home.html')
@app.route('/invoice/download', methods=['GET'])
def download():
# when all is done, read the ecosio folder and show all downloadable files, by country
xmlfiles = {} # structure: downloads[date][country]=filename
dir_list = os.listdir(ECOSIO_PATH)
for f in dir_list:
if f.endswith('.xml'): # PL_A282_T000_260110_1.xml
country = f[0:2] # first two chars
date = f"20{f[13:19]}"
if date not in xmlfiles:
xmlfiles[date] = 0
xmlfiles[date] += 1
states = {}
# r = StateOfDate.query.filter(system_issues=0, data_issues>0).all()
date_states = StateOfDate.query.all()
for d in date_states:
if d.system_issues > 0 or d.data_issues > 0:
files = 0
if d.date in xmlfiles:
files = xmlfiles[d.date]
states[d.date] = [files, d.system_issues, d.data_issues]
for yyyymmdd in xmlfiles.keys():
if yyyymmdd not in states:
states[yyyymmdd] = [xmlfiles[yyyymmdd],0,0]
return render_template('ecosio_service.html', downloads=states)
@app.route('/invoice/downloadaszip', methods=['GET'])
def downloadaszip():
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
try:
filenames = [
f for f in os.listdir(ECOSIO_PATH)
if f.endswith(".xml")
]
except FileNotFoundError:
abort(404, description="Files directory not found")
if not filenames:
abort(404, description="No xml files found")
# Create in-memory ZIP file
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
for filename in filenames: # PL_A282_T000_260110_1.xml
file_path = os.path.join(ECOSIO_PATH, filename)
zf.write(file_path, arcname=filename)
# move the file to the archive
os.replace(file_path, os.path.join(DONE_PATH, filename))
# update db with file status TODO
invoiceIsEcosiod(filename[3:].replace('.xml',''),0) # parameter like A282_T000_260110_1
memory_file.seek(0)
return send_file(
memory_file,
mimetype="application/zip",
as_attachment=True,
download_name="bundle_for_ecosio.zip"
)
def file_has_invoice(filename):
is_invoice = False
sto = dat = til = seq = ''
transactiontytpe = '<RetailTransaction TransactionStatus="SES:Invoice">'
with open(filename, 'r', encoding='utf-8') as f:
while line := f.readline():
if transactiontytpe in line:
is_invoice = True
if is_invoice:
e = ET.parse(filename)
root = e.getroot()
tree = etree_to_dict(root)
trx = tree['POSLog']['Transaction']['RetailTransaction']
if 'InvoiceNumber' in trx:
trx_link = get_trxlink_from_dict(trx)
if len(trx_link) > 1:
return f"{trx_link['store']}_T{trx_link['till']}_{trx_link['year']}{trx_link['month']}{trx_link['day']}_{trx_link['seq']}"
return 0
@app.route('/invoice/requests', methods=['GET','POST'])
def get_invoices():
'''
this url takes a store and a date
then shows all found invoice-requests for that date
clicking on one of these will show the invoice in message format
'''
xml_files = []
store = ""
date = ""
if request.method == "POST":
store = request.form.get("store")
date = request.form.get("date")
folder = os.path.join(SORTED_PATH, date, store)
if folder and os.path.isdir(folder):
for f in os.listdir(folder):
if '_T000_' in f and f.endswith(".xml"):
filepath = os.path.join(folder, f)
i = file_has_invoice(filepath)
if i:
xml_files.append([f, i])
else:
xml_files = []
return render_template(
"requests.html",
xml_files=xml_files,
store=store,
date=date
)
@app.route("/invoice/invoice", methods=['GET'])
def view_invoice():
'''
this endpoint will show one invoice in message format
'''
date = request.args.get("date")
store = request.args.get("store")
filename = request.args.get("filename")
if not date or not store or not filename:
print("wrong url")
abort(400)
y,m,d = date.split('/')
# req, sales = filename.split(':')
filepath = os.path.join(SORTED_PATH, y, m, d, store, filename)
# Basic safety check
if not os.path.isfile(filepath) or not filepath.lower().endswith(".xml"):
abort(404)
invoice = process_invoice_request(filepath)
return str(invoice)
def read_cust_csv():
with open(BUYERS_BROKEN, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
rows = list(reader)
return rows[0], rows[1:] # header, data
def write_repaired_csv(rows):
with open(BUYERS_REPAIRED, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# writer.writerow(header)
writer.writerows(rows)
def write_broken_csv(header, rows):
2026-03-31 22:07:11 +02:00
print("write broken")
2026-02-25 10:24:12 +01:00
with open(BUYERS_BROKEN, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(header)
writer.writerows(rows)
EDITABLECOLS = 7
@app.route("/invoice/buyers", methods=["GET", "POST"])
def broken_buyers():
'''
this endpoint will show all data from the broken-buyers csv file
after upodating any line, if its approved as good data, the buyer will move from broken to repaired csv
'''
header, rows = read_cust_csv()
if request.method == "POST":
updated_rows = []
not_updated_rows = []
for i, row in enumerate(rows):
updated = row[:-EDITABLECOLS] # untouched columns
dataOk = True
for j in range(EDITABLECOLS):
field_name = f"row_{i}_col_{j}"
value = request.form[field_name]
if len(value) < 2:
dataOk = False
updated.append(value)
if dataOk:
updated_rows.append(updated)
else:
not_updated_rows.append(updated)
write_broken_csv(header, not_updated_rows)
write_repaired_csv(updated_rows)
return redirect(url_for("table"))
return render_template(
"buyers.html",
header=header,
rows=rows,
editable_cols=EDITABLECOLS,
)
@app.route('/invoice/api/getStatusOfDate/<YYYY>/<MM>/<DD>', methods=['GET'])
def getStatusOfDate(YYYY, MM, DD):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
result = {}
result['status'] = 'pending'
if YYYY and MM and DD:
d=f"{YYYY}{MM}{DD}"
r = StateOfDate.query.filter_by(date=d).first()
if r:
result['data'] = str(r)
if r.isDone():
result['status'] = 'done'
return json.dumps(result), 200
@app.route('/invoice/api/setStatusOfDate/<YYYY>/<MM>/<DD>/<systemerrors>/<dataerrors>', methods=['GET'])
def setStatusOfDate(YYYY, MM, DD, systemerrors, dataerrors):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
result = {}
if YYYY and MM and DD:
d=f"{YYYY}{MM}{DD}"
r = StateOfDate.query.filter_by(date=d).first()
if r:
r.system_issues = systemerrors
r.data_issues = dataerrors
db.session.commit()
result['status'] = 'Ok'
result['data'] = str(r)
else:
r = StateOfDate(date=f"{YYYY}{MM}{DD}", system_issues=systemerrors, data_issues=dataerrors)
db.session.add(r)
db.session.commit()
result['status'] = 'Ok'
result['data'] = str(r)
return json.dumps(result), 200
@app.route('/invoice/api/getNextProcessDate/<YYYYMMDD>', methods=['GET'])
def getNextProcessDate(YYYYMMDD):
result = {}
latest_date = datetime.datetime.today().date()
date = latest_date
earliest_date = datetime.datetime.strptime(YYYYMMDD, "%Y%m%d").date()
for sod in StateOfDate.query.all():
if sod.system_issues > 0 or sod.data_issues > 0:
date = datetime.datetime.strptime(sod.date, "%Y%m%d").date()
if date >= earliest_date and date < latest_date:
latest_date = date
result['status'] = 'Ok'
result['data'] = str(date.strftime('%Y%m%d'))
return json.dumps(result), 200
def setInvoiceState(poslogid=None, receiptid=None, country='XX', state=-1):
result = {}
if poslogid:
r = StateOfInvoice.query.filter_by(poslogid=poslogid).first()
if r:
r.state = state
db.session.commit()
result['status'] = 'Ok'
result['data'] = str(r)
else:
YYYYMM=receiptid[4:10]
print(f">->->{YYYYMM}")
r = StateOfInvoice(poslogid=poslogid, receiptid=receiptid, receiptmonth=YYYYMM, country=country, state=state)
db.session.add(r)
db.session.commit()
result['status'] = 'Ok'
result['data'] = str(r)
return json.dumps(result), 200
def getInvoiceState(poslogid=None):
if poslogid:
r = StateOfInvoice.query.filter_by(poslogid=poslogid).first()
if r:
return r.state
return -1
@app.route('/invoice/api/poslog/isConverted/<poslogid>/<receiptid>/<country>', methods=['GET'])
def poslogIsConverted(poslogid, receiptid, country):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
return setInvoiceState(poslogid=poslogid, receiptid=receiptid, country=country, state=1)
@app.route('/invoice/api/poslog/hasIssues/<poslogid>/<receiptid>/<country>', methods=['GET'])
def poslogHasIssues(poslogid, receiptid, country):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
return setInvoiceState(poslogid=poslogid, receiptid=receiptid, country=country, state=0)
@app.route('/invoice/api/invoice/isEcosiod/<poslogid>', methods=['GET'])
def invoiceIsEcosiod(poslogid, receiptid):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
return setInvoiceState(poslogid=poslogid, state=2)
@app.route('/invoice/api/invoice/isMonthlied/<poslogid>', methods=['GET'])
def isMonthlied(poslogid, receiptid):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
return setInvoiceState(poslogid=poslogid, state=3)
@app.route('/invoice/api/poslog/canBeConverted/<poslogid>', methods=['GET'])
def isOkToCreateXML(poslogid):
result = {}
result['status'] = 'NotOk'
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
r = getInvoiceState(poslogid=poslogid)
if r<2:
result['status'] = 'Ok'
return result
@app.route('/invoice/showtables/<status>', methods=['GET'])
def showPoslogs(status):
# if there is a filename someone wants a download
# if the filename is "all", then zip up all the avalable files and send the zip
result = 'poslogs:<br/>'
if status:
poslogs = StateOfInvoice.query.all()
if poslogs:
for p in poslogs:
result += f"{p}<br/>"
result += '<br/>date-states:<br/>'
states = StateOfDate.query.all()
if states:
for p in states:
result += f"{p}<br/>"
return result
def file_has_invoicenr(filename, nrs):
with open(filename, 'r', encoding='utf-8') as f:
while line := f.readline():
for nr in nrs:
if nr in line:
return True
return False
@app.route('/invoice/find_rejectedids', methods=['GET','POST'])
def find_rejectedids():
rows=[]
if request.method == "POST":
if len(request.form['date']) == 6 and len(request.form['countrycode']) == 2 and len(request.form['invoices']) > 10:
# scroll though the ecosio/downloaded folder in search for <country>, later than <date>, then open them to see if they match an invoicenr
invoices = []
files = []
for i in request.form['invoices'].split("\n"):
if len(i) > 10:
invoices.append(i.strip())
YY = request.form['date'][0:2]
MM = request.form['date'][2:4]
DD = request.form['date'][4:]
for f in os.listdir(DONE_PATH):
if f.startswith(f"{request.form['countrycode']}_") and f.endswith(".xml"):
country,store,till,filename_datestr,dummy = f.split('_')
file_date = datetime.datetime.strptime(filename_datestr, "%y%m%d").date()
ref_date = datetime.datetime.strptime(request.form['date'], "%y%m%d").date()
if file_date >= ref_date:
filepath = os.path.join(DONE_PATH, f)
if file_has_invoicenr(filepath, invoices):
# get the original sorted poslog file
filepath = os.path.join(SORTED_PATH, file_date.strftime("%Y"),file_date.strftime("%m"),file_date.strftime("%d"),store,f"{f[3:]}" )
inv = process_invoice_request(filepath)
row = [inv.country(), inv.nr, inv.receipt_id(), inv.poslogid, inv.buyer.name, inv.buyer.orgid, inv.buyer.taxID]
rows.append(row)
if len(rows) > 0:
return render_template("repair_rejectedids.html", rows=rows)
return render_template("find_rejectedids.html")
@app.route('/invoice/repair_rejectedids', methods=['POST'])
def repair_rejectedids():
files = {}
download = []
for f in request.form.keys():
store, till, date, seq, field = f.split("_")
filename = f"{store}_{till}_{date}_{seq}.xml"
if not filename in files:
files[filename] = {}
files[filename][field] = request.form[f]
# we now have a nice dict with filenames and their new orgid and taxid
for f in files.keys():
# make the sorted filename of the invoice:
store, till, date, dummy = f.split("_")
inv_filepath = os.path.join(SORTED_PATH, f"20{date[0:2]}", date[2:4], date[4:], store, f)
inv = process_invoice_request(inv_filepath)
# update the orgid and taxid
inv.buyer.orgid = files[f]['orgid']
inv.buyer.taxID = files[f]['taxid']
# overwrite the file in the downloaded folder
fname = f"{inv.country()}_{inv.poslogid}.xml"
with open(os.path.join(DONE_PATH, fname), "w", encoding="utf-8") as lfile:
lfile.write(str(inv))
download.append(fname)
# now zipo up and download the new files
# Create in-memory ZIP file
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
for filename in download: # PL_A282_T000_260110_1.xml
file_path = os.path.join(DONE_PATH, filename)
zf.write(file_path, arcname=filename)
memory_file.seek(0)
return send_file(
memory_file,
mimetype="application/zip",
as_attachment=True,
download_name="repairs_for_ecosio.zip"
)
@app.route('/invoice/monthlyreport', methods=['GET','POST'])
def monthlyreport():
csv = ''
month = '202601'
include = False
if request.method == "POST":
today = datetime.datetime.today().strftime('%Y-%m-%d')
if len(request.form['countrycode']) == 2 and len(request.form['month']) == 6:
country = request.form['countrycode']
month = request.form['month']
include = 'include_older' in request.form
csv = 'ReportDate,InvoiceNr,InvoiceSentdate,oibSeller,oibBuyer,PaymentDate,PaymentAmount,PaymentMethod\n'
if country == 'HR':
csv = 'datumVrijemeSlanja,brojDokumenta,datumIzdavanja,oibPorezniBrojIzdavatelja,oibPorezniBrojPrimatelja,datumNaplate,naplaceniIznos,nacinPlacanja\n'
# get the invoices from the database
unreported = StateOfInvoice.query.filter_by(country=country, state=2).all()
for inv in unreported:
# get info from all
if (include and inv.receiptmonth <= month) or (inv.receiptmonth==month):
# load the ecosio file
filepath = os.path.join(DONE_PATH, f"{inv.country}_{inv.poslogid}.xml")
downloaddate = inv.updated_at.strftime('%Y-%m-%d')
if os.path.exists(filepath):
e = ET.parse(filepath)
root = e.getroot()
tree = etree_to_dict(root)
line = f"{today};{tree['Invoice']['ID']};{downloaddate};{tree['Invoice']['AccountingSupplierParty']['Party']['PartyLegalEntity']['CompanyID']};{tree['Invoice']['AccountingCustomerParty']['Party']['PartyLegalEntity']['CompanyID']};{tree['Invoice']['DueDate']};{tree['Invoice']['LegalMonetaryTotal']['PayableAmount']['#text']};T\n"
csv += line
isMonthlied(inv.poslogid, 0)
# download to csv file
buffer = io.BytesIO()
buffer.write(csv.encode("utf-8"))
buffer.seek(0)
return send_file(
buffer,
mimetype="text/csv",
as_attachment=True,
download_name=f"monthly_report_{country}_{month}.csv"
)
return render_template("monthlyreport.html", )
return render_template("monthlyreport.html")
def read_csv(csv_file):
with open(csv_file, newline='', encoding="utf-8") as f:
reader = list(csv.reader(f))
header = reader[0]
rows = reader[1:]
return header, rows
def write_csv(csv_file, header, rows):
with open(csv_file, "w", newline='', encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(header)
writer.writerows(rows)
@app.route("/invoice/maintain_csv/<csv_file>", methods=["GET", "POST"])
def maintain_csv(csv_file):
csv_path = os.path.join(DATADIR, f"{csv_file}.csv")
header, rows = read_csv(csv_path)
if request.method == "POST":
new_rows = []
num_columns = len(header)
# Collect form data
row_index = 0
while True:
row = []
for col_index in range(num_columns):
field_name = f"row-{row_index}-col-{col_index}"
if field_name not in request.form:
break
row.append(request.form.get(field_name).strip())
if len(row) != num_columns:
break
# Skip completely empty rows
if any(cell != "" for cell in row):
new_rows.append(row)
row_index += 1
write_csv(csv_path, header, new_rows)
return redirect(url_for("home"))
return render_template("edit_csv.html", header=header, rows=rows, csv=csv_file)
def tail(filename, n=500, loglevel=1):
lines = []
with open(filename, 'r', encoding='utf-8') as f:
lines = list(deque(f, maxlen=n))
if loglevel==0:
return ''.join(lines)
if loglevel==1:
return ''.join( [ l for l in lines if LOGLEVELS[0] not in l ] )
if loglevel>1:
return ''.join( [ l for l in lines if LOGLEVELS[2] in l or LOGLEVELS[3] in l ] )
@app.route("/invoice/log/<loglevel>")
def show_log(loglevel):
return render_template("show_log.html", txt=tail('log.txt', loglevel=int(loglevel)))
@app.route("/invoice/earliestdate", methods=["GET", "POST"])
def earliestdate():
if request.method == "POST":
earliestdate = request.form['earliestdate']
with open(EARLIEST_DATE_FILE, 'w') as file:
file.write(earliestdate)
else:
earliestdate = '20260101'
if os.path.exists(EARLIEST_DATE_FILE):
with open(EARLIEST_DATE_FILE, 'r') as f:
earliestdate = f.readline()
return render_template("update_earliest_date.html", earliestdate=earliestdate)
if __name__ == '__main__':
# as native flask
app.run(debug=True, host='0.0.0.0', port=9666)