Compare commits
5 Commits
b9bcd49a0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| caf73ce156 | |||
| 83f9f606de | |||
| 4b3ef3eb8b | |||
| 656d1bb895 | |||
| c0a737915e |
42
README.md
42
README.md
@@ -1,4 +1,4 @@
|
|||||||
# SekiPOS v2.1 🍫🥤
|
# SekiPOS v2.2 🍫🥤
|
||||||
|
|
||||||
A reactive POS inventory system for software engineers with a snack addiction. Features real-time UI updates, automatic product discovery via Open Food Facts, and local image caching.
|
A reactive POS inventory system for software engineers with a snack addiction. Features real-time UI updates, automatic product discovery via Open Food Facts, and local image caching.
|
||||||
|
|
||||||
@@ -10,6 +10,44 @@ A reactive POS inventory system for software engineers with a snack addiction. F
|
|||||||
- **Secure:** Hashed password authentication via Flask-Login.
|
- **Secure:** Hashed password authentication via Flask-Login.
|
||||||
- **On device scanner:** Add and scan products from within your phone!
|
- **On device scanner:** Add and scan products from within your phone!
|
||||||
|
|
||||||
|
## 📦 Building the Desktop App (.exe)
|
||||||
|
|
||||||
|
If you want to run SekiPOS as a standalone Windows application with its own embedded browser window, you need to compile it using PyInstaller.
|
||||||
|
|
||||||
|
### 1. Prerequisites
|
||||||
|
You **must** use a stable Python release like **Python 3.11** or **3.12**. Pre-release versions (like 3.14) will fail to compile the PyWebView C# dependencies.
|
||||||
|
|
||||||
|
Install the required build tools globally for your stable Python version:
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m pip install -r requirements.txt pyinstaller
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Prepare `app.py`
|
||||||
|
Before compiling, scroll to the absolute bottom of `app.py` and ensure the standalone runner is active. It should look like this:
|
||||||
|
```python
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
# For standalone desktop app with embedded browser, use
|
||||||
|
run_standalone()
|
||||||
|
|
||||||
|
# For docker or traditional server deployment, comment out run_standalone() and uncomment the line below:
|
||||||
|
# socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Compile
|
||||||
|
Run this exact command in your terminal. It includes the hidden SocketIO threads and bundles your web templates:
|
||||||
|
```powershell
|
||||||
|
py -3.11 -m PyInstaller --noconfirm --onedir --windowed --add-data "templates;templates/" --add-data "static;static/" --hidden-import "engineio.async_drivers.threading" --icon "static/favicon.png" app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 4. Post-Build
|
||||||
|
Your portable app will be generated inside the `dist\app` folder.
|
||||||
|
* To keep your existing inventory, copy your `db/pos_database.db` and `static/cache/` folders from your source code into the new `dist\app` folder before running the `.exe`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🐳 Docker Deployment (Server)
|
## 🐳 Docker Deployment (Server)
|
||||||
|
|
||||||
Build and run the central inventory server:
|
Build and run the central inventory server:
|
||||||
@@ -35,6 +73,8 @@ services:
|
|||||||
sekipos:
|
sekipos:
|
||||||
ports:
|
ports:
|
||||||
- 5000:5000
|
- 5000:5000
|
||||||
|
environment:
|
||||||
|
- TZ=America/Santiago
|
||||||
volumes:
|
volumes:
|
||||||
- YOUR_PATH/sekipos/db:/app/db
|
- YOUR_PATH/sekipos/db:/app/db
|
||||||
- YOUR_PATH/sekipos/static/cache:/app/static/cache
|
- YOUR_PATH/sekipos/static/cache:/app/static/cache
|
||||||
|
|||||||
64
app.py
64
app.py
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import requests
|
import requests
|
||||||
from flask import send_file
|
from flask import send_file
|
||||||
@@ -12,6 +13,8 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import zipfile
|
import zipfile
|
||||||
import io
|
import io
|
||||||
|
import webview
|
||||||
|
import threading
|
||||||
|
|
||||||
# from dotenv import load_dotenv
|
# from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -20,16 +23,42 @@ import io
|
|||||||
# MP_ACCESS_TOKEN = os.getenv('MP_ACCESS_TOKEN')
|
# MP_ACCESS_TOKEN = os.getenv('MP_ACCESS_TOKEN')
|
||||||
# MP_TERMINAL_ID = os.getenv('MP_TERMINAL_ID')
|
# MP_TERMINAL_ID = os.getenv('MP_TERMINAL_ID')
|
||||||
|
|
||||||
app = Flask(__name__)
|
# --- PATH HELPERS FOR PYINSTALLER ---
|
||||||
app.config['SECRET_KEY'] = 'seki_super_secret_key_99' # Change this if you have actual friends
|
def get_bundled_path(relative_path):
|
||||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
"""Path for read-only files packed inside the .exe (templates, static)"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
base_path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
# Auth Setup
|
def get_persistent_path(relative_path):
|
||||||
|
"""Path for read/write files that must survive reboots (db, cache)"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base_path = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
base_path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
# --- FLASK INIT ---
|
||||||
|
app = Flask(
|
||||||
|
__name__,
|
||||||
|
template_folder=get_bundled_path('templates'),
|
||||||
|
static_folder=get_bundled_path('static')
|
||||||
|
)
|
||||||
|
app.config['SECRET_KEY'] = 'seki_super_secret_key_99' # Change this if you have actual friends
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
|
|
||||||
|
# --- AUTH SETUP (Do not delete this) ---
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = 'login'
|
||||||
|
|
||||||
DB_FILE = 'db/pos_database.db'
|
# --- DIRECTORY SETUP ---
|
||||||
CACHE_DIR = 'static/cache'
|
DB_DIR = get_persistent_path('db')
|
||||||
|
os.makedirs(DB_DIR, exist_ok=True)
|
||||||
|
DB_FILE = os.path.join(DB_DIR, "pos_database.db")
|
||||||
|
|
||||||
|
CACHE_DIR = get_persistent_path(os.path.join('static', 'cache'))
|
||||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
# --- MODELS ---
|
# --- MODELS ---
|
||||||
@@ -753,6 +782,29 @@ def delete_gasto(gasto_id):
|
|||||||
# })
|
# })
|
||||||
# return jsonify({"status": "ok"}), 200
|
# return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
def start_server():
|
||||||
|
# Use socketio.run instead of default app.run
|
||||||
|
socketio.run(app, host='127.0.0.1', port=5000)
|
||||||
|
|
||||||
|
def run_standalone():
|
||||||
|
t = threading.Thread(target=start_server)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# GIVE FLASK 2 SECONDS TO BOOT UP BEFORE OPENING THE BROWSER
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
webview.create_window('SekiPOS', 'http://127.0.0.1:5000', width=1366, height=768, resizable=True, fullscreen=False, min_size=(800, 600), maximized=True)
|
||||||
|
|
||||||
|
# private_mode=False is the magic flag that allows localStorage to survive.
|
||||||
|
# It saves data to %APPDATA%\pywebview on Windows.
|
||||||
|
webview.start(private_mode=False)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
|
# For standalone desktop app with embedded browser, use
|
||||||
|
#run_standalone()
|
||||||
|
|
||||||
|
# For docker or traditional server deployment, comment out run_standalone() and uncomment the line below:
|
||||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
|
||||||
|
|||||||
45
app.spec
Normal file
45
app.spec
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['app.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('templates', 'templates/'), ('static', 'static/')],
|
||||||
|
hiddenimports=['engineio.async_drivers.threading'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='app',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['static\\favicon.png'],
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='app',
|
||||||
|
)
|
||||||
2
build/.gitignore
vendored
Normal file
2
build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
2
dist/.gitignore
vendored
Normal file
2
dist/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1008,7 +1008,16 @@
|
|||||||
|
|
||||||
const finalPaid = paidAmount > 0 ? paidAmount : total;
|
const finalPaid = paidAmount > 0 ? paidAmount : total;
|
||||||
|
|
||||||
|
// Calculate Neto & IVA (Assuming 19% IVA)
|
||||||
|
const neto = Math.round(total / 1.19);
|
||||||
|
const iva = total - neto;
|
||||||
|
|
||||||
document.getElementById('receipt-ticket-id').innerText = saleId || "N/A";
|
document.getElementById('receipt-ticket-id').innerText = saleId || "N/A";
|
||||||
|
|
||||||
|
// Add the Neto and IVA fields
|
||||||
|
document.getElementById('receipt-neto-print').innerText = clp.format(neto);
|
||||||
|
document.getElementById('receipt-iva-print').innerText = clp.format(iva);
|
||||||
|
|
||||||
document.getElementById('receipt-total-print').innerText = clp.format(total);
|
document.getElementById('receipt-total-print').innerText = clp.format(total);
|
||||||
document.getElementById('receipt-paid-print').innerText = clp.format(finalPaid);
|
document.getElementById('receipt-paid-print').innerText = clp.format(finalPaid);
|
||||||
document.getElementById('receipt-change-print').innerText = clp.format(finalPaid - total);
|
document.getElementById('receipt-change-print').innerText = clp.format(finalPaid - total);
|
||||||
@@ -1037,7 +1046,6 @@
|
|||||||
currentOrderNotes = '';
|
currentOrderNotes = '';
|
||||||
currentPickupTime = '';
|
currentPickupTime = '';
|
||||||
|
|
||||||
|
|
||||||
// Check the setting before printing
|
// Check the setting before printing
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (localStorage.getItem('seki_auto_print') !== 'false') {
|
if (localStorage.getItem('seki_auto_print') !== 'false') {
|
||||||
|
|||||||
@@ -56,29 +56,103 @@
|
|||||||
#receipt-print-zone{{ id_suffix }} {
|
#receipt-print-zone{{ id_suffix }} {
|
||||||
position: absolute; left: 0; top: 0; width: 80mm;
|
position: absolute; left: 0; top: 0; width: 80mm;
|
||||||
padding: 2mm 5mm; margin: 0; display: block !important;
|
padding: 2mm 5mm; margin: 0; display: block !important;
|
||||||
font-family: 'Courier New', Courier, monospace; font-size: 10px; color: #000;
|
font-family: 'Courier New', Courier, monospace; font-size: 11px; color: #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.receipt-table { width: 100%; border-collapse: collapse; font-family: monospace; font-size: 12px; }
|
.receipt-table { width: 100%; border-collapse: collapse; font-family: 'Courier New', Courier, monospace; font-size: 11px; margin-top: 10px;}
|
||||||
.receipt-header { text-align: center; margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 5px; }
|
.receipt-header { text-align: center; margin-bottom: 5px; }
|
||||||
.receipt-total-row { border-top: 1px dashed #000; margin-top: 5px; padding-top: 5px; font-weight: bold; }
|
.sii-box { border: 2px solid #000; padding: 5px; text-align: center; font-weight: bold; }
|
||||||
|
.receipt-subtotal-row { display: flex; justify-content: space-between; font-size: 11px; }
|
||||||
|
.receipt-total-row { margin-top: 2px; padding-top: 2px; font-weight: bold; font-size: 14px; display: flex; justify-content: space-between; }
|
||||||
|
.ted-block { text-align: center; margin-top: 15px; margin-bottom: 10px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="receipt-header">
|
<div class="receipt-header">
|
||||||
<h3 style="margin: 0; font-weight: 800;" class="receipt-biz-name">SekiPOS</h3>
|
<h3 style="margin: 0; font-weight: 800; font-size: 16px;" class="receipt-biz-name text-uppercase">SekiPOS</h3>
|
||||||
|
|
||||||
|
<div class="receipt-sii-info" style="display: none; font-size: 10px; margin-top: 2px;">
|
||||||
|
<div class="receipt-giro text-uppercase"></div>
|
||||||
|
<div class="receipt-address text-uppercase"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt-title-box" style="text-align: center; font-weight: bold; margin: 10px 0;">
|
||||||
|
<div class="receipt-sii-info" style="display: none;">
|
||||||
|
<div style="font-size: 12px;">R.U.T.: <span class="receipt-rut"></span></div>
|
||||||
|
<div style="font-size: 12px; margin-top: 2px;">BOLETA ELECTRONICA</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt-standard-info" style="font-size: 12px;">
|
||||||
|
COMPROBANTE DE VENTA
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 2px; font-size: 12px;">
|
||||||
|
Ticket Nº <span id="receipt-ticket-id{{ id_suffix }}"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt-sii-info" style="display: none; font-weight: bold; font-size: 10px; margin-bottom: 10px;">
|
||||||
|
S.I.I. - CONCEPCION
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.receipt-biz-name').forEach(el => {
|
const bizName = localStorage.getItem('seki_biz_name') || 'SekiPOS';
|
||||||
el.innerText = localStorage.getItem('seki_biz_name') || 'SekiPOS';
|
const showSii = localStorage.getItem('seki_show_sii') === 'true';
|
||||||
|
|
||||||
|
document.querySelectorAll('.receipt-biz-name').forEach(el => el.innerText = bizName);
|
||||||
|
|
||||||
|
// Toggle visibility based on SII mode
|
||||||
|
document.querySelectorAll('.receipt-sii-info').forEach(el => el.style.display = showSii ? 'block' : 'none');
|
||||||
|
document.querySelectorAll('.receipt-standard-info').forEach(el => el.style.display = showSii ? 'none' : 'block');
|
||||||
|
|
||||||
|
// Toggle the heavy border box
|
||||||
|
document.querySelectorAll('.receipt-title-box').forEach(el => {
|
||||||
|
if (showSii) {
|
||||||
|
el.classList.add('sii-box');
|
||||||
|
el.style.margin = '10px 15%';
|
||||||
|
} else {
|
||||||
|
el.classList.remove('sii-box');
|
||||||
|
el.style.margin = '10px 0';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (showSii) {
|
||||||
|
document.querySelectorAll('.receipt-rut').forEach(el => el.innerText = localStorage.getItem('seki_rut') || '-');
|
||||||
|
document.querySelectorAll('.receipt-giro').forEach(el => el.innerText = localStorage.getItem('seki_giro') || '-');
|
||||||
|
document.querySelectorAll('.receipt-address').forEach(el => el.innerText = localStorage.getItem('seki_address') || '-');
|
||||||
|
|
||||||
|
// Load Barcode generator for the SII PDF417 TED
|
||||||
|
if (!document.getElementById('bwip-script')) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = 'bwip-script';
|
||||||
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/bwip-js/3.4.1/bwip-js-min.js';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tedData = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI/Pgo8VEVEIHZlcnNpb249IjEuMCI+PEREPjxSRT43NjEyMzQ1Ni03PC9SRT48VEQ+Mzk8L1REPjxGPjEyMzwvRj48RkU+MjAyNi0wNC0xNTwvRkU+PFJSPjExMTExMTExLTE8L1JSPjxSU1I+SlVBTiBQRVJFPC9SU1I+PE1OVD4xNTAwMDwvTU5UPjxJVDE+VmFyaW9zPC9JVDE+PENBRiB2ZXJzaW9uPSIxLjAiPjxEQT48UkU+NzYxMjM0NTYtNzwvUkU+PFJTPlNLSVBPUyBMSU1JVEFEQTwvUlM+PFREPjM5PC9URD48Uk5HPjxEPjE8L0Q+PEg+MTAwMDwvSD48L1JORz48RkE+MjAyNi0wNC0xNTwvRkE+PFJTQVBLPjxNPndYeFl5WnouLi48L00+PEU+QXc9PTwvRT48L1JTQVBLPjxJREs+MTAwPC9JREs+PC9EQT48RlJNQSBhbGdvcml0bW89IlNIQTF3aXRoUlNBIj5hQmNEZUY8L0ZSTUE+PC9DQUY+PFRTVEVEPjIwMjYtMDQtMTVUMTI6MDA6MDA8L1RTVEVEPjwvREQ+PEZSTVQgYWxnb3JpdG1vPSJTSEExd2l0aFJTQSI+elp5WXhYPC9GUk1UPjwvVEVEPg==";
|
||||||
|
|
||||||
|
const drawTED = () => {
|
||||||
|
if (typeof bwipjs === 'undefined') {
|
||||||
|
setTimeout(drawTED, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
bwipjs.toCanvas('receipt-ted-canvas{{ id_suffix }}', {
|
||||||
|
bcid: 'pdf417',
|
||||||
|
text: tedData,
|
||||||
|
scale: 2,
|
||||||
|
columns: 12
|
||||||
|
});
|
||||||
|
} catch (e) { console.error("Error drawing TED:", e); }
|
||||||
|
};
|
||||||
|
drawTED();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="font-size: 10px; margin-bottom: 5px;" id="receipt-type{{ id_suffix }}">Comprobante de Venta</div>
|
<div style="font-size: 11px; text-align: left; display: flex; justify-content: space-between;">
|
||||||
<div style="font-size: 11px; font-weight: bold;">
|
<span>Fecha: <span id="receipt-date{{ id_suffix }}"></span></span>
|
||||||
Ticket Nº <span id="receipt-ticket-id{{ id_suffix }}"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="receipt-date{{ id_suffix }}" style="font-size: 11px;"></div>
|
|
||||||
|
|
||||||
<div id="receipt-order-info{{ id_suffix }}" style="display: none; margin-top: 5px; padding-top: 5px; border-top: 1px dashed #000; text-align: left; font-size: 11px;">
|
<div id="receipt-order-info{{ id_suffix }}" style="display: none; margin-top: 5px; padding-top: 5px; border-top: 1px dashed #000; text-align: left; font-size: 11px;">
|
||||||
<div style="font-weight: bold;">Cliente: <span id="receipt-client-name{{ id_suffix }}"></span></div>
|
<div style="font-weight: bold;">Cliente: <span id="receipt-client-name{{ id_suffix }}"></span></div>
|
||||||
@@ -89,32 +163,54 @@
|
|||||||
|
|
||||||
<table class="receipt-table">
|
<table class="receipt-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr style="border-bottom: 1px dashed #000; border-top: 1px dashed #000;">
|
||||||
<th style="width: 15%; text-align: left;">Cant</th>
|
<th style="width: 15%; text-align: left; padding: 2px 0;">Cant</th>
|
||||||
<th style="width: 60%; padding-left: 5px; text-align: left;">Desc</th>
|
<th style="width: 55%; padding-left: 5px; text-align: left;">Articulo</th>
|
||||||
<th style="width: 25%; text-align: right;">Total</th>
|
<th style="width: 30%; text-align: right;">Total</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="receipt-items-print{{ id_suffix }}"></tbody>
|
<tbody id="receipt-items-print{{ id_suffix }}"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="receipt-total-row d-flex justify-content-between">
|
<div style="border-top: 1px dashed #000; margin-top: 5px; padding-top: 5px;">
|
||||||
<span>TOTAL:</span>
|
<div class="receipt-subtotal-row">
|
||||||
<span id="receipt-total-print{{ id_suffix }}"></span>
|
<span>NETO:</span>
|
||||||
|
<span id="receipt-neto-print{{ id_suffix }}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-subtotal-row">
|
||||||
|
<span>I.V.A. (19%):</span>
|
||||||
|
<span id="receipt-iva-print{{ id_suffix }}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-total-row">
|
||||||
|
<span>TOTAL:</span>
|
||||||
|
<span id="receipt-total-print{{ id_suffix }}"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="receipt-payment-info{{ id_suffix }}">
|
<div id="receipt-payment-info{{ id_suffix }}" style="font-size: 11px; margin-top: 5px;">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<span>RECIBIDO:</span>
|
<span>Efectivo:</span>
|
||||||
<span id="receipt-paid-print{{ id_suffix }}"></span>
|
<span id="receipt-paid-print{{ id_suffix }}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<span>VUELTO:</span>
|
<span>Vuelto:</span>
|
||||||
<span id="receipt-change-print{{ id_suffix }}"></span>
|
<span id="receipt-change-print{{ id_suffix }}"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 20px; font-size: 10px;">¡Gracias por su compra!</div>
|
<div class="receipt-sii-info ted-block" style="display: none;">
|
||||||
|
<canvas id="receipt-ted-canvas{{ id_suffix }}" style="max-width: 90%; height: auto; image-rendering: pixelated; margin: 0 auto; display: block;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt-sii-info" style="display: none; text-align: center; font-size: 9px; font-weight: bold;">
|
||||||
|
Timbre electrónico SII - Res. 80 de 2014<br>
|
||||||
|
Verifique documento en sii.cl<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 15px; font-size: 11px; font-weight: bold;">
|
||||||
|
¡Gracias por su compra!
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@@ -126,17 +222,31 @@
|
|||||||
<h5 class="modal-title"><i class="bi bi-gear-fill me-2"></i>Configuración del POS</h5>
|
<h5 class="modal-title"><i class="bi bi-gear-fill me-2"></i>Configuración del POS</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label text-muted small mb-1">Nombre del Local (Impreso en la Boleta)</label>
|
<label class="form-label text-muted small mb-1">Nombre del Local</label>
|
||||||
<input type="text" id="setting-biz-name" class="form-control" placeholder="Ej: Mi Tiendita" autocomplete="off">
|
<input type="text" id="setting-biz-name" class="form-control" placeholder="Ej: Mi Tiendita" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="setting-show-sii" onchange="toggleSiiFields()">
|
||||||
|
<label class="form-check-label text-muted small" for="setting-show-sii">
|
||||||
|
Spoof datos legales para SII (Boleta)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="setting-sii-fields" class="d-none border-start border-2 border-primary ps-3 mb-4 mt-2">
|
||||||
|
<input type="text" id="setting-rut" class="form-control form-control-sm mb-2" placeholder="RUT (Ej: 76.123.456-7)">
|
||||||
|
<input type="text" id="setting-giro" class="form-control form-control-sm mb-2" placeholder="Giro (Ej: Minimarket)">
|
||||||
|
<input type="text" id="setting-address" class="form-control form-control-sm" placeholder="Dirección y Comuna">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
<div class="form-check form-switch mb-3">
|
||||||
<input class="form-check-input" type="checkbox" id="setting-auto-print">
|
<input class="form-check-input" type="checkbox" id="setting-auto-print">
|
||||||
<label class="form-check-label text-muted small" for="setting-auto-print">
|
<label class="form-check-label text-muted small" for="setting-auto-print">
|
||||||
Imprimir automáticamente al finalizar venta
|
Imprimir automáticamente al finalizar venta
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-2">
|
<div class="form-check form-switch mb-2">
|
||||||
<input class="form-check-input" type="checkbox" id="setting-ask-order-details">
|
<input class="form-check-input" type="checkbox" id="setting-ask-order-details">
|
||||||
<label class="form-check-label text-muted small" for="setting-ask-order-details">
|
<label class="form-check-label text-muted small" for="setting-ask-order-details">
|
||||||
@@ -152,6 +262,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function toggleSiiFields() {
|
||||||
|
const isChecked = document.getElementById('setting-show-sii').checked;
|
||||||
|
document.getElementById('setting-sii-fields').classList.toggle('d-none', !isChecked);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const modalEl = document.getElementById('settingsModal');
|
const modalEl = document.getElementById('settingsModal');
|
||||||
if (modalEl) {
|
if (modalEl) {
|
||||||
@@ -159,6 +274,14 @@
|
|||||||
document.getElementById('setting-biz-name').value = localStorage.getItem('seki_biz_name') || 'SekiPOS';
|
document.getElementById('setting-biz-name').value = localStorage.getItem('seki_biz_name') || 'SekiPOS';
|
||||||
document.getElementById('setting-auto-print').checked = localStorage.getItem('seki_auto_print') !== 'false';
|
document.getElementById('setting-auto-print').checked = localStorage.getItem('seki_auto_print') !== 'false';
|
||||||
document.getElementById('setting-ask-order-details').checked = localStorage.getItem('seki_ask_order_details') === 'true';
|
document.getElementById('setting-ask-order-details').checked = localStorage.getItem('seki_ask_order_details') === 'true';
|
||||||
|
|
||||||
|
const showSii = localStorage.getItem('seki_show_sii') === 'true';
|
||||||
|
document.getElementById('setting-show-sii').checked = showSii;
|
||||||
|
document.getElementById('setting-rut').value = localStorage.getItem('seki_rut') || '';
|
||||||
|
document.getElementById('setting-giro').value = localStorage.getItem('seki_giro') || '';
|
||||||
|
document.getElementById('setting-address').value = localStorage.getItem('seki_address') || '';
|
||||||
|
|
||||||
|
toggleSiiFields();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -167,14 +290,42 @@
|
|||||||
const bizName = document.getElementById('setting-biz-name').value.trim() || 'SekiPOS';
|
const bizName = document.getElementById('setting-biz-name').value.trim() || 'SekiPOS';
|
||||||
const autoPrint = document.getElementById('setting-auto-print').checked;
|
const autoPrint = document.getElementById('setting-auto-print').checked;
|
||||||
const askDetails = document.getElementById('setting-ask-order-details').checked;
|
const askDetails = document.getElementById('setting-ask-order-details').checked;
|
||||||
|
const showSii = document.getElementById('setting-show-sii').checked;
|
||||||
|
|
||||||
localStorage.setItem('seki_biz_name', bizName);
|
localStorage.setItem('seki_biz_name', bizName);
|
||||||
localStorage.setItem('seki_auto_print', autoPrint);
|
localStorage.setItem('seki_auto_print', autoPrint);
|
||||||
localStorage.setItem('seki_ask_order_details', askDetails);
|
localStorage.setItem('seki_ask_order_details', askDetails);
|
||||||
|
localStorage.setItem('seki_show_sii', showSii);
|
||||||
|
|
||||||
document.querySelectorAll('.receipt-biz-name').forEach(el => { el.innerText = bizName; });
|
if (showSii) {
|
||||||
|
localStorage.setItem('seki_rut', document.getElementById('setting-rut').value.trim());
|
||||||
|
localStorage.setItem('seki_giro', document.getElementById('setting-giro').value.trim());
|
||||||
|
localStorage.setItem('seki_address', document.getElementById('setting-address').value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.receipt-biz-name').forEach(el => el.innerText = bizName);
|
||||||
|
|
||||||
|
document.querySelectorAll('.receipt-sii-info').forEach(el => el.style.display = showSii ? 'block' : 'none');
|
||||||
|
document.querySelectorAll('.receipt-standard-info').forEach(el => el.style.display = showSii ? 'none' : 'block');
|
||||||
|
|
||||||
|
document.querySelectorAll('.receipt-title-box').forEach(el => {
|
||||||
|
if (showSii) {
|
||||||
|
el.classList.add('sii-box');
|
||||||
|
el.style.margin = '10px 15%';
|
||||||
|
} else {
|
||||||
|
el.classList.remove('sii-box');
|
||||||
|
el.style.margin = '10px 0';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showSii) {
|
||||||
|
document.querySelectorAll('.receipt-rut').forEach(el => el.innerText = localStorage.getItem('seki_rut') || '-');
|
||||||
|
document.querySelectorAll('.receipt-giro').forEach(el => el.innerText = localStorage.getItem('seki_giro') || '-');
|
||||||
|
document.querySelectorAll('.receipt-address').forEach(el => el.innerText = localStorage.getItem('seki_address') || '-');
|
||||||
|
}
|
||||||
|
|
||||||
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
|
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
Reference in New Issue
Block a user