SekiPOS server sync

This commit is contained in:
2026-06-23 15:20:14 -04:00
parent 5704980dbd
commit ccd1836d38
15 changed files with 1063 additions and 149 deletions

View File

@@ -1,4 +1,6 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
import os
import json
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import check_password_hash, generate_password_hash
from core.db import get_db_connection
@@ -56,5 +58,67 @@ def update_settings():
flash('Configuración actualizada')
return redirect(request.referrer)
@auth_bp.route('/api/instance/config')
@login_required
def get_instance_config():
db_dir = os.path.dirname(current_app.config.get('DB_FILE', ''))
instance_file = os.path.join(db_dir, 'instance.json')
if os.path.exists(instance_file):
with open(instance_file) as f:
config = json.load(f)
else:
config = {}
return jsonify({
"instance_id": config.get('instance_id', ''),
"display_name": config.get('display_name', 'Caja Principal'),
"server_url": config.get('server_url', ''),
"sync_secret": config.get('sync_secret', ''),
"mode": config.get('mode', 'desktop')
})
@auth_bp.route('/api/instance/config', methods=['POST'])
@login_required
def save_instance_config():
data = request.get_json()
db_dir = os.path.dirname(current_app.config.get('DB_FILE', ''))
instance_file = os.path.join(db_dir, 'instance.json')
if os.path.exists(instance_file):
with open(instance_file) as f:
config = json.load(f)
else:
config = {}
if 'server_url' in data:
config['server_url'] = data['server_url']
if 'display_name' in data:
config['display_name'] = data['display_name']
if 'sync_secret' in data:
config['sync_secret'] = data['sync_secret']
with open(instance_file, 'w') as f:
json.dump(config, f, indent=2)
return jsonify({"status": "ok"})
@auth_bp.route('/api/sync/trigger', methods=['POST'])
@login_required
def trigger_sync():
from core.sync import SyncManager
try:
db_file = current_app.config.get('DB_FILE', '')
instance_id = current_app.config.get('INSTANCE_ID', '')
server_url = current_app.config.get('SERVER_URL', '')
sync_secret = ''
if db_file:
instance_file = os.path.join(os.path.dirname(db_file), 'instance.json')
if os.path.exists(instance_file):
with open(instance_file) as f:
sync_secret = json.load(f).get('sync_secret', '')
if server_url:
sm = SyncManager(db_file, instance_id, server_url, current_app.config.get('DISPLAY_NAME', ''), sync_secret)
sm._push()
sm._pull()
return jsonify({"status": "ok"})
return jsonify({"status": "error", "message": "No server URL configured"}), 400
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
def init_login_manager(app):
login_manager.init_app(app)

View File

@@ -1,9 +1,18 @@
from flask import Blueprint, render_template, request, jsonify
import uuid as _uuid
from datetime import datetime, timezone
from flask import Blueprint, render_template, request, jsonify, current_app
from flask_login import login_required, current_user
from core.db import get_db_connection
finance_bp = Blueprint('finance', __name__)
def _log_deletion(conn, entity_type, entity_uuid):
conn.execute("INSERT INTO sync_deletions (entity_type, entity_uuid) VALUES (?, ?)",
(entity_type, entity_uuid))
def _instance_id():
return current_app.config.get('INSTANCE_ID', '')
@finance_bp.route('/dicom')
@login_required
def dicom():
@@ -51,26 +60,31 @@ def pay_debtor_ticket(debtor_id):
data = request.get_json()
ticket_id = data.get('ticket_id')
amount = float(data.get('amount', 0))
inst_id = _instance_id()
now = datetime.now(timezone.utc).isoformat()
if not ticket_id or amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
with get_db_connection() as conn:
conn.execute('''UPDATE debtor_tickets
conn.execute('''UPDATE debtor_tickets
SET amount_paid = amount_paid + ?,
status = CASE WHEN (total - amount_paid - ?) <= 0 THEN 'paid' ELSE 'partial' END
WHERE id = ?''', (amount, amount, ticket_id))
# Update status based on final values
conn.execute('''UPDATE debtor_tickets
SET status = CASE
status = CASE WHEN (total - amount_paid - ?) <= 0 THEN 'paid' ELSE 'partial' END,
updated_at = ?,
updated_by = ?
WHERE id = ?''', (amount, amount, now, inst_id, ticket_id))
conn.execute('''UPDATE debtor_tickets
SET status = CASE
WHEN total - amount_paid <= 0 THEN 'paid'
WHEN amount_paid > 0 THEN 'partial'
ELSE 'unpaid'
END
WHERE id = ?''', (ticket_id,))
END,
updated_at = ?,
updated_by = ?
WHERE id = ?''', (now, inst_id, ticket_id))
conn.commit()
return jsonify({"status": "success"})
@finance_bp.route('/api/dicom/pay', methods=['POST'])
@@ -80,34 +94,34 @@ def dicom_pay():
ticket_id = data.get('ticket_id')
amount = float(data.get('amount', 0))
payment_method = data.get('payment_method', 'efectivo')
inst_id = _instance_id()
now = datetime.now(timezone.utc).isoformat()
if not ticket_id or amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Update the debtor ticket payment
cur.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?', (amount, ticket_id))
# Update status based on final values
cur.execute('''UPDATE debtor_tickets
SET status = CASE
conn.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?', (amount, ticket_id))
conn.execute('''UPDATE debtor_tickets
SET status = CASE
WHEN total - amount_paid <= 0 THEN 'paid'
WHEN amount_paid > 0 THEN 'partial'
ELSE 'unpaid'
END
WHERE id = ?''', (ticket_id,))
# Insert into sales table to track daily revenue
cur.execute('''INSERT INTO sales (date, total, payment_method)
VALUES (CURRENT_TIMESTAMP, ?, ?)''', (amount, payment_method))
END,
updated_at = ?,
updated_by = ?
WHERE id = ?''', (now, inst_id, ticket_id))
conn.execute('''INSERT INTO sales (date, total, payment_method, uuid)
VALUES (CURRENT_TIMESTAMP, ?, ?, ?)''',
(amount, payment_method, str(_uuid.uuid4())))
conn.commit()
return jsonify({"status": "success", "amount": amount}), 200
except Exception as e:
print(f"Dicom Pay Error: {e}")
return jsonify({"error": str(e)}), 500
@@ -121,22 +135,27 @@ def update_dicom():
notes = data.get('notes', '')
image_url = data.get('image_url', '')
action = data.get('action')
inst_id = _instance_id()
now = datetime.now(timezone.utc).isoformat()
if not name or amount <= 0:
return jsonify({"error": "Nombre y monto válidos son requeridos"}), 400
if action == 'add':
amount = -amount
with get_db_connection() 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,
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))
existing = conn.execute("SELECT uuid FROM dicom WHERE name = ?", (name,)).fetchone()
ent_uuid = existing[0] if existing else str(_uuid.uuid4())
conn.execute('''INSERT INTO dicom (uuid, name, amount, notes, image_url, updated_at, updated_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
amount = amount + excluded.amount,
notes = excluded.notes,
image_url = CASE WHEN excluded.image_url != "" THEN excluded.image_url ELSE dicom.image_url END,
updated_at = ?,
updated_by = ?''',
(ent_uuid, name, amount, notes, image_url, now, inst_id, now, inst_id))
conn.commit()
return jsonify({"status": "success"}), 200
@@ -145,6 +164,9 @@ def update_dicom():
def delete_dicom(debtor_id):
try:
with get_db_connection() as conn:
row = conn.execute("SELECT uuid FROM dicom WHERE id = ?", (debtor_id,)).fetchone()
if row:
_log_deletion(conn, 'dicom', row[0])
conn.execute('DELETE FROM dicom WHERE id = ?', (debtor_id,))
conn.commit()
return jsonify({"status": "success"}), 200
@@ -189,13 +211,13 @@ def add_gasto():
data = request.get_json()
desc = data.get('description')
amount = data.get('amount')
if not desc or not amount:
return jsonify({"error": "Faltan datos"}), 400
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("INSERT INTO expenses (description, amount) VALUES (?, ?)", (desc, int(amount)))
conn.execute("INSERT INTO expenses (uuid, description, amount) VALUES (?, ?, ?)",
(str(_uuid.uuid4()), desc, int(amount)))
conn.commit()
return jsonify({"success": True})
@@ -203,8 +225,10 @@ def add_gasto():
@login_required
def delete_gasto(gasto_id):
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("DELETE FROM expenses WHERE id = ?", (gasto_id,))
row = conn.execute("SELECT uuid FROM expenses WHERE id = ?", (gasto_id,)).fetchone()
if row:
_log_deletion(conn, 'expense', row[0])
conn.execute("DELETE FROM expenses WHERE id = ?", (gasto_id,))
conn.commit()
return jsonify({"success": True})
@@ -213,22 +237,17 @@ def delete_gasto(gasto_id):
def get_debtors():
try:
with get_db_connection() as conn:
# Check if table exists
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='debtors'")
if not cur.fetchone():
print("Debtors table does not exist!")
return jsonify([])
cur.execute('SELECT id, name, contact_info FROM debtors ORDER BY name')
cur.execute('SELECT id, name, contact_info, uuid FROM debtors ORDER BY name')
debtors = cur.fetchall()
print(f"Found {len(debtors)} debtors:", debtors)
return jsonify([{"id": d[0], "name": d[1], "contact_info": d[2]} for d in debtors])
return jsonify([{"id": d[0], "name": d[1], "contact_info": d[2], "uuid": d[3]} for d in debtors])
except Exception as e:
print(f"Error getting debtors: {e}")
import traceback
traceback.print_exc()
return jsonify([])
@finance_bp.route('/api/dicom/checkout', methods=['POST'])
@@ -240,51 +259,50 @@ def dicom_checkout():
debtor_name = data.get('debtor_name', '').strip()
contact_info = data.get('contact_info', '').strip()
initial_payment = data.get('initial_payment', 0) or 0
inst_id = _instance_id()
now = datetime.now(timezone.utc).isoformat()
if not cart:
return jsonify({"error": "Carrito vacío"}), 400
if not debtor_name:
return jsonify({"error": "Nombre del deudor requerido"}), 400
total = sum(item.get('subtotal', 0) for item in cart)
with get_db_connection() as conn:
cur = conn.cursor()
# Upsert debtor
cur.execute('''INSERT INTO debtors (name, contact_info)
VALUES (?, ?)
ON CONFLICT(name) DO UPDATE SET
contact_info = excluded.contact_info''',
(debtor_name, contact_info))
# Get debtor ID
debtor_id = cur.execute('SELECT id FROM debtors WHERE name = ?', (debtor_name,)).fetchone()[0]
# Insert debtor ticket
debtor_uuid = str(_uuid.uuid4())
conn.execute('''INSERT INTO debtors (uuid, name, contact_info, updated_at, updated_by)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
contact_info = excluded.contact_info,
updated_at = excluded.updated_at,
updated_by = excluded.updated_by''',
(debtor_uuid, debtor_name, contact_info, now, inst_id))
debtor_id = conn.execute('SELECT id FROM debtors WHERE name = ?', (debtor_name,)).fetchone()[0]
status = 'partial' if initial_payment > 0 else 'unpaid'
cur.execute('''INSERT INTO debtor_tickets (debtor_id, total, amount_paid, status)
VALUES (?, ?, ?, ?)''',
(debtor_id, total, initial_payment, status))
ticket_id = cur.lastrowid
# Insert ticket items and deduct stock
ticket_uuid = str(_uuid.uuid4())
c = conn.execute('''INSERT INTO debtor_tickets (uuid, debtor_id, total, amount_paid, status, updated_at, updated_by)
VALUES (?, ?, ?, ?, ?, ?, ?)''',
(ticket_uuid, debtor_id, total, initial_payment, status, now, inst_id))
ticket_id = c.lastrowid
for item in cart:
cur.execute('''INSERT INTO debtor_ticket_items
conn.execute('''INSERT INTO debtor_ticket_items
(ticket_id, barcode, name, price, quantity, subtotal)
VALUES (?, ?, ?, ?, ?, ?)''',
(ticket_id, item.get('barcode', ''), item.get('name'),
(ticket_id, item.get('barcode', ''), item.get('name'),
item.get('price'), item.get('qty'), item.get('subtotal')))
# Deduct stock (skip for manual products)
if item.get('barcode') and not item.get('barcode', '').startswith(('MANUAL-', 'VARIOS-', 'RAPIDA-')):
cur.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?',
conn.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?',
(item.get('qty'), item.get('barcode')))
conn.commit()
return jsonify({"status": "success", "ticket_id": ticket_id, "debtor": debtor_name}), 200
except Exception as e:
print(f"Dicom Checkout Error: {e}")
return jsonify({"error": str(e)}), 500
@@ -294,13 +312,15 @@ def dicom_checkout():
def delete_debtor(debtor_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Delete items first
cur.execute('DELETE FROM debtor_ticket_items WHERE ticket_id IN (SELECT id FROM debtor_tickets WHERE debtor_id = ?)', (debtor_id,))
# Delete tickets
cur.execute('DELETE FROM debtor_tickets WHERE debtor_id = ?', (debtor_id,))
# Delete debtor
cur.execute('DELETE FROM debtors WHERE id = ?', (debtor_id,))
row = conn.execute("SELECT uuid FROM debtors WHERE id = ?", (debtor_id,)).fetchone()
if row:
_log_deletion(conn, 'debtor', row[0])
tickets = conn.execute("SELECT uuid FROM debtor_tickets WHERE debtor_id = ?", (debtor_id,)).fetchall()
for t in tickets:
_log_deletion(conn, 'ticket', t[0])
conn.execute('DELETE FROM debtor_ticket_items WHERE ticket_id IN (SELECT id FROM debtor_tickets WHERE debtor_id = ?)', (debtor_id,))
conn.execute('DELETE FROM debtor_tickets WHERE debtor_id = ?', (debtor_id,))
conn.execute('DELETE FROM debtors WHERE id = ?', (debtor_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
@@ -312,11 +332,11 @@ def delete_debtor(debtor_id):
def delete_ticket(ticket_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Delete items first
cur.execute('DELETE FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,))
# Delete ticket
cur.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
row = conn.execute("SELECT uuid FROM debtor_tickets WHERE id = ?", (ticket_id,)).fetchone()
if row:
_log_deletion(conn, 'ticket', row[0])
conn.execute('DELETE FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,))
conn.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
@@ -328,28 +348,23 @@ def delete_ticket(ticket_id):
def delete_item(item_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Get item info to update ticket total
item = cur.execute('SELECT ticket_id, subtotal FROM debtor_ticket_items WHERE id = ?', (item_id,)).fetchone()
item = conn.execute('SELECT ticket_id, subtotal FROM debtor_ticket_items WHERE id = ?', (item_id,)).fetchone()
if not item:
return jsonify({"error": "Item no encontrado"}), 404
ticket_id, item_subtotal = item
# Delete item
cur.execute('DELETE FROM debtor_ticket_items WHERE id = ?', (item_id,))
# Check if ticket has remaining items
remaining_items = cur.execute('SELECT COUNT(*) FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,)).fetchone()[0]
conn.execute('DELETE FROM debtor_ticket_items WHERE id = ?', (item_id,))
remaining_items = conn.execute('SELECT COUNT(*) FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,)).fetchone()[0]
if remaining_items == 0:
# Delete ticket if no items left
cur.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
row = conn.execute("SELECT uuid FROM debtor_tickets WHERE id = ?", (ticket_id,)).fetchone()
if row:
_log_deletion(conn, 'ticket', row[0])
conn.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
conn.commit()
return jsonify({"status": "success", "ticket_deleted": True}), 200
# Update ticket total
cur.execute('UPDATE debtor_tickets SET total = total - ? WHERE id = ?', (item_subtotal, ticket_id))
conn.execute('UPDATE debtor_tickets SET total = total - ? WHERE id = ?', (item_subtotal, ticket_id))
conn.commit()
return jsonify({"status": "success", "ticket_deleted": False}), 200
except Exception as e:
@@ -363,36 +378,34 @@ def pay_all_debtor(debtor_id):
data = request.get_json()
amount = float(data.get('amount', 0))
payment_method = data.get('payment_method', 'efectivo')
inst_id = _instance_id()
now = datetime.now(timezone.utc).isoformat()
if amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
with get_db_connection() as conn:
cur = conn.cursor()
# Get all unpaid/partial tickets for this debtor
tickets = cur.execute('''SELECT id, total, amount_paid, total - amount_paid as remaining
FROM debtor_tickets
tickets = conn.execute('''SELECT id, total, amount_paid, total - amount_paid as remaining
FROM debtor_tickets
WHERE debtor_id = ? AND status != 'paid' ''', (debtor_id,)).fetchall()
for ticket in tickets:
ticket_id = ticket[0]
remaining = ticket[3]
if remaining > 0:
# Pay remaining amount
cur.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?',
(remaining, ticket_id))
# Update status
cur.execute('''UPDATE debtor_tickets SET status = 'paid' WHERE id = ?''', (ticket_id,))
# Record sale
cur.execute('''INSERT INTO sales (date, total, payment_method)
VALUES (CURRENT_TIMESTAMP, ?, ?)''', (remaining, payment_method))
conn.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ?, updated_at = ?, updated_by = ? WHERE id = ?',
(remaining, now, inst_id, ticket_id))
conn.execute('UPDATE debtor_tickets SET status = \'paid\', updated_at = ?, updated_by = ? WHERE id = ?',
(now, inst_id, ticket_id))
conn.execute('''INSERT INTO sales (date, total, payment_method, uuid)
VALUES (CURRENT_TIMESTAMP, ?, ?, ?)''',
(remaining, payment_method, str(_uuid.uuid4())))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Pay All Debtor Error: {e}")
return jsonify({"error": str(e)}), 500

View File

@@ -38,16 +38,22 @@ def upsert():
cache_dir = current_app.config['CACHE_DIR']
final_image_path = download_image(image_url, barcode, cache_dir)
from datetime import datetime, timezone
now = datetime.now(timezone.utc).isoformat()
instance_id = current_app.config.get('INSTANCE_ID', '')
with get_db_connection() as conn:
conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type)
VALUES (?,?,?,?,?,?)
conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type, updated_at, updated_by)
VALUES (?,?,?,?,?,?,?,?)
ON CONFLICT(barcode) DO UPDATE SET
name=excluded.name,
price=excluded.price,
image_url=excluded.image_url,
stock=excluded.stock,
unit_type=excluded.unit_type''',
(barcode, name, price, final_image_path, stock, unit_type))
unit_type=excluded.unit_type,
updated_at=excluded.updated_at,
updated_by=excluded.updated_by''',
(barcode, name, price, final_image_path, stock, unit_type, now, instance_id))
conn.commit()
return redirect(url_for('inventory.inventory'))
@@ -102,9 +108,12 @@ def bulk_price_update():
return jsonify({"error": "Missing data"}), 400
try:
from datetime import datetime, timezone
now = datetime.now(timezone.utc).isoformat()
instance_id = current_app.config.get('INSTANCE_ID', '')
with get_db_connection() as conn:
params = [(float(new_price), b) for b in barcodes]
conn.executemany('UPDATE products SET price = ? WHERE barcode = ?', params)
params = [(float(new_price), now, instance_id, b) for b in barcodes]
conn.executemany('UPDATE products SET price = ?, updated_at = ?, updated_by = ? WHERE barcode = ?', params)
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:

View File

@@ -1,5 +1,6 @@
import os
import time
import uuid
from flask import Blueprint, render_template, request, jsonify, current_app
from flask_login import login_required, current_user
from core.db import get_db_connection
@@ -81,7 +82,8 @@ def process_checkout():
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute('INSERT INTO sales (date, total, payment_method) VALUES (CURRENT_TIMESTAMP, ?, ?)', (total, payment_method))
sale_uuid = str(uuid.uuid4())
cur.execute('INSERT INTO sales (date, total, payment_method, uuid) VALUES (CURRENT_TIMESTAMP, ?, ?, ?)', (total, payment_method, sale_uuid))
sale_id = cur.lastrowid
for item in cart:

View File

@@ -130,6 +130,11 @@ def reverse_sale(sale_id):
with get_db_connection() as conn:
cur = conn.cursor()
sale_uuid = cur.execute('SELECT uuid FROM sales WHERE id = ?', (sale_id,)).fetchone()
if not sale_uuid:
return jsonify({"error": "Sale not found"}), 404
sale_uuid = sale_uuid[0]
items = cur.execute('SELECT barcode, quantity FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
for barcode, qty in items:
@@ -137,6 +142,8 @@ def reverse_sale(sale_id):
cur.execute('DELETE FROM sale_items WHERE sale_id = ?', (sale_id,))
cur.execute('DELETE FROM sales WHERE id = ?', (sale_id,))
cur.execute('INSERT INTO sync_deletions (entity_type, entity_uuid) VALUES (?, ?)',
('sale', sale_uuid))
conn.commit()

254
blueprints/sync_server.py Normal file
View File

@@ -0,0 +1,254 @@
from datetime import datetime, timezone
from flask import Blueprint, request, jsonify, current_app
from functools import wraps
sync_bp = Blueprint('sync', __name__)
def require_sync_secret(f):
@wraps(f)
def decorated(*args, **kwargs):
secret = current_app.config.get('SYNC_SECRET', '')
if secret and request.headers.get('X-Sync-Secret') != secret:
return jsonify({"error": "unauthorized"}), 401
return f(*args, **kwargs)
return decorated
@sync_bp.route('/api/ping')
def ping():
return jsonify({"status": "ok"}), 200
@sync_bp.route('/api/sync/push', methods=['POST'])
@require_sync_secret
def sync_push():
data = request.get_json()
if not data:
return jsonify({"error": "empty payload"}), 400
instance_id = data.get('instance_id', 'unknown')
synced_at = data.get('synced_at') or datetime.now(timezone.utc).isoformat()
from core.db import get_db_connection
with get_db_connection() as conn:
# 1. Deletions
for d in data.get('deletions', []):
et = d['entity_type']
eu = d['entity_uuid']
if et == 'debtor':
conn.execute("DELETE FROM debtors WHERE uuid = ?", (eu,))
elif et == 'ticket':
conn.execute("DELETE FROM debtor_tickets WHERE uuid = ?", (eu,))
elif et == 'dicom':
conn.execute("DELETE FROM dicom WHERE uuid = ?", (eu,))
elif et == 'expense':
conn.execute("DELETE FROM expenses WHERE uuid = ?", (eu,))
elif et == 'sale':
conn.execute("DELETE FROM sales WHERE uuid = ?", (eu,))
# 2. Sales
for sale in data.get('sales', []):
existing = conn.execute(
"SELECT id FROM sales WHERE uuid = ?", (sale['uuid'],)
).fetchone()
if existing:
continue
cur = conn.execute(
"INSERT INTO sales (date, total, payment_method, uuid, synced_at) VALUES (?, ?, ?, ?, ?)",
(sale.get('date', synced_at), sale['total'], sale.get('payment_method', 'efectivo'),
sale['uuid'], synced_at)
)
sale_id = cur.lastrowid
for item in sale.get('items', []):
conn.execute(
"INSERT INTO sale_items (sale_id, barcode, name, price, quantity, subtotal) VALUES (?, ?, ?, ?, ?, ?)",
(sale_id, item['barcode'], item['name'], item.get('price', 0),
item.get('qty', 1), item.get('subtotal', 0))
)
# 3. Products
now = datetime.now(timezone.utc).isoformat()
for p in data.get('products', []):
existing = conn.execute(
"SELECT updated_at FROM products WHERE barcode = ?", (p['barcode'],)
).fetchone()
if existing and existing[0] >= p.get('updated_at', now):
continue
conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type, updated_at, updated_by)
VALUES (?,?,?,?,?,?,?,?)
ON CONFLICT(barcode) DO UPDATE SET
name=excluded.name, price=excluded.price,
image_url=excluded.image_url, stock=excluded.stock,
unit_type=excluded.unit_type, updated_at=excluded.updated_at,
updated_by=excluded.updated_by''',
(p['barcode'], p.get('name', ''), p.get('price', 0),
p.get('image_url'), p.get('stock', 0), p.get('unit_type', 'unit'),
p.get('updated_at', now), instance_id))
# 4. Debtors
for d in data.get('debtors', []):
conn.execute('''INSERT INTO debtors (uuid, name, contact_info, updated_at, updated_by)
VALUES (?,?,?,?,?)
ON CONFLICT(uuid) DO UPDATE SET
name=excluded.name, contact_info=excluded.contact_info,
updated_at=excluded.updated_at, updated_by=excluded.updated_by''',
(d['uuid'], d['name'], d.get('contact_info', ''),
d.get('updated_at', now), d.get('updated_by', instance_id)))
# 5. Dicom (legacy)
for d in data.get('dicom', []):
conn.execute('''INSERT INTO dicom (uuid, name, amount, notes, image_url, updated_at, updated_by)
VALUES (?,?,?,?,?,?,?)
ON CONFLICT(uuid) DO UPDATE SET
name=excluded.name, amount=excluded.amount,
notes=excluded.notes, image_url=excluded.image_url,
updated_at=excluded.updated_at, updated_by=excluded.updated_by''',
(d['uuid'], d['name'], d.get('amount', 0), d.get('notes', ''),
d.get('image_url', ''), d.get('updated_at', now), d.get('updated_by', instance_id)))
# 6. Debtor tickets with items
for t in data.get('tickets', []):
existing = conn.execute(
"SELECT id FROM debtor_tickets WHERE uuid = ?", (t['uuid'],)
).fetchone()
if existing:
continue
debtor_row = conn.execute(
"SELECT id FROM debtors WHERE uuid = ?", (t['debtor_uuid'],)
).fetchone()
if not debtor_row:
continue
cur = conn.execute(
"INSERT INTO debtor_tickets (uuid, debtor_id, date, total, amount_paid, status, updated_at, updated_by) VALUES (?,?,?,?,?,?,?,?)",
(t['uuid'], debtor_row[0], t.get('date', synced_at), t['total'],
t.get('amount_paid', 0), t.get('status', 'unpaid'),
t.get('updated_at', now), t.get('updated_by', instance_id))
)
ticket_id = cur.lastrowid
for item in t.get('items', []):
conn.execute(
"INSERT INTO debtor_ticket_items (ticket_id, barcode, name, price, quantity, subtotal) VALUES (?,?,?,?,?,?)",
(ticket_id, item['barcode'], item['name'], item.get('price', 0),
item.get('qty', 1), item.get('subtotal', 0))
)
# 7. Expenses
for e in data.get('expenses', []):
existing = conn.execute(
"SELECT id FROM expenses WHERE uuid = ?", (e['uuid'],)
).fetchone()
if existing:
continue
conn.execute(
"INSERT INTO expenses (uuid, date, description, amount) VALUES (?,?,?,?)",
(e['uuid'], e.get('date', synced_at), e['description'], e['amount'])
)
conn.commit()
return jsonify({"status": "ok", "instance_id": instance_id}), 200
@sync_bp.route('/api/sync/pull')
@require_sync_secret
def sync_pull():
since = request.args.get('since', '1970-01-01')
since_sales = request.args.get('since_sales', '1970-01-01')
since_debtors = request.args.get('since_debtors', '1970-01-01')
since_tickets = request.args.get('since_tickets', '1970-01-01')
since_dicom = request.args.get('since_dicom', '1970-01-01')
since_expenses = request.args.get('since_expenses', '1970-01-01')
since_deletions = request.args.get('since_deletions', '1970-01-01')
from core.db import get_db_connection
with get_db_connection() as conn:
# Products
products = conn.execute(
"SELECT barcode, name, price, image_url, stock, unit_type, updated_at, updated_by FROM products WHERE updated_at >= ?",
(since,)
).fetchall()
# Sales with items
sales_rows = conn.execute(
"SELECT id, uuid, date, total, payment_method FROM sales WHERE date >= ? ORDER BY date",
(since_sales,)
).fetchall()
sales_payload = []
for s in sales_rows:
items = conn.execute(
"SELECT barcode, name, price, quantity, subtotal FROM sale_items WHERE sale_id = ?",
(s[0],)
).fetchall()
sales_payload.append({
"uuid": s[1], "date": s[2], "total": s[3], "payment_method": s[4],
"items": [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items]
})
# Debtors
debtors = conn.execute(
"SELECT uuid, name, contact_info, updated_at, updated_by FROM debtors WHERE updated_at >= ?",
(since_debtors,)
).fetchall()
# Dicom (legacy)
dicom_rows = conn.execute(
"SELECT uuid, name, amount, notes, image_url, updated_at, updated_by FROM dicom WHERE updated_at >= ?",
(since_dicom,)
).fetchall()
# Debtor tickets with items
tickets = conn.execute(
"SELECT id, uuid, debtor_id, date, total, amount_paid, status, updated_at, updated_by FROM debtor_tickets WHERE updated_at >= ?",
(since_tickets,)
).fetchall()
ticket_payload = []
for t in tickets:
ticket_id, ticket_uuid, debtor_id = t[0], t[1], t[2]
debtor_uuid = conn.execute("SELECT uuid FROM debtors WHERE id = ?", (debtor_id,)).fetchone()
items = conn.execute(
"SELECT barcode, name, price, quantity, subtotal FROM debtor_ticket_items WHERE ticket_id = ?",
(ticket_id,)
).fetchall()
ticket_payload.append({
"uuid": ticket_uuid, "debtor_uuid": debtor_uuid[0] if debtor_uuid else '',
"date": t[3], "total": t[4], "amount_paid": t[5], "status": t[6],
"updated_at": t[7], "updated_by": t[8],
"items": [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items]
})
# Expenses
expenses = conn.execute(
"SELECT uuid, date, description, amount FROM expenses WHERE date >= ?",
(since_expenses,)
).fetchall()
# Deletions
deletions = conn.execute(
"SELECT entity_type, entity_uuid, deleted_at FROM sync_deletions WHERE deleted_at >= ?",
(since_deletions,)
).fetchall()
return jsonify({
"products": [
{"barcode": p[0], "name": p[1], "price": p[2], "image_url": p[3],
"stock": p[4], "unit_type": p[5], "updated_at": p[6], "updated_by": p[7]}
for p in products
],
"sales": sales_payload,
"debtors": [
{"uuid": d[0], "name": d[1], "contact_info": d[2], "updated_at": d[3], "updated_by": d[4]}
for d in debtors
],
"dicom": [
{"uuid": d[0], "name": d[1], "amount": d[2], "notes": d[3], "image_url": d[4],
"updated_at": d[5], "updated_by": d[6]}
for d in dicom_rows
],
"tickets": ticket_payload,
"expenses": [
{"uuid": e[0], "date": e[1], "description": e[2], "amount": e[3]}
for e in expenses
],
"deletions": [
{"entity_type": d[0], "entity_uuid": d[1], "deleted_at": d[2]}
for d in deletions
]
}), 200