dicom evidence update
This commit is contained in:
25
app.py
25
app.py
@@ -178,7 +178,7 @@ def checkout():
|
||||
@login_required
|
||||
def dicom():
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
debtors = conn.execute('SELECT id, name, amount, notes, datetime(last_updated, "localtime") FROM dicom ORDER BY amount DESC').fetchall()
|
||||
debtors = conn.execute('SELECT id, name, amount, notes, datetime(last_updated, "localtime"), image_url FROM dicom ORDER BY amount DESC').fetchall()
|
||||
return render_template('dicom.html', active_page='dicom', user=current_user, debtors=debtors)
|
||||
|
||||
@app.route('/sales')
|
||||
@@ -474,29 +474,26 @@ def update_dicom():
|
||||
name = data.get('name', '').strip()
|
||||
amount = float(data.get('amount', 0))
|
||||
notes = data.get('notes', '')
|
||||
action = data.get('action') # 'add' or 'pay'
|
||||
image_url = data.get('image_url', '')
|
||||
action = data.get('action')
|
||||
|
||||
if not name or amount <= 0:
|
||||
return jsonify({"error": "Nombre y monto válidos son requeridos"}), 400
|
||||
|
||||
# If we are giving them credit (Fiar), their balance drops into the negative
|
||||
if action == 'add':
|
||||
amount = -amount
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cur = conn.cursor()
|
||||
# Upsert logic: if they exist, modify debt. If they don't, create them.
|
||||
cur.execute('''INSERT INTO dicom (name, amount, notes, last_updated)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute('''INSERT INTO dicom (name, amount, notes, image_url, last_updated)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
amount = amount + excluded.amount,
|
||||
notes = excluded.notes,
|
||||
last_updated = CURRENT_TIMESTAMP''', (name, amount, notes))
|
||||
conn.commit()
|
||||
return jsonify({"status": "success"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
image_url = CASE WHEN excluded.image_url != "" THEN excluded.image_url ELSE dicom.image_url END,
|
||||
last_updated = CURRENT_TIMESTAMP''', (name, amount, notes, image_url))
|
||||
conn.commit()
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
@app.route('/api/dicom/<int:debtor_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "macros/base.html" %}
|
||||
{% from 'macros/modals.html' import confirm_modal, scanner_modal %}
|
||||
{% from 'macros/modals.html' import confirm_modal, scanner_modal, render_receipt %}
|
||||
|
||||
{% block title %}Caja{% endblock %}
|
||||
|
||||
@@ -16,54 +16,11 @@
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#receipt-print-zone, #receipt-print-zone * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#receipt-print-zone {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 58mm;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block !important;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.container-fluid, .main, body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.receipt-header {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px dashed #000;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ render_receipt() }}
|
||||
{% call confirm_modal('removeConfirmModal', 'Quitar Producto', 'btn-danger-discord', 'Quitar', 'executeRemoveItem()') %}
|
||||
¿Estás seguro de que quieres quitar <strong id="removeItemName"></strong> del carrito?
|
||||
{% endcall %}
|
||||
@@ -117,41 +74,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="receipt-print-zone" class="d-none d-print-block">
|
||||
<div class="receipt-header">
|
||||
<h3 style="margin: 0; font-weight: 800;">SekiPOS</h3>
|
||||
<div style="font-size: 10px; margin-bottom: 5px;">Comprobante de Venta</div>
|
||||
<div style="font-size: 11px; font-weight: bold;">
|
||||
Ticket Nº <span id="receipt-ticket-id"></span>
|
||||
</div>
|
||||
<div id="receipt-date" style="font-size: 11px;"></div>
|
||||
</div>
|
||||
<table class="receipt-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%;">Cant</th>
|
||||
<th style="width: 60%; padding-left: 5px;">Desc</th>
|
||||
<th style="width: 25%; text-align: right;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="receipt-items-print">
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="receipt-total-row d-flex justify-content-between pt-2">
|
||||
<span>TOTAL:</span>
|
||||
<span id="receipt-total-print"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>RECIBIDO:</span>
|
||||
<span id="receipt-paid-print"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>VUELTO:</span>
|
||||
<span id="receipt-change-print"></span>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; font-size: 10px;">¡Gracias por su compra!</div>
|
||||
</div>
|
||||
<div class="modal fade" id="successModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-success">
|
||||
@@ -488,7 +411,6 @@
|
||||
|
||||
document.getElementById('grand-total').innerText = clp.format(total);
|
||||
|
||||
// This will actually run now
|
||||
saveCart();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,29 @@
|
||||
<label class="small text-muted mb-1">Monto (CLP)</label>
|
||||
<input type="number" id="dicom-amount" class="form-control" placeholder="Ej: 5000">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="small text-muted mb-1">Nota (Opcional)</label>
|
||||
<input type="text" id="dicom-notes" class="form-control" placeholder="Ej: Pan y bebida"
|
||||
onkeydown="if(event.key === 'Enter') submitDicom('add')">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="small text-muted mb-1">Foto / Comprobante</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="dicom-image-url" class="form-control" placeholder="URL de imagen" readonly>
|
||||
<input type="file" id="dicom-camera-input" accept="image/*" capture="environment" style="display: none;"
|
||||
onchange="handleDicomUpload(this)">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
onclick="document.getElementById('dicom-camera-input').click()">
|
||||
<i class="bi bi-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="dicom-img-preview-container" class="mt-2 d-none">
|
||||
<img id="dicom-img-preview" src="" class="img-thumbnail" style="max-height: 100px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<button class="btn btn-danger py-2 fw-bold" onclick="submitDicom('add')">
|
||||
<i class="bi bi-cart-plus me-1"></i> Fiar (Sumar Deuda)
|
||||
@@ -57,6 +74,7 @@
|
||||
<table class="table mb-0" id="dicom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Foto</th>
|
||||
<th>Nombre</th>
|
||||
<th>Deuda Total</th>
|
||||
<th>Última Nota</th>
|
||||
@@ -67,6 +85,16 @@
|
||||
<tbody>
|
||||
{% for d in debtors %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if d[5] %}
|
||||
<img src="{{ d[5] }}" class="rounded" style="width: 40px; height: 40px; object-fit: cover; cursor: pointer;"
|
||||
onclick="window.open(this.src, '_blank')">
|
||||
{% else %}
|
||||
<div class="bg-secondary rounded" style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; opacity: 0.3;">
|
||||
<i class="bi bi-image text-white"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="fw-bold">{{ d[1] }}</td>
|
||||
<td class="fw-bold price-cell" data-value="{{ d[2] }}"></td>
|
||||
<td class="text-muted small">{{ d[3] }}</td>
|
||||
@@ -112,14 +140,62 @@
|
||||
}
|
||||
});
|
||||
|
||||
function clearDicomForm() {
|
||||
document.getElementById('dicom-name').value = '';
|
||||
document.getElementById('dicom-amount').value = '';
|
||||
document.getElementById('dicom-notes').value = '';
|
||||
document.getElementById('dicom-name').focus();
|
||||
function compressImage(file, maxWidth, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = event => {
|
||||
const img = new Image();
|
||||
img.src = event.target.result;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
if (width > maxWidth) {
|
||||
height = Math.round((height * maxWidth) / width);
|
||||
width = maxWidth;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
canvas.toBlob(blob => resolve(blob), 'image/jpeg', quality);
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
async function handleDicomUpload(input) {
|
||||
const name = document.getElementById('dicom-name').value;
|
||||
if (!name) {
|
||||
alert("Primero ingresa un nombre para asociar la foto.");
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const compressedBlob = await compressImage(file, 800, 0.7);
|
||||
const formData = new FormData();
|
||||
// Use a unique name for the file based on the debtor
|
||||
formData.append('image', compressedBlob, `debt_${name}_${Date.now()}.jpg`);
|
||||
formData.append('barcode', `debt_${name}`); // Reusing the barcode field as a prefix
|
||||
|
||||
const res = await fetch('/upload_image', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
document.getElementById('dicom-image-url').value = data.image_url;
|
||||
document.getElementById('dicom-img-preview').src = data.image_url;
|
||||
document.getElementById('dicom-img-preview-container').classList.remove('d-none');
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Error procesando imagen.");
|
||||
}
|
||||
}
|
||||
|
||||
function filterDicom() {
|
||||
const q = document.getElementById('dicom-search').value.toLowerCase();
|
||||
document.querySelectorAll('#dicom-table tbody tr').forEach(row => {
|
||||
@@ -139,6 +215,7 @@
|
||||
const name = document.getElementById('dicom-name').value;
|
||||
const amount = document.getElementById('dicom-amount').value;
|
||||
const notes = document.getElementById('dicom-notes').value;
|
||||
const image_url = document.getElementById('dicom-image-url').value; // Added
|
||||
|
||||
if (!name || amount <= 0) {
|
||||
alert('Ingresa un nombre y monto válido.');
|
||||
@@ -149,17 +226,20 @@
|
||||
const res = await fetch('/api/dicom/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, amount, notes, action })
|
||||
body: JSON.stringify({ name, amount, notes, action, image_url }) // Added image_url
|
||||
});
|
||||
if (res.ok) window.location.reload();
|
||||
} catch (e) { alert("Error de conexión."); }
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error actualizando la base de datos.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Error de conexión.");
|
||||
}
|
||||
// UPDATE your existing clearDicomForm
|
||||
function clearDicomForm() {
|
||||
document.getElementById('dicom-name').value = '';
|
||||
document.getElementById('dicom-amount').value = '';
|
||||
document.getElementById('dicom-notes').value = '';
|
||||
document.getElementById('dicom-image-url').value = ''; // Added
|
||||
document.getElementById('dicom-img-preview-container').classList.add('d-none'); // Added
|
||||
document.getElementById('dicom-name').focus();
|
||||
}
|
||||
|
||||
async function forgiveDebt(id, name) {
|
||||
|
||||
@@ -43,4 +43,61 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_receipt(id_suffix="") %}
|
||||
<div id="receipt-print-zone{{ id_suffix }}" class="d-none d-print-block">
|
||||
<style>
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
#receipt-print-zone{{ id_suffix }}, #receipt-print-zone{{ id_suffix }} * { visibility: visible; }
|
||||
#receipt-print-zone{{ id_suffix }} {
|
||||
position: absolute; left: 0; top: 0; width: 80mm;
|
||||
padding: 5mm; margin: 0; display: block !important;
|
||||
font-family: 'Courier New', Courier, monospace; font-size: 10px; color: #000;
|
||||
}
|
||||
}
|
||||
.receipt-table { width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px; }
|
||||
.receipt-header { text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 5px; }
|
||||
.receipt-total-row { border-top: 1px dashed #000; margin-top: 5px; padding-top: 5px; font-weight: bold; }
|
||||
</style>
|
||||
|
||||
<div class="receipt-header">
|
||||
<h3 style="margin: 0; font-weight: 800;">SekiPOS</h3>
|
||||
<div style="font-size: 10px; margin-bottom: 5px;" id="receipt-type{{ id_suffix }}">Comprobante de Venta</div>
|
||||
<div style="font-size: 11px; font-weight: bold;">
|
||||
Ticket Nº <span id="receipt-ticket-id{{ id_suffix }}"></span>
|
||||
</div>
|
||||
<div id="receipt-date{{ id_suffix }}" style="font-size: 11px;"></div>
|
||||
</div>
|
||||
|
||||
<table class="receipt-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%; text-align: left;">Cant</th>
|
||||
<th style="width: 60%; padding-left: 5px; text-align: left;">Desc</th>
|
||||
<th style="width: 25%; text-align: right;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="receipt-items-print{{ id_suffix }}"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="receipt-total-row d-flex justify-content-between">
|
||||
<span>TOTAL:</span>
|
||||
<span id="receipt-total-print{{ id_suffix }}"></span>
|
||||
</div>
|
||||
|
||||
<div id="receipt-payment-info{{ id_suffix }}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>RECIBIDO:</span>
|
||||
<span id="receipt-paid-print{{ id_suffix }}"></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>VUELTO:</span>
|
||||
<span id="receipt-change-print{{ id_suffix }}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 20px; font-size: 10px;">¡Gracias por su compra!</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,71 +1,14 @@
|
||||
{% extends "macros/base.html" %}
|
||||
{% from 'macros/modals.html' import confirm_modal, scanner_modal %}
|
||||
{% from 'macros/modals.html' import confirm_modal, scanner_modal, render_receipt %}
|
||||
|
||||
{% block title %}Ventas{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@media print {
|
||||
body * { visibility: hidden; }
|
||||
#receipt-print-zone, #receipt-print-zone * { visibility: visible; }
|
||||
#receipt-print-zone {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 58mm;
|
||||
margin: 0;
|
||||
padding: 5mm;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table td, .table th {
|
||||
padding: 0.5rem 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-sm span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table th:last-child,
|
||||
.table td:last-child {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<!--HEAD-->
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="receipt-print-zone" class="d-none d-print-block">
|
||||
<div class="receipt-header" style="text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 5px;">
|
||||
<h3 style="margin: 0; font-weight: 800;">SekiPOS</h3>
|
||||
<div style="font-size: 10px; margin-bottom: 5px;">Re-impresión de Comprobante</div>
|
||||
<div style="font-size: 11px; font-weight: bold;">
|
||||
Ticket Nº <span id="receipt-ticket-id"></span>
|
||||
</div>
|
||||
<div id="receipt-date" style="font-size: 11px;"></div>
|
||||
</div>
|
||||
<table class="receipt-table" style="width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%; text-align: left;">Cant</th>
|
||||
<th style="width: 60%; padding-left: 5px; text-align: left;">Desc</th>
|
||||
<th style="width: 25%; text-align: right;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="receipt-items-print">
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="receipt-total-row d-flex justify-content-between pt-2" style="display: flex; justify-content: space-between; font-weight: bold; border-top: 1px dashed #000; margin-top: 5px;">
|
||||
<span>TOTAL:</span>
|
||||
<span id="receipt-total-print"></span>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 20px; font-size: 10px;">¡Gracias por su compra!</div>
|
||||
</div>
|
||||
{{ render_receipt() }}
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-4">
|
||||
@@ -286,6 +229,7 @@
|
||||
document.getElementById('receipt-ticket-id').innerText = id;
|
||||
document.getElementById('receipt-total-print').innerText = clp.format(total);
|
||||
document.getElementById('receipt-date').innerText = new Date(rawDate + " UTC").toLocaleString('es-CL');
|
||||
document.getElementById('receipt-payment-info').style.display = 'none';
|
||||
|
||||
window.print();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user