63 Commits

Author SHA1 Message Date
5119d4bb31 Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-03-13 21:55:23 -03:00
acaf537f11 reverese engeneering of TM1621 display 2026-03-13 21:54:45 -03:00
55ff314163 Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-03-13 18:07:54 -03:00
376b8c54a6 Version bump to v2.0, updated checkout and dicom 2026-03-13 18:07:09 -03:00
1a048a0e07 Fixed recepit lenght 2026-03-11 16:52:17 -03:00
77fc5920a2 image export like db 2026-03-11 15:59:50 -03:00
78d48db9ea added image_url column to dicom table 2026-03-10 23:39:44 -03:00
92e3a3f0f9 dicom evidence update 2026-03-10 23:22:50 -03:00
57cb27f6cf db dump tool 2026-03-10 22:47:40 -03:00
c24dae9694 actions button position fix 2026-03-10 22:41:34 -03:00
e0ac23a8e0 re-print old receipt from sales page 2026-03-10 22:37:47 -03:00
c1a06dc44c allow for inmediate confimation of checkout 2026-03-10 22:32:37 -03:00
72f6e0c822 receipt print fix 2026-03-10 22:25:09 -03:00
b2418d8c7e style fixes 2026-03-10 22:15:18 -03:00
9cb057668b login style fix 2026-03-10 21:41:15 -03:00
cb2aa89b16 WIP rewrite with macros 2026-03-10 20:15:58 -03:00
ef9a9296dd initial modularization + templates 2026-03-10 18:40:47 -03:00
3c4b2e148d Implemented Dicom 2026-03-09 18:25:49 -03:00
bf1bc84cd0 Checkout UI fixes 2026-03-09 16:54:55 -03:00
8e37f9e776 Implemented change calculator and various UI fixes 2026-03-09 16:32:23 -03:00
216abc8ad2 Fixed receipt design 2026-03-09 13:02:00 -03:00
cffa3d789b modified: templates/checkout.html 2026-03-09 11:34:49 -03:00
d7ef1573e5 modified: templates/checkout.html 2026-03-09 10:20:59 -03:00
e101833c7d modified: templates/checkout.html 2026-03-09 09:53:30 -03:00
6c98919c80 Added option forcustom product during checkout 2026-03-09 07:00:47 -03:00
cae35a266f Implemented receipt generation and printing 2026-03-09 06:20:24 -03:00
c57e8ab6db Implemented sales dashboard 2026-03-09 05:58:24 -03:00
135b14adcf Added persistent cart for chechout 2026-03-09 05:06:25 -03:00
9f59e122ef Various chechout improvements: subtotal rounding, placeholder color, and weight input now in grams for better UX. 2026-03-09 03:53:28 -03:00
43cc2a3caa Fixed UI inconsistencies and added logic to handle unit types in the product list and form. Also added a call to toggle the stock input visibility on page load based on the default unit type. 2026-03-09 02:55:07 -03:00
shironeko
2f2998b0fd esp attempt, stock + unit type 2026-03-07 19:21:14 -03:00
788b67804e oops 2026-03-07 03:35:22 -03:00
2bb38570f9 deactivated MP stuff 2026-03-07 03:23:00 -03:00
aacbce2557 oops 2026-03-07 03:07:55 -03:00
6c5085093d chekout update, mp study v1 2026-03-07 03:07:45 -03:00
423d563cc0 image compression, initial checkout(? 2026-03-04 22:47:02 -03:00
5e79b6938c delimiter character setup + extended api data 2026-03-01 03:54:05 -03:00
dcd14f1021 Scanner with LF thing to prevent partial scanning 2026-03-01 03:45:37 -03:00
b4344361e4 adding data to scan request + Go ARMv7 2026-03-01 03:23:53 -03:00
fcb75cb5a4 settings file for scanner 2026-03-01 02:21:17 -03:00
676f299796 version bump 2026-02-28 22:41:33 -03:00
751c77cac5 zoom + flash + focus = scanning improvements for iphone and android 2026-02-28 22:39:57 -03:00
85b2c0b4db more theme and camera fixes TwT 2026-02-27 01:11:54 -03:00
741690b30e X fix 2026-02-27 01:06:42 -03:00
7235c7ff09 theme and camera cookie 2026-02-27 00:57:45 -03:00
8cba7937c3 image cache fix 2026-02-27 00:29:25 -03:00
4779452acd image timestamp fix 2026-02-27 00:16:43 -03:00
600df52b04 camera index? 2026-02-27 00:03:55 -03:00
b1b99bc887 webui camera barcode scanner 2026-02-26 23:59:22 -03:00
c7c0b3feb2 picture qol fixes 2026-02-26 23:55:49 -03:00
184f2722bf sort table 2026-02-26 23:38:55 -03:00
0f9966d224 qol and scan fixes 2026-02-26 23:29:45 -03:00
43b2a9e2d5 version bump :p 2026-02-26 22:53:44 -03:00
3a39cb95db re-retriede image if cached one doesnt exist anymore 2026-02-26 22:52:34 -03:00
81cacd3589 bulk delete + more prompts 2026-02-26 22:50:20 -03:00
1f521ec1d2 colors and prompts fix 2026-02-26 22:33:38 -03:00
0dcf0bc930 modified: static/styleIndex.css
modified:   templates/index.html
2026-02-26 21:46:42 -03:00
df4ff9171d Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-02-26 21:16:41 -03:00
1b2e63bc86 added bulk price update feature, allowing users to select multiple products and apply a new price to all of them at once. The bulk action bar now shows the count of selected items and enables the "OK" button only when at least one product is selected. 2026-02-26 21:16:16 -03:00
80bf539484 Merge branch 'main' of https://gitea.sekidesu.xyz/SekiDesu01/SekiPOS 2026-02-26 20:48:56 -03:00
13bba33c26 modified: templates/index.html 2026-02-26 04:21:20 -03:00
ecd98c72ce new file: ScannerPython/output/scannerV2.exe
new file:   ScannerPython/scannerV2.py
2026-02-26 04:10:48 -03:00
344229b77b bump version 2026-02-26 03:35:34 -03:00
167 changed files with 4704 additions and 3206 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
pos_database.db
ScannerGO/ScannerGO-*
ScannerGO/config.json
DataToolsGO/imageTools-*
.env

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Build SekiPOS (F5)",
"type": "node",
"request": "launch",
"preLaunchTask": "build-sekipos"
}
]
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python-envs.defaultEnvManager": "ms-python.python:pyenv"
}

15
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-sekipos",
"type": "shell",
"command": "docker build -t sekipos:latest .",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new"
}
}
]
}

42
DataToolsGO/build.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Define binary names
LINUX_BIN="imageTools-linux"
LINUX_ARM_BIN="imageTools-linuxARMv7"
WINDOWS_BIN="imageTools-windows.exe"
echo "Starting build process..."
# Build for Linux (64-bit)
echo "Building for Linux..."
GOOS=linux GOARCH=amd64 go build -o "$LINUX_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $LINUX_BIN"
else
echo "Failed to build Linux binary"
exit 1
fi
# Build for Windows (64-bit)
echo "Building for Windows..."
GOOS=windows GOARCH=amd64 go build -o "$WINDOWS_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $WINDOWS_BIN"
else
echo "Failed to build Windows binary"
exit 1
fi
# Build for Linux ARM (ARMv7)
echo "Building for Linux ARMv7..."
GOOS=linux GOARCH=arm GOARM=7 go build -o "$LINUX_ARM_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $LINUX_ARM_BIN"
else
echo "Failed to build Linux ARMv7 binary"
exit 1
fi
echo "Build complete."

5
DataToolsGO/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module dataTools
go 1.25.7
require github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646

2
DataToolsGO/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=

84
DataToolsGO/main.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"flag"
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strings"
"github.com/nfnt/resize"
)
func main() {
// Command line arguments
dirPath := flag.String("dir", "./", "Directory containing images")
maxWidth := flag.Uint("width", 1000, "Maximum width for resizing")
quality := flag.Int("quality", 75, "JPEG quality (1-100)")
flag.Parse()
files, err := os.ReadDir(*dirPath)
if err != nil {
fmt.Printf("Error reading directory: %v\n", err)
return
}
fmt.Printf("Processing images in %s (Max Width: %d, Quality: %d)\n", *dirPath, *maxWidth, *quality)
for _, file := range files {
if file.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(file.Name()))
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
continue
}
filePath := filepath.Join(*dirPath, file.Name())
processImage(filePath, *maxWidth, *quality)
}
fmt.Println("Done. Your storage can finally breathe again.")
}
func processImage(path string, maxWidth uint, quality int) {
file, err := os.Open(path)
if err != nil {
fmt.Printf("Failed to open %s: %v\n", path, err)
return
}
img, _, err := image.Decode(file)
file.Close()
if err != nil {
fmt.Printf("Failed to decode %s: %v\n", path, err)
return
}
// Only resize if original is wider than maxWidth
bounds := img.Bounds()
var finalImg image.Image
if uint(bounds.Dx()) > maxWidth {
finalImg = resize.Resize(maxWidth, 0, img, resize.Lanczos3)
fmt.Printf("Resized and compressed: %s\n", filepath.Base(path))
} else {
finalImg = img
fmt.Printf("Compressed (no resize needed): %s\n", filepath.Base(path))
}
// Overwrite the original file
out, err := os.Create(path)
if err != nil {
fmt.Printf("Failed to create output file %s: %v\n", path, err)
return
}
defer out.Close()
err = jpeg.Encode(out, finalImg, &jpeg.Options{Quality: quality})
if err != nil {
fmt.Printf("Failed to encode %s: %v\n", path, err)
}
}

View File

@@ -10,6 +10,7 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY templates/ ./templates/
COPY static/ ./static/
#COPY .env .
# Create the folder structure for the volume mounts
RUN mkdir -p /app/static/cache

File diff suppressed because it is too large Load Diff

162
KeyGenerator/frutas2.json Normal file
View File

@@ -0,0 +1,162 @@
[
{
"name": "Paltas",
"plu": "3509",
"image": "https://server-ifps.accurateig.com/assets/commodities/3509-gem-avocado_1625011346.JPG"
},
{
"name": "Papas",
"plu": "3414",
"image": "https://server-ifps.accurateig.com/assets/commodities/3414-baking-potato-white_1635179333.jpg"
},
{
"name": "Limones",
"plu": "3626",
"image": "https://server-ifps.accurateig.com/assets/commodities/3626-meyer-lemons_1460404763.jpg"
},
{
"name": "Cebollines / Cebollas",
"plu": "4068",
"image": "https://server-ifps.accurateig.com/assets/commodities/4068-onions-green-2_1625063600.jpg"
},
{
"name": "Ajo",
"plu": "4608",
"image": "https://server-ifps.accurateig.com/assets/commodities/4608-garlic-regular_1637184640.jpg"
},
{
"name": "Zanahorias",
"plu": "4560",
"image": "https://server-ifps.accurateig.com/assets/commodities/4560-carrots-baby_1625673556.jpg"
},
{
"name": "Plátanos",
"plu": "4235",
"image": "https://server-ifps.accurateig.com/assets/commodities/4235-plantains-01_1625076376.jpg"
},
{
"name": "Manzanas",
"plu": "4099",
"image": "https://server-ifps.accurateig.com/assets/commodities/apples-akane_1629314651.png"
},
{
"name": "Naranjas",
"plu": "4381",
"image": "https://server-ifps.accurateig.com/assets/commodities/4381-oranges-blood-01_1625082045.jpg"
},
{
"name": "Choclo",
"plu": "3087",
"image": "https://server-ifps.accurateig.com/assets/commodities/3087-corn-04_1614633780.jpg"
},
{
"name": "Zapallo",
"plu": "4734",
"image": "https://server-ifps.accurateig.com/assets/commodities/4734-mini-pumpkin_1633964765.jpg"
},
{
"name": "Zapallo italiano",
"plu": "4750",
"image": "https://server-ifps.accurateig.com/assets/commodities/4750-squash-acorn-01_1625751871.jpg"
},
{
"name": "Repollo",
"plu": "4069",
"image": "https://server-ifps.accurateig.com/assets/commodities/4069-green-cabbage_1633958066.jpg"
},
{
"name": "Betarragas",
"plu": "4537",
"image": "https://server-ifps.accurateig.com/assets/commodities/4537-baby-golden-beets_1635173500.jpg"
},
{
"name": "Porotos verdes",
"plu": "4527",
"image": "https://server-ifps.accurateig.com/assets/commodities/4527-beans-chinese-long-07_1625671743.jpg"
},
{
"name": "Mandarinas",
"plu": "3524",
"image": "https://server-ifps.accurateig.com/assets/commodities/noble-juicycrunch-productisolated-900x900-rgb-copy_1627662136.png"
},
{
"name": "Uvas",
"plu": "3491",
"image": "https://server-ifps.accurateig.com/assets/commodities/arra-15-grape_1518122851.JPG"
},
{
"name": "Duraznos",
"plu": "3113",
"image": "https://server-ifps.accurateig.com/assets/commodities/3113-peaches-donut-04_1614707155.jpg"
},
{
"name": "Cerezas",
"plu": "3549",
"image": "https://server-ifps.accurateig.com/assets/commodities/screenshot-2023-05-02-100428_1683036305.png"
},
{
"name": "Peras",
"plu": "3317",
"image": "https://server-ifps.accurateig.com/assets/commodities/3317-angelys-pear_1460402454.jpg"
},
{
"name": "Ciruelas",
"plu": "4435",
"image": "https://server-ifps.accurateig.com/assets/commodities/4435-plums-greengage-01_1625667846.jpg"
},
{
"name": "Kiwi",
"plu": "3279",
"image": "https://server-ifps.accurateig.com/assets/commodities/3279-kiwi-gold-03_1614718637.jpg"
},
{
"name": "Piña",
"plu": "4430",
"image": "https://server-ifps.accurateig.com/assets/commodities/4430-pineapple-05_1625667649.jpg"
},
{
"name": "Puerros",
"plu": "4629",
"image": "https://server-ifps.accurateig.com/assets/commodities/4629-leeks-02_1625680225.jpg"
},
{
"name": "Alcachofas",
"plu": "4519",
"image": "https://server-ifps.accurateig.com/assets/commodities/4519-artichokes-baby-02_1625671366.jpg"
},
{
"name": "Espárragos",
"plu": "4521",
"image": "https://server-ifps.accurateig.com/assets/commodities/4521-asparagus-01_1625671446.jpg"
},
{
"name": "Berenjena",
"plu": "4599",
"image": "https://server-ifps.accurateig.com/assets/commodities/4599-baby-eggplant-aubergine_1633372314.jpg"
},
{
"name": "Durazno Nectarin",
"plu": "3437",
"image": "https://server-ifps.accurateig.com/assets/commodities/yellow-fleshed-flat-nectarine_1629140965.jpg"
},
{
"name": "Damascos",
"plu": "3044",
"image": "https://server-ifps.accurateig.com/assets/commodities/3044-apricots-black-velvet-03_1614619576.jpg"
},
{
"name": "Membrillo",
"plu": "4447",
"image": "https://server-ifps.accurateig.com/assets/commodities/4447-quince-1_1625668926.jpg"
},
{
"name": "Champiñones",
"plu": "4647",
"image": "https://server-ifps.accurateig.com/assets/commodities/4647-mushrooms-chanterelles-1_1625681503.jpg"
},
{
"name": "Castañas",
"plu": "4927",
"image": "https://server-ifps.accurateig.com/assets/commodities/4927-chestnuts-italian-02_1625756943.jpg"
}
]

View File

@@ -11,7 +11,7 @@ from io import BytesIO
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# --- CONFIGURATION ---
JSON_FILE = os.path.join(os.getcwd(), 'frutas.json')
JSON_FILE = os.path.join(os.getcwd(), 'frutas2.json')
OUTPUT_DIR = os.path.join(os.getcwd(), 'keychain_cards')
IMG_CACHE_DIR = os.path.join(os.getcwd(), 'image_cache')
os.makedirs(OUTPUT_DIR, exist_ok=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,437 +0,0 @@
[
{
"PLU": "4924",
"Name": "Almonds",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4924-almonds_1630598936.jpg"
},
{
"PLU": "3525",
"Name": "Apples",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3486-cn121-apple-image_1627662287.JPG"
},
{
"PLU": "4218",
"Name": "Apricots",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3302-apricots-1-1614718940_1633443369.jpg"
},
{
"PLU": "4516",
"Name": "Artichokes",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4516-artichokes_1630598556.jpg"
},
{
"PLU": "4080",
"Name": "Asparagus",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4521-asparagus-01-1625671446_1632939137.jpg"
},
{
"PLU": "4046",
"Name": "Avocados",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4770-hass-avocado_1635960530.jpg"
},
{
"PLU": "3303",
"Name": "Babaco",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3303-babacoa_1614719281.JPG"
},
{
"PLU": "4186",
"Name": "Bananas",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4011-yellow-banana_1634142809.jpg"
},
{
"PLU": "4528",
"Name": "Beans",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4528-beans-fava-07_1625671831.jpg"
},
{
"PLU": "4537",
"Name": "Beets",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4537-baby-golden-beets_1635173500.jpg"
},
{
"PLU": "4247",
"Name": "Berries",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4028-berries-strawberries-06-1625013614_1633444773.jpg"
},
{
"PLU": "4926",
"Name": "Brazilnuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4926-brazil-nuts-02_1625756785.JPG"
},
{
"PLU": "4069",
"Name": "Cabbage",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4069-green-cabbage_1633958066.jpg"
},
{
"PLU": "4560",
"Name": "Carrots",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4560-carrots-baby_1625673556.jpg"
},
{
"PLU": "3105",
"Name": "Cashews",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3105-cashews-03_1614635878.JPG"
},
{
"PLU": "3321",
"Name": "Celery Root/Celeriac",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3321-celery-root-03_1614720213.jpg"
},
{
"PLU": "3357",
"Name": "Cherries",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4045-cherries-05-1625061853_1633445797.jpg"
},
{
"PLU": "4927",
"Name": "Chestnuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4927-chestnuts-italian-02_1625756943.jpg"
},
{
"PLU": "4261",
"Name": "Coconuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4261-coconuts-02_1625077777.jpg"
},
{
"PLU": "3087",
"Name": "Corn",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3087-corn-04_1614633780.jpg"
},
{
"PLU": "4862",
"Name": "Dates",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4862-dates_1630598790.jpg"
},
{
"PLU": "4892",
"Name": "Dill",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4892-dill-baby-02_1625755376.jpg"
},
{
"PLU": "4599",
"Name": "Eggplant (Aubergine)",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4599-baby-eggplant-aubergine_1633372314.jpg"
},
{
"PLU": "4606",
"Name": "Fiddlehead Ferns",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4606-fiddlehead-ferns-07_1625679559.jpg"
},
{
"PLU": "4266",
"Name": "Figs",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4266-figs-03_1625077969.jpg"
},
{
"PLU": "4608",
"Name": "Garlic",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4608-garlic-regular_1637184640.jpg"
},
{
"PLU": "4612",
"Name": "Ginger Root",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4612-ginger-root-06_1625679690.jpg"
},
{
"PLU": "3091",
"Name": "Gobo Root/Burdock",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3091gobo-root-02_1614634374.jpg"
},
{
"PLU": "4280",
"Name": "Grapefruit",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4492-grapefruit-ruby-red_1633960726.jpg"
},
{
"PLU": "3505",
"Name": "Grapes",
"URL": "https://server-ifps.accurateig.com/assets/commodities/arra-32-image-3_1629296403.PNG"
},
{
"PLU": "4614",
"Name": "Greens",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4614-collard-greens-trimmed_1625679839.JPG"
},
{
"PLU": "4625",
"Name": "Horseradish Root",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4625-horseradish-root-04_1625680026.jpg"
},
{
"PLU": "4626",
"Name": "Jicama/Yam Bean",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4626-jicama_1635179919.jpg"
},
{
"PLU": "3280",
"Name": "Kiwifruit",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4030-kiwi-fruit-04-1625013864_1633447661.jpg"
},
{
"PLU": "4303",
"Name": "Kumquat",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4303-kumquats-03_1625079229.jpg"
},
{
"PLU": "4629",
"Name": "Leeks",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4629-leeks-02_1625680225.jpg"
},
{
"PLU": "4033",
"Name": "Lemons",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4033-lemons_1630597866.jpg"
},
{
"PLU": "4328",
"Name": "Limequats",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4328-limequats-01_1625081231.jpg"
},
{
"PLU": "4048",
"Name": "Limes",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4048-limes-02_1625062144.jpg"
},
{
"PLU": "4308",
"Name": "Loquats",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4308-lychees-5_1625079574.jpg"
},
{
"PLU": "3099",
"Name": "Lotus Root",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3099-lotus-root-03_1614635240.jpg"
},
{
"PLU": "3106",
"Name": "Macadamia",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3106-macadamias_1614705972.JPG"
},
{
"PLU": "4644",
"Name": "Malanga",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4644-malanga_1633956612.jpg"
},
{
"PLU": "3475",
"Name": "Mint",
"URL": "https://server-ifps.accurateig.com/assets/commodities/peppermint-3475_1625790587.jpg"
},
{
"PLU": "4645",
"Name": "Mushrooms",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4645-mushrooms-white-button-02_1625681263.jpg"
},
{
"PLU": "3276",
"Name": "Name",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3276-name-white_1634146226.jpg"
},
{
"PLU": "4377",
"Name": "Nectarine",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4378-nectarines-1-1625081931_1633448513.jpg"
},
{
"PLU": "4665",
"Name": "Onions",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4093-onions-yellow-01-1625064621_1632941716.jpg"
},
{
"PLU": "4013",
"Name": "Oranges",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4012-navel-orange_1634141444.jpg"
},
{
"PLU": "4672",
"Name": "Parsnip",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4672-parsnip_1633958885.jpg"
},
{
"PLU": "3311",
"Name": "Passion Fruit",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3311-passion-fruit-02_1614719436.jpg"
},
{
"PLU": "4037",
"Name": "Peaches",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4403-peaches-1-1625664942_1633449692.jpg"
},
{
"PLU": "4931",
"Name": "Peanuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4931-peanuts_1625757234.JPG"
},
{
"PLU": "4025",
"Name": "Pears",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4025-anjou-pear_1629988897.PNG"
},
{
"PLU": "4673",
"Name": "Peas",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4673-peas-black-eyed-01_1625684339.jpg"
},
{
"PLU": "4936",
"Name": "Pecans",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4936-pecans-01_1625757366.JPG"
},
{
"PLU": "3459",
"Name": "Persimmon",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3459-shiny-red-persimmon_1486570958.JPG"
},
{
"PLU": "3039",
"Name": "Physalis/Cape Gooseberry/Ground Cherry",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3039-cape-gooseberry_1630596655.jpg"
},
{
"PLU": "4029",
"Name": "Pineapple",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4029-pineapple_1630597815.jpg"
},
{
"PLU": "4939",
"Name": "Pistachio",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4939-pistachios_1625757519.JPG"
},
{
"PLU": "3040",
"Name": "Pitahaya",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3040-pitaya-dragon-fruit-01_1614282658.jpg"
},
{
"PLU": "3611",
"Name": "Plumcot (Interspecific Plum)",
"URL": "https://server-ifps.accurateig.com/assets/commodities/interspecific-plum-black-picture_1629142350.JPG"
},
{
"PLU": "4434",
"Name": "Plums",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4435-plums-greengage-01-1625667846_1633453839.jpg"
},
{
"PLU": "4445",
"Name": "Pomegranate",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4445-pomegranates-07_1625668628.jpg"
},
{
"PLU": "3128",
"Name": "Potato",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3128-potatoes-purple-06_1614708225.jpg"
},
{
"PLU": "3631",
"Name": "Pumpkin",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3631-pink-pumpkin_1460472104.jpg"
},
{
"PLU": "3480",
"Name": "Pumpkin Vine",
"URL": "https://server-ifps.accurateig.com/assets/commodities/pumpkin-vine-3480_1625790222.jpg"
},
{
"PLU": "3478",
"Name": "Quelites",
"URL": "https://server-ifps.accurateig.com/assets/commodities/quelite-3478_1625790359.jpg"
},
{
"PLU": "4447",
"Name": "Quince",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4447-quince-1_1625668926.jpg"
},
{
"PLU": "3168",
"Name": "Radicchio",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3168-radicchio-castelfranco-02_1614718366.jpg"
},
{
"PLU": "4739",
"Name": "Radish",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4739-radish-black-03_1625751544.jpg"
},
{
"PLU": "4745",
"Name": "Rhubarb",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4745-rhubarb-01_1625751764.jpg"
},
{
"PLU": "4747",
"Name": "Rutabagas (Swede)",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4747-rutabaga_1635168852.jpg"
},
{
"PLU": "3137",
"Name": "Sapote",
"URL": "https://server-ifps.accurateig.com/assets/commodities/3137-sapote-white_1635264028.jpg"
},
{
"PLU": "4757",
"Name": "Squash",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4757-squash-banana_1625752222.JPG"
},
{
"PLU": "4790",
"Name": "Sugar Cane",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4790-sugar-cane_1630598686.jpg"
},
{
"PLU": "4791",
"Name": "Sunchokes (Jerusalem Artichokes)",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4791-sunchokes-03_1625753005.jpg"
},
{
"PLU": "4942",
"Name": "Sunflower Seeds",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4942-sunflower-seeds_1630599026.jpg"
},
{
"PLU": "4074",
"Name": "Sweet Potato/Yam/Kumara",
"URL": "https://server-ifps.accurateig.com/assets/commodities/sweet-potato-red-flesh-4074_1614281686.png"
},
{
"PLU": "4448",
"Name": "Tamarind",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4448-tamarindo-01_1625669100.jpg"
},
{
"PLU": "3428",
"Name": "Tangerines/Mandarins",
"URL": "https://server-ifps.accurateig.com/assets/commodities/murcott-mandarin-1629310772_1633456088.png"
},
{
"PLU": "4794",
"Name": "Taro Root (Dasheen)",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4795-taro-root-01-1625753179_1633374616.jpg"
},
{
"PLU": "4811",
"Name": "Turnip",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4811-turnip-purple-top_1635177577.jpg"
},
{
"PLU": "4943",
"Name": "Walnuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4943-walnuts_1625757652.JPG"
},
{
"PLU": "4814",
"Name": "Water Chestnuts",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4814-waterchestnuts-01_1625753939.jpg"
},
{
"PLU": "3481",
"Name": "Xpelon",
"URL": "https://server-ifps.accurateig.com/assets/commodities/expelon-3481_1625790150.jpg"
},
{
"PLU": "4819",
"Name": "Yuca Root/Cassava/Manioc",
"URL": "https://server-ifps.accurateig.com/assets/commodities/4819-yucca-root-03_1625754507.jpg"
}
]

8
MPStudyGO/go.mod Normal file
View File

@@ -0,0 +1,8 @@
module MPPOS
go 1.25.7
require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
)

4
MPStudyGO/go.sum Normal file
View File

@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

143
MPStudyGO/main.go Normal file
View File

@@ -0,0 +1,143 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
// Request structures based on MP documentation
type PrintRequest struct {
Type string `json:"type"`
ExternalReference string `json:"external_reference"`
Config PrintConfig `json:"config"`
Content string `json:"content"`
}
type PrintConfig struct {
Point PointSettings `json:"point"`
}
type PointSettings struct {
TerminalID string `json:"terminal_id"`
Subtype string `json:"subtype"`
}
func main() {
err := godotenv.Load("../.env")
if err != nil {
fmt.Println("Error loading .env file")
}
// Example receipt using supported tags: {b}, {center}, {br}, {s}
receiptContent := "{center}{b}SEKIPOS VENTA{/b}{br}" +
"--------------------------------{br}" +
"{left}Producto: Choripan Premium{br}" +
"{left}Total: $5.500{br}" +
"--------------------------------{br}" +
"{center}{s}Gracias por su compra{/s}{br}"
resp, err := SendPrintAction(receiptContent)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Response: %s\n", resp)
}
func SendPrintAction(content string) (string, error) {
apiURL := "https://api.mercadopago.com/terminals/v1/actions"
accessToken := os.Getenv("MP_ACCESS_TOKEN")
terminalID := os.Getenv("MP_TERMINAL_ID")
payload := PrintRequest{
Type: "print", // Required
ExternalReference: fmt.Sprintf("ref_%d", time.Now().Unix()),
Config: PrintConfig{
Point: PointSettings{
TerminalID: terminalID,
Subtype: "custom", // For text with tags
},
},
Content: content, // Must be 100-4096 chars
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
// Mandatory Unique UUID V4
req.Header.Set("X-Idempotency-Key", uuid.New().String())
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("MP Error %d: %s", resp.StatusCode, string(body))
}
return string(body), nil
}
func PartialRefund(orderID string, paymentID string, amount string) (string, error) {
// Endpoint para reembolsos según la referencia de API
apiURL := fmt.Sprintf("https://api.mercadopago.com/v1/orders/%s/refund", orderID)
token := os.Getenv("MP_ACCESS_TOKEN")
payload := map[string]interface{}{
"transactions": []map[string]string{
{
"id": paymentID,
"amount": amount, // Debe ser un string sin decimales
},
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Idempotency-Key", uuid.New().String())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
resBody, _ := io.ReadAll(resp.Body)
return string(resBody), nil
}
func GetOrderStatus(orderID string) (string, error) {
apiURL := fmt.Sprintf("https://api.mercadopago.com/v1/orders/%s", orderID)
token := os.Getenv("MP_ACCESS_TOKEN")
req, _ := http.NewRequest("GET", apiURL, nil)
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
resBody, _ := io.ReadAll(resp.Body)
return string(resBody), nil
}

View File

@@ -1,4 +1,4 @@
# SekiPOS v1.2 🍫🥤
# SekiPOS v1.6 🍫🥤
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.
@@ -8,6 +8,7 @@ A reactive POS inventory system for software engineers with a snack addiction. F
- **Local Cache:** Saves images locally to `static/cache` to avoid IP bans.
- **CLP Ready:** Chilean Peso formatting ($1.234) for local commerce.
- **Secure:** Hashed password authentication via Flask-Login.
- **On device scanner:** Add and scan products from within your phone!
## 🐳 Docker Deployment (Server)
@@ -23,6 +24,7 @@ docker run -d \
-v $(pwd)/sekipos/db:/app/db \
-v $(pwd)/sekipos/static/cache:/app/static/cache \
--name sekipos-server \
--restart unless-stopped \
sekipos:latest
```
@@ -38,6 +40,7 @@ services:
- YOUR_PATH/sekipos/static/cache:/app/static/cache
container_name: sekipos-server
image: sekipos:latest
restart: unless-stopped
```
## 🔌 Hardware Scanner Bridge (`ScannerGO`)
@@ -80,7 +83,8 @@ python app.py
- `db/pos_database.db`: SQLite storage.
## 📋 TODOs?
- Better admin registration(?)
- Some form of user registration(?)
- Major refactoring of the codebase
## 🥼 Food Datasets
- https://www.ifpsglobal.com/plu-codes-search

42
ScannerGO/build.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Define binary names
LINUX_BIN="ScannerGO-linux"
LINUX_ARM_BIN="ScannerGO-linuxARMv7"
WINDOWS_BIN="ScannerGO-windows.exe"
echo "Starting build process..."
# Build for Linux (64-bit)
echo "Building for Linux..."
GOOS=linux GOARCH=amd64 go build -o "$LINUX_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $LINUX_BIN"
else
echo "Failed to build Linux binary"
exit 1
fi
# Build for Windows (64-bit)
echo "Building for Windows..."
GOOS=windows GOARCH=amd64 go build -o "$WINDOWS_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $WINDOWS_BIN"
else
echo "Failed to build Windows binary"
exit 1
fi
# Build for Linux ARM (ARMv7)
echo "Building for Linux ARMv7..."
GOOS=linux GOARCH=arm GOARM=7 go build -o "$LINUX_ARM_BIN" main.go
if [ $? -eq 0 ]; then
echo "Successfully built: $LINUX_ARM_BIN"
else
echo "Failed to build Linux ARMv7 binary"
exit 1
fi
echo "Build complete."

View File

@@ -1,6 +1,9 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
@@ -13,45 +16,120 @@ import (
"github.com/tarm/serial"
)
type Config struct {
Port string `json:"port"`
URL string `json:"url"`
BaudRate int `json:"baud"`
Delimiter string `json:"delimiter"`
}
var defaultConfig = Config{
Port: "/dev/ttyACM0",
URL: "https://scanner.sekidesu.xyz/scan",
BaudRate: 115200,
Delimiter: "\n",
}
const configPath = "config.json"
func main() {
portName := flag.String("port", "/dev/ttyACM0", "Serial port name")
endpoint := flag.String("url", "https://scanner.sekidesu.xyz/scan", "Target URL endpoint")
baudRate := flag.Int("baud", 115200, "Baud rate")
cfg := loadConfig()
portName := flag.String("port", cfg.Port, "Serial port name")
endpoint := flag.String("url", cfg.URL, "Target URL endpoint")
baudRate := flag.Int("baud", cfg.BaudRate, "Baud rate")
delim := flag.String("delim", cfg.Delimiter, "Line delimiter")
save := flag.Bool("save", false, "Save current parameters to config.json")
flag.Parse()
config := &serial.Config{
Name: *portName,
Baud: *baudRate,
ReadTimeout: time.Second * 2,
cfg.Port = *portName
cfg.URL = *endpoint
cfg.BaudRate = *baudRate
cfg.Delimiter = *delim
if *save {
saveConfig(cfg)
fmt.Println("Settings saved to", configPath)
}
port, err := serial.OpenPort(config)
serialConfig := &serial.Config{
Name: cfg.Port,
Baud: cfg.BaudRate,
ReadTimeout: 0,
}
port, err := serial.OpenPort(serialConfig)
if err != nil {
fmt.Printf("Error opening port %s: %v\n", *portName, err)
fmt.Printf("Error opening port %s: %v\n", cfg.Port, err)
os.Exit(1)
}
defer port.Close()
fmt.Printf("Listening on %s (Baud: %d)...\n", *portName, *baudRate)
fmt.Printf("Sending data to: %s\n", *endpoint)
fmt.Printf("Listening on %s (Baud: %d, Delim: %q)...\n", cfg.Port, cfg.BaudRate, cfg.Delimiter)
buf := make([]byte, 128)
for {
n, err := port.Read(buf)
if err != nil {
if err != io.EOF {
fmt.Printf("Read error: %v\n", err)
}
continue
scanner := bufio.NewScanner(port)
// Custom split function to handle the configurable delimiter
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte(cfg.Delimiter)); i >= 0 {
return i + len(cfg.Delimiter), data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
})
if n > 0 {
content := strings.TrimSpace(string(buf[:n]))
if content != "" {
sendToEndpoint(*endpoint, content)
}
for scanner.Scan() {
content := strings.TrimSpace(scanner.Text())
if content != "" {
sendToEndpoint(cfg.URL, content)
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("Scanner error: %v\n", err)
}
}
func loadConfig() Config {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
saveConfig(defaultConfig)
return defaultConfig
}
file, err := os.Open(configPath)
if err != nil {
return defaultConfig
}
defer file.Close()
var cfg Config
decoder := json.NewDecoder(file)
if err := decoder.Decode(&cfg); err != nil {
return defaultConfig
}
// Handle case where JSON exists but field is missing
if cfg.Delimiter == "" {
cfg.Delimiter = "\n"
}
return cfg
}
func saveConfig(cfg Config) {
file, err := os.Create(configPath)
if err != nil {
fmt.Printf("Failed to create/save config: %v\n", err)
return
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
encoder.Encode(cfg)
}
func sendToEndpoint(baseURL, content string) {
@@ -60,7 +138,6 @@ func sendToEndpoint(baseURL, content string) {
}
fullURL := fmt.Sprintf("%s?content=%s", baseURL, url.QueryEscape(content))
resp, err := client.Get(fullURL)
if err != nil {
fmt.Printf("Network Error: %v\n", err)
@@ -68,5 +145,15 @@ func sendToEndpoint(baseURL, content string) {
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response: %v\n", err)
return
}
fmt.Printf("Data: [%s] | Status: %s\n", content, resp.Status)
if len(body) > 0 {
fmt.Printf("Response: %s\n", string(body))
}
fmt.Println(strings.Repeat("-", 30))
}

Binary file not shown.

View File

@@ -0,0 +1,36 @@
import serial
import requests
import time
import argparse
import sys
def run_bridge():
parser = argparse.ArgumentParser(description="Scanner Bridge for the technically impaired")
parser.add_argument('--port', default='COM5', help='Serial port (default: COM5)')
parser.add_argument('--baud', type=int, default=115200, help='Baud rate (default: 115200)')
parser.add_argument('--url', default='https://scanner.sekidesu.xyz/scan', help='Server URL')
args = parser.parse_args()
try:
ser = serial.Serial(args.port, args.baud, timeout=0.1)
print(f"Connected to {args.port} at {args.baud} bauds.")
while True:
if ser.in_waiting > 0:
barcode = ser.readline().decode('utf-8', errors='ignore').strip()
if barcode:
print(f"Scanned: {barcode}")
try:
resp = requests.get(args.url, params={'content': barcode}, timeout=5)
print(f"Server responded: {resp.status_code}")
except Exception as e:
print(f"Failed to send to server: {e}")
time.sleep(0.01)
except serial.SerialException as e:
print(f"Error opening {args.port}: {e}")
except KeyboardInterrupt:
print("\nBridge stopped. Finally.")
if __name__ == "__main__":
run_bridge()

5
SekiScaleESP/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
SekiScaleESP/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

View File

@@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
SekiScaleESP/lib/README Normal file
View File

@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

View File

@@ -0,0 +1,17 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:waveshare_rp2040_zero]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = waveshare_rp2040_zero
framework = arduino
monitor_speed = 115200
lib_archive = no
lib_deps = bogde/HX711@^0.7.5

224
SekiScaleESP/src/main.cpp Normal file
View File

@@ -0,0 +1,224 @@
#include <Arduino.h>
#define LCD_DATA 29
#define LCD_WR 28
#define LCD_CS 27
// Tracking variables
int currentAddr = 0;
int currentBit = 0;
void writeBits(uint32_t data, uint8_t count) {
for (int8_t i = count - 1; i >= 0; i--) {
digitalWrite(LCD_WR, LOW);
digitalWrite(LCD_DATA, (data >> i) & 0x01);
digitalWrite(LCD_WR, HIGH);
}
}
void sendCmd(uint8_t cmd) {
digitalWrite(LCD_CS, LOW);
writeBits(0x04, 3); // Binary 100 [cite: 432]
writeBits(cmd, 8); // Command [cite: 413, 534]
writeBits(0, 1); // X bit [cite: 416]
digitalWrite(LCD_CS, HIGH);
}
void writeAddr(uint8_t addr, uint8_t data) {
digitalWrite(LCD_CS, LOW);
uint16_t header = (0x05 << 6) | (addr & 0x3F); // Mode 101 [cite: 475]
writeBits(header, 9);
writeBits(data & 0x0F, 4); // 4 bits of data [cite: 475]
digitalWrite(LCD_CS, HIGH);
}
void updateDisplay() {
// Clear all segments first
for (int i = 0; i < 32; i++) {
writeAddr(i, 0x00);
}
// Light up only the current bit
writeAddr(currentAddr, (1 << currentBit));
Serial.print(">>> CURRENT - Address: ");
Serial.print(currentAddr);
Serial.print(" | Bit (COM): ");
Serial.println(currentBit);
Serial.println("Enter 'n' for Next, 'p' for Prev:");
}
void setup() {
pinMode(LCD_DATA, OUTPUT);
pinMode(LCD_WR, OUTPUT);
pinMode(LCD_CS, OUTPUT);
digitalWrite(LCD_CS, HIGH); // Initialize serial interface [cite: 443]
Serial.begin(9600);
// while (!Serial);
Serial.println("--- TM1621 Interactive Mapper ---");
sendCmd(0x01); // SYS EN [cite: 534]
sendCmd(0x29); // BIAS 1/3, 4 COM [cite: 420, 544]
sendCmd(0x03); // LCD ON [cite: 534]
updateDisplay();
}
const uint8_t digitMap[] = {
0b11111100, // 0
0b00000110, // 1
0b01011011, // 2
0b01001111, // 3
0b01100110, // 4
0b01101101, // 5
0b01111101, // 6
0b00000111, // 7
0b01111111, // 8
0b01101111 // 9
};
const int arrows[] = {283,363,183,203}; //tare,?,?,Zero
const int battery[] = {63,83,103}; //LOW / MED / FULL
// Following your pattern: {A, B, C, D, E, F, G}
const int row1_d1[] = {300, 301, 302, 313, 312, 310, 311}; // Addresses 30 & 31
const int row1_d2[] = {280, 281, 282, 293, 292, 290, 291}; // Addresses 28 & 29
const int row1_d3[] = {260, 261, 262, 273, 272, 270, 271}; // Addresses 26 & 27
const int row1_d4[] = {240, 241, 242, 253, 252, 250, 251}; // Addresses 24 & 25
const int row1_d5[] = {220, 221, 222, 233, 232, 230, 231}; // Addresses 22 & 23
const int row1_decimal[] = {263,232,223}; //XX.X.X.X
const int row2_d1[] = {200, 201, 202, 213, 212, 210, 211}; // Addresses 20 & 21
const int row2_d2[] = {180, 181, 182, 193, 192, 190, 191}; // Addresses 18 & 19
const int row2_d3[] = {160, 161, 162, 173, 172, 170, 171}; // Addresses 16 & 17
const int row2_d4[] = {140, 141, 142, 153, 152, 150, 151}; // Addresses 14 & 15
const int row2_d5[] = {120, 121, 122, 133, 132, 130, 131}; // Addresses 12 & 13
const int row2_decimal[] = {163,143,123}; //XX.X.X.X
const int row3_d1[] = {100, 101, 102, 113, 112, 110, 111}; // Addresses 10 & 11
const int row3_d2[] = {80, 81, 82, 93, 92, 90, 91}; // Addresses 8 & 9
const int row3_d3[] = {60, 61, 62, 73, 72, 70, 71}; // Addresses 6 & 7
const int row3_d4[] = {40, 41, 42, 53, 52, 50, 51}; // Addresses 4 & 5
const int row3_d5[] = {20, 21, 22, 33, 32, 30, 31}; // Addresses 2 & 3
const int row3_d6[] = {0, 1, 2, 13, 12, 10, 11}; // Addresses 0 & 1
const int row3_decimal[] = {43,23,3}; //XX.X.X.X
const int* digitsRow1[] = {row1_d1, row1_d2, row1_d3, row1_d4, row1_d5};
const int* digitsRow2[] = {row2_d1, row2_d2, row2_d3, row2_d4, row2_d5};
const int* digitsRow3[] = {row3_d1, row3_d2, row3_d3, row3_d4, row3_d5, row3_d6};
const int* decimals[] = {row1_decimal, row2_decimal, row3_decimal};
uint8_t shadowRAM[32] = {0};
void writeMappedSegment(int aab, bool state) {
int addr = aab / 10;
int bit = aab % 10;
if (state) shadowRAM[addr] |= (1 << bit);
else shadowRAM[addr] &= ~(1 << bit);
writeAddr(addr, shadowRAM[addr]);
}
void displayDigit(const int segments[], int number) {
uint8_t bits = digitMap[number % 10];
for (int i = 0; i < 7; i++) {
// We check Bit 0, then Bit 1, etc.
// This maps i=0 to Segment A, i=1 to Segment B...
bool state = (bits >> i) & 0x01;
writeMappedSegment(segments[i], state);
}
}
void printToRow(int row, long value, int decimalPos = 0) {
const int** currentRow;
int numDigits;
// Select row configuration
switch(row) {
case 1: currentRow = digitsRow1; numDigits = 5; break;
case 2: currentRow = digitsRow2; numDigits = 5; break;
case 3: currentRow = digitsRow3; numDigits = 6; break;
default: return;
}
// Display the number right-aligned
long tempValue = value;
for (int i = numDigits - 1; i >= 0; i--) {
if (tempValue > 0 || i == numDigits - 1) { // Show at least one digit
displayDigit(currentRow[i], tempValue % 10);
tempValue /= 10;
} else {
// Clear leading zeros (all segments off)
for (int s = 0; s < 7; s++) writeMappedSegment(currentRow[i][s], false);
}
}
// Handle Decimal Points (if applicable for that row)
if (decimalPos > 0 && decimalPos <= 3) {
writeMappedSegment(decimals[row-1][decimalPos-1], true);
}
}
int counter1 = 0;
int counter2 = 0;
int counter3 = 0;
void loop() {
printToRow(1, counter1);
printToRow(2, counter2);
printToRow(3, counter3);
counter1 = (counter1 + 1) % 1000;
counter2 = (counter2 + 2) % 1000;
counter3 = (counter3 + 3) % 1000;
delay(100);
}
// void loop() {
// writeAddr(0, 0x00);
// writeAddr(1, 0x00);
// writeAddr(2, 0x00);
// writeAddr(3, 0x00);
// displayDigit(row1_d1, counter);
// Serial.println(counter);
// counter++;
// if (counter > 9) {
// counter = 0;
// }
// delay(1000);
// }
// void loop() {
// if (Serial.available() > 0) {
// char input = Serial.read();
// // Ignore newline/carriage return characters
// if (input == '\n' || input == '\r') return;
// if (input == 'n' || input == 'N') {
// currentBit++;
// if (currentBit > 3) {
// currentBit = 0;
// currentAddr++;
// }
// if (currentAddr > 31) currentAddr = 0;
// updateDisplay();
// }
// else if (input == 'p' || input == 'P') {
// currentBit--;
// if (currentBit < 0) {
// currentBit = 3;
// currentAddr--;
// }
// if (currentAddr < 0) currentAddr = 31;
// updateDisplay();
// }
// }
// }

11
SekiScaleESP/test/README Normal file
View File

@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html

518
app.py
View File

@@ -1,11 +1,24 @@
import os
import sqlite3
import requests
from flask import send_file
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, send_from_directory
from flask_socketio import SocketIO, emit
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
import mimetypes
import time
import uuid
from datetime import datetime
import zipfile
import io
# from dotenv import load_dotenv
# load_dotenv()
# MP_ACCESS_TOKEN = os.getenv('MP_ACCESS_TOKEN')
# MP_TERMINAL_ID = os.getenv('MP_TERMINAL_ID')
app = Flask(__name__)
app.config['SECRET_KEY'] = 'seki_super_secret_key_99' # Change this if you have actual friends
@@ -30,10 +43,41 @@ def init_db():
with sqlite3.connect(DB_FILE) as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS users
(id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)''')
# Updated table definition
conn.execute('''CREATE TABLE IF NOT EXISTS products
(barcode TEXT PRIMARY KEY, name TEXT, price REAL, image_url TEXT)''')
(barcode TEXT PRIMARY KEY,
name TEXT,
price REAL,
image_url TEXT,
stock REAL DEFAULT 0,
unit_type TEXT DEFAULT 'unit')''')
# Default user: admin / Pass: choripan1234
# Add these two tables for sales history
conn.execute('''CREATE TABLE IF NOT EXISTS sales
(id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT DEFAULT CURRENT_TIMESTAMP,
total REAL,
payment_method TEXT)''')
conn.execute('''CREATE TABLE IF NOT EXISTS sale_items
(id INTEGER PRIMARY KEY AUTOINCREMENT,
sale_id INTEGER,
barcode TEXT,
name TEXT,
price REAL,
quantity REAL,
subtotal REAL,
FOREIGN KEY(sale_id) REFERENCES sales(id))''')
conn.execute('''CREATE TABLE IF NOT EXISTS dicom
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
amount REAL DEFAULT 0,
notes TEXT,
image_url TEXT,
last_updated TEXT DEFAULT CURRENT_TIMESTAMP)''')
# Default user logic remains same...
user = conn.execute('SELECT * FROM users WHERE username = ?', ('admin',)).fetchone()
if not user:
hashed_pw = generate_password_hash('choripan1234')
@@ -74,11 +118,6 @@ def download_image(url, barcode):
return url
def fetch_from_openfoodfacts(barcode):
# Check if we already have a cached image even if API fails
local_filename = f"{barcode}.jpg"
local_path = os.path.join(CACHE_DIR, local_filename)
cached_url = f"/static/cache/{local_filename}" if os.path.exists(local_path) else None
url = f"https://world.openfoodfacts.org/api/v2/product/{barcode}.json"
try:
headers = {'User-Agent': 'SekiPOS/1.0'}
@@ -89,16 +128,14 @@ def fetch_from_openfoodfacts(barcode):
name = p.get('product_name_es') or p.get('product_name') or p.get('brands', 'Unknown')
imgs = p.get('selected_images', {}).get('front', {}).get('display', {})
img_url = imgs.get('es') or imgs.get('en') or p.get('image_url', '')
local_img = download_image(img_url, barcode)
return {"name": name, "image": local_img}
if img_url:
local_img = download_image(img_url, barcode)
return {"name": name, "image": local_img}
return {"name": name, "image": None}
except Exception as e:
print(f"API Error: {e}")
# If API fails but we have a cache, return the cache with a generic name
if cached_url:
return {"name": "Producto Cacheado", "image": cached_url}
return None
# --- ROUTES ---
@@ -111,23 +148,70 @@ def login():
user = conn.execute('SELECT * FROM users WHERE username = ?', (user_in,)).fetchone()
if user and check_password_hash(user[2], pass_in):
login_user(User(user[0], user[1]))
return redirect(url_for('index'))
return redirect(url_for('inventory'))
flash('Invalid credentials.')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user(); return redirect(url_for('login'))
logout_user()
return redirect(url_for('login'))
@app.route('/')
@login_required
def index():
def defaultRoute():
return redirect(url_for('inventory'))
@app.route('/inventory')
@login_required
def inventory():
with sqlite3.connect(DB_FILE) as conn:
products = conn.execute('SELECT * FROM products').fetchall()
return render_template('index.html', products=products, user=current_user)
return render_template('inventory.html', active_page='inventory', products=products, user=current_user)
@app.route('/upsert', methods=['POST'])
@app.route("/checkout")
@login_required
def checkout():
with sqlite3.connect(DB_FILE) as conn:
# Fetching the same columns the scanner expects
products = conn.execute('SELECT barcode, name, price, image_url, stock, unit_type FROM products').fetchall()
return render_template("checkout.html", active_page='checkout', user=current_user, products=products)
@app.route('/dicom')
@login_required
def dicom():
with sqlite3.connect(DB_FILE) as conn:
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')
@login_required
def sales():
selected_date = request.args.get('date')
with sqlite3.connect(DB_FILE) as conn:
cur = conn.cursor()
# Determine the target date for the "Daily" stat
target_date = selected_date if selected_date else cur.execute("SELECT date('now', 'localtime')").fetchone()[0]
stats = {
"daily": cur.execute("SELECT SUM(total) FROM sales WHERE date(date, 'localtime') = ?", (target_date,)).fetchone()[0] or 0,
"week": cur.execute("SELECT SUM(total) FROM sales WHERE date(date, 'localtime') >= date('now', 'localtime', '-7 days')").fetchone()[0] or 0,
"month": cur.execute("SELECT SUM(total) FROM sales WHERE strftime('%Y-%m', date, 'localtime') = strftime('%Y-%m', 'now', 'localtime')").fetchone()[0] or 0
}
if selected_date:
sales_data = cur.execute('''SELECT id, date, total, payment_method FROM sales
WHERE date(date, 'localtime') = ?
ORDER BY date DESC''', (selected_date,)).fetchall()
else:
sales_data = cur.execute('SELECT id, date, total, payment_method FROM sales ORDER BY date DESC LIMIT 100').fetchall()
return render_template('sales.html', active_page='sales', user=current_user, sales=sales_data, stats=stats, selected_date=selected_date)
@app.route("/upsert", methods=["POST"])
@login_required
def upsert():
d = request.form
@@ -135,18 +219,27 @@ def upsert():
try:
price = float(d['price'])
stock = float(d.get('stock', 0)) # New field
except (ValueError, TypeError):
price = 0.0
stock = 0.0
unit_type = d.get('unit_type', 'unit') # New field (unit or kg)
final_image_path = download_image(d['image_url'], barcode)
with sqlite3.connect(DB_FILE) as conn:
conn.execute('''INSERT INTO products (barcode, name, price, image_url) VALUES (?,?,?,?)
ON CONFLICT(barcode) DO UPDATE SET name=excluded.name,
price=excluded.price, image_url=excluded.image_url''',
(barcode, d['name'], price, final_image_path))
# Updated UPSERT query
conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type)
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, d['name'], price, final_image_path, stock, unit_type))
conn.commit()
return redirect(url_for('index'))
return redirect(url_for('inventory'))
@app.route('/delete/<barcode>', methods=['POST'])
@login_required
@@ -158,37 +251,390 @@ def delete(barcode):
img_p = os.path.join(CACHE_DIR, f"{barcode}.jpg")
if os.path.exists(img_p): os.remove(img_p)
socketio.emit('product_deleted', {"barcode": barcode})
return redirect(url_for('index'))
return redirect(url_for('inventory'))
@app.route('/scan', methods=['GET'])
def scan():
barcode = request.args.get('content', '').replace('{content}', '')
if not barcode: return jsonify({"err": "empty"}), 400
if not barcode:
return jsonify({"status": "error", "message": "empty barcode"}), 400
with sqlite3.connect(DB_FILE) as conn:
p = conn.execute('SELECT * FROM products WHERE barcode = ?', (barcode,)).fetchone()
# Fixed: Selecting all 6 necessary columns
p = conn.execute('SELECT barcode, name, price, image_url, stock, unit_type FROM products WHERE barcode = ?', (barcode,)).fetchone()
if p:
socketio.emit('new_scan', {"barcode": p[0], "name": p[1], "price": int(p[2]), "image": p[3]})
return jsonify({"status": "ok"})
# Now matches the 6 columns in the SELECT statement
barcode_val, name, price, image_path, stock, unit_type = p
# Not in DB, try external API or Local Cache
if image_path and image_path.startswith('/static/'):
clean_path = image_path.split('?')[0].lstrip('/')
if not os.path.exists(clean_path):
ext_data = fetch_from_openfoodfacts(barcode_val)
if ext_data and ext_data.get('image'):
image_path = ext_data['image']
with sqlite3.connect(DB_FILE) as conn:
conn.execute('UPDATE products SET image_url = ? WHERE barcode = ?', (image_path, barcode_val))
conn.commit()
product_data = {
"barcode": barcode_val,
"name": name,
"price": int(price),
"image": image_path,
"stock": stock,
"unit_type": unit_type
}
socketio.emit('new_scan', product_data)
return jsonify({"status": "ok", "data": product_data}), 200
# 2. Product not in DB, try external API
ext = fetch_from_openfoodfacts(barcode)
if ext:
socketio.emit('scan_error', {
# We found it externally, but it's still a 404 relative to our local DB
external_data = {
"barcode": barcode,
"name": ext['name'],
"image": ext['image']
})
else:
socketio.emit('scan_error', {"barcode": barcode})
"image": ext['image'],
"source": "openfoodfacts"
}
socketio.emit('scan_error', external_data)
return jsonify({"status": "not_found", "data": external_data}), 404
return jsonify({"status": "not_found"}), 404
# 3. Truly not found anywhere
socketio.emit('scan_error', {"barcode": barcode})
return jsonify({"status": "not_found", "data": {"barcode": barcode}}), 404
@app.route('/static/cache/<path:filename>')
def serve_cache(filename):
return send_from_directory(CACHE_DIR, filename)
@app.route('/bulk_price_update', methods=['POST'])
@login_required
def bulk_price_update():
data = request.get_json()
barcodes = data.get('barcodes', [])
new_price = data.get('new_price')
if not barcodes or new_price is None:
return jsonify({"error": "Missing data"}), 400
try:
with sqlite3.connect(DB_FILE) as conn:
# Use executemany for efficiency
params = [(float(new_price), b) for b in barcodes]
conn.executemany('UPDATE products SET price = ? WHERE barcode = ?', params)
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Bulk update failed: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/bulk_delete', methods=['POST'])
@login_required
def bulk_delete():
data = request.get_json()
barcodes = data.get('barcodes', [])
if not barcodes:
return jsonify({"error": "No barcodes provided"}), 400
try:
with sqlite3.connect(DB_FILE) as conn:
# Delete records from DB
conn.execute(f'DELETE FROM products WHERE barcode IN ({",".join(["?"]*len(barcodes))})', barcodes)
conn.commit()
# Clean up cache for each deleted product
for barcode in barcodes:
# This is a bit naive as it only checks .jpg, but matches your existing delete logic
img_p = os.path.join(CACHE_DIR, f"{barcode}.jpg")
if os.path.exists(img_p):
os.remove(img_p)
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Bulk delete failed: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/upload_image', methods=['POST'])
@login_required
def upload_image():
if 'image' not in request.files or 'barcode' not in request.form:
return jsonify({"error": "Missing data"}), 400
file = request.files['image']
barcode = request.form['barcode']
if file.filename == '' or not barcode:
return jsonify({"error": "Invalid data"}), 400
filename = f"{barcode}.jpg"
filepath = os.path.join(CACHE_DIR, filename)
file.save(filepath)
timestamp = int(time.time())
return jsonify({"status": "success", "image_url": f"/static/cache/{filename}?t={timestamp}"}), 200
@app.route('/api/scale/weight', methods=['POST'])
def update_scale_weight():
data = request.get_json()
# Assuming the scale sends {"weight": 1250} (in grams)
weight_grams = data.get('weight', 0)
# Optional: Convert to kg if you prefer
weight_kg = round(weight_grams / 1000, 3)
# Broadcast to all connected clients via SocketIO
socketio.emit('scale_update', {
"grams": weight_grams,
"kilograms": weight_kg,
"timestamp": time.time()
})
return jsonify({"status": "received"}), 200
@app.route('/api/checkout', methods=['POST'])
@login_required
def process_checkout():
try:
data = request.get_json()
cart = data.get('cart', [])
payment_method = data.get('payment_method', 'efectivo')
if not cart:
return jsonify({"error": "Cart is empty"}), 400
# Recalculate total on the server because the frontend is a liar
total = sum(item.get('subtotal', 0) for item in cart)
with sqlite3.connect(DB_FILE) as conn:
cur = conn.cursor()
# Let SQLite handle the exact UTC timestamp internally
cur.execute('INSERT INTO sales (date, total, payment_method) VALUES (CURRENT_TIMESTAMP, ?, ?)', (total, payment_method))
sale_id = cur.lastrowid
# Record each item and deduct stock
for item in cart:
cur.execute('''INSERT INTO sale_items (sale_id, barcode, name, price, quantity, subtotal)
VALUES (?, ?, ?, ?, ?, ?)''',
(sale_id, item['barcode'], item['name'], item['price'], item['qty'], item['subtotal']))
# Deduct from inventory (Manual products will safely be ignored here)
cur.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?', (item['qty'], item['barcode']))
conn.commit()
return jsonify({"status": "success", "sale_id": sale_id}), 200
except Exception as e:
print(f"Checkout Error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/sale/<int:sale_id>')
@login_required
def get_sale_details(sale_id):
with sqlite3.connect(DB_FILE) as conn:
items = conn.execute('SELECT barcode, name, price, quantity, subtotal FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
# Format it as a neat list of dictionaries for JavaScript to digest
item_list = [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items]
return jsonify(item_list), 200
@app.route('/api/sale/<int:sale_id>', methods=['DELETE'])
@login_required
def reverse_sale(sale_id):
try:
with sqlite3.connect(DB_FILE) as conn:
cur = conn.cursor()
# 1. Get the items and quantities from the receipt
items = cur.execute('SELECT barcode, quantity FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
# 2. Add the stock back to the inventory
for barcode, qty in items:
# This safely ignores manual products since their fake barcodes won't match any row
cur.execute('UPDATE products SET stock = stock + ? WHERE barcode = ?', (qty, barcode))
# 3. Destroy the evidence
cur.execute('DELETE FROM sale_items WHERE sale_id = ?', (sale_id,))
cur.execute('DELETE FROM sales WHERE id = ?', (sale_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Reverse Sale Error: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/dicom/update', methods=['POST'])
@login_required
def update_dicom():
data = request.get_json()
name = data.get('name', '').strip()
amount = float(data.get('amount', 0))
notes = data.get('notes', '')
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 action == 'add':
amount = -amount
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,
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
def delete_dicom(debtor_id):
try:
with sqlite3.connect(DB_FILE) as conn:
conn.execute('DELETE FROM dicom WHERE id = ?', (debtor_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/settings/update', methods=['POST'])
@login_required
def update_settings():
new_password = request.form.get('password')
profile_pic = request.form.get('profile_pic')
with sqlite3.connect(DB_FILE) as conn:
if new_password and len(new_password) > 0:
hashed_pw = generate_password_hash(new_password)
conn.execute('UPDATE users SET password = ? WHERE id = ?', (hashed_pw, current_user.id))
if profile_pic:
conn.execute('UPDATE users SET profile_pic = ? WHERE id = ?', (profile_pic, current_user.id))
conn.commit()
flash('Configuración actualizada')
return redirect(request.referrer)
@app.route('/export/db')
@login_required
def export_db():
if os.path.exists(DB_FILE):
return send_file(DB_FILE, as_attachment=True, download_name=f"SekiPOS_Backup_{datetime.now().strftime('%Y%m%d')}.db", mimetype='application/x-sqlite3')
return "Error: Database file not found", 404
@app.route('/export/images')
@login_required
def export_images():
if not os.path.exists(CACHE_DIR) or not os.listdir(CACHE_DIR):
return "No images found to export", 404
# Create an in-memory byte stream to hold the zip data
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(CACHE_DIR):
for file in files:
file_path = os.path.join(root, file)
# Store files using their names only to avoid nesting inside the zip
zf.write(file_path, arcname=file)
memory_file.seek(0)
return send_file(
memory_file,
mimetype='application/zip',
as_attachment=True,
download_name=f"SekiPOS_Images_{datetime.now().strftime('%Y%m%d')}.zip"
)
# @app.route('/process_payment', methods=['POST'])
# @login_required
# def process_payment():
# data = request.get_json()
# total_amount = data.get('total')
# if not total_amount or total_amount <= 0:
# return jsonify({"error": "Invalid amount"}), 400
# url = "https://api.mercadopago.com/v1/orders"
# headers = {
# "Authorization": f"Bearer {MP_ACCESS_TOKEN}",
# "Content-Type": "application/json",
# "X-Idempotency-Key": str(uuid.uuid4())
# }
# # MP Point API often prefers integer strings for CLP or exact strings
# # We use int() here if you are dealing with CLP (no cents)
# formatted_amount = str(int(float(total_amount)))
# payload = {
# "type": "point",
# "external_reference": f"ref_{int(time.time())}",
# "description": "Venta SekiPOS",
# "expiration_time": "PT16M",
# "transactions": {
# "payments": [
# {
# "amount": formatted_amount
# }
# ]
# },
# "config": {
# "point": {
# "terminal_id": MP_TERMINAL_ID,
# "print_on_terminal": "no_ticket"
# },
# "payment_method": {
# "default_type": "credit_card"
# }
# },
# "integration_data": {
# "platform_id": "dev_1234567890",
# "integrator_id": "dev_1234567890"
# },
# "taxes": [
# {
# "payer_condition": "payment_taxable_iva"
# }
# ]
# }
# try:
# # Verify the payload in your terminal if it fails again
# response = requests.post(url, json=payload, headers=headers)
# if response.status_code != 201 and response.status_code != 200:
# print(f"DEBUG MP ERROR: {response.text}")
# return jsonify(response.json()), response.status_code
# except Exception as e:
# return jsonify({"error": str(e)}), 500
# @app.route('/api/mp-webhook', methods=['POST'])
# def webhook_notify():
# data = request.get_json()
# action = data.get('action', 'unknown')
# # Emitimos a todos los clientes conectados
# socketio.emit('payment_update', {
# "status": action,
# "id": data.get('data', {}).get('id')
# })
# return jsonify({"status": "ok"}), 200
if __name__ == '__main__':
init_db()
socketio.run(app, host='0.0.0.0', port=5000, debug=True)

162
frutas2.json Normal file
View File

@@ -0,0 +1,162 @@
[
{
"name": "Paltas",
"plu": "3509",
"image": "https://server-ifps.accurateig.com/assets/commodities/3509-gem-avocado_1625011346.JPG"
},
{
"name": "Papas",
"plu": "3414",
"image": "https://server-ifps.accurateig.com/assets/commodities/3414-baking-potato-white_1635179333.jpg"
},
{
"name": "Limones",
"plu": "3626",
"image": "https://server-ifps.accurateig.com/assets/commodities/3626-meyer-lemons_1460404763.jpg"
},
{
"name": "Cebollines / Cebollas",
"plu": "4068",
"image": "https://server-ifps.accurateig.com/assets/commodities/4068-onions-green-2_1625063600.jpg"
},
{
"name": "Ajo",
"plu": "4608",
"image": "https://server-ifps.accurateig.com/assets/commodities/4608-garlic-regular_1637184640.jpg"
},
{
"name": "Zanahorias",
"plu": "4560",
"image": "https://server-ifps.accurateig.com/assets/commodities/4560-carrots-baby_1625673556.jpg"
},
{
"name": "Plátanos",
"plu": "4235",
"image": "https://server-ifps.accurateig.com/assets/commodities/4235-plantains-01_1625076376.jpg"
},
{
"name": "Manzanas",
"plu": "4099",
"image": "https://server-ifps.accurateig.com/assets/commodities/apples-akane_1629314651.png"
},
{
"name": "Naranjas",
"plu": "4381",
"image": "https://server-ifps.accurateig.com/assets/commodities/4381-oranges-blood-01_1625082045.jpg"
},
{
"name": "Choclo",
"plu": "3087",
"image": "https://server-ifps.accurateig.com/assets/commodities/3087-corn-04_1614633780.jpg"
},
{
"name": "Zapallo",
"plu": "4734",
"image": "https://server-ifps.accurateig.com/assets/commodities/4734-mini-pumpkin_1633964765.jpg"
},
{
"name": "Zapallo italiano",
"plu": "4750",
"image": "https://server-ifps.accurateig.com/assets/commodities/4750-squash-acorn-01_1625751871.jpg"
},
{
"name": "Repollo",
"plu": "4069",
"image": "https://server-ifps.accurateig.com/assets/commodities/4069-green-cabbage_1633958066.jpg"
},
{
"name": "Betarragas",
"plu": "4537",
"image": "https://server-ifps.accurateig.com/assets/commodities/4537-baby-golden-beets_1635173500.jpg"
},
{
"name": "Porotos verdes",
"plu": "4527",
"image": "https://server-ifps.accurateig.com/assets/commodities/4527-beans-chinese-long-07_1625671743.jpg"
},
{
"name": "Mandarinas",
"plu": "3524",
"image": "https://server-ifps.accurateig.com/assets/commodities/noble-juicycrunch-productisolated-900x900-rgb-copy_1627662136.png"
},
{
"name": "Uvas",
"plu": "3491",
"image": "https://server-ifps.accurateig.com/assets/commodities/arra-15-grape_1518122851.JPG"
},
{
"name": "Duraznos",
"plu": "3113",
"image": "https://server-ifps.accurateig.com/assets/commodities/3113-peaches-donut-04_1614707155.jpg"
},
{
"name": "Cerezas",
"plu": "3549",
"image": "https://server-ifps.accurateig.com/assets/commodities/screenshot-2023-05-02-100428_1683036305.png"
},
{
"name": "Peras",
"plu": "3317",
"image": "https://server-ifps.accurateig.com/assets/commodities/3317-angelys-pear_1460402454.jpg"
},
{
"name": "Ciruelas",
"plu": "4435",
"image": "https://server-ifps.accurateig.com/assets/commodities/4435-plums-greengage-01_1625667846.jpg"
},
{
"name": "Kiwi",
"plu": "3279",
"image": "https://server-ifps.accurateig.com/assets/commodities/3279-kiwi-gold-03_1614718637.jpg"
},
{
"name": "Piña",
"plu": "4430",
"image": "https://server-ifps.accurateig.com/assets/commodities/4430-pineapple-05_1625667649.jpg"
},
{
"name": "Puerros",
"plu": "4629",
"image": "https://server-ifps.accurateig.com/assets/commodities/4629-leeks-02_1625680225.jpg"
},
{
"name": "Alcachofas",
"plu": "4519",
"image": "https://server-ifps.accurateig.com/assets/commodities/4519-artichokes-baby-02_1625671366.jpg"
},
{
"name": "Espárragos",
"plu": "4521",
"image": "https://server-ifps.accurateig.com/assets/commodities/4521-asparagus-01_1625671446.jpg"
},
{
"name": "Berenjena",
"plu": "4599",
"image": "https://server-ifps.accurateig.com/assets/commodities/4599-baby-eggplant-aubergine_1633372314.jpg"
},
{
"name": "Durazno Nectarin",
"plu": "3437",
"image": "https://server-ifps.accurateig.com/assets/commodities/yellow-fleshed-flat-nectarine_1629140965.jpg"
},
{
"name": "Damascos",
"plu": "3044",
"image": "https://server-ifps.accurateig.com/assets/commodities/3044-apricots-black-velvet-03_1614619576.jpg"
},
{
"name": "Membrillo",
"plu": "4447",
"image": "https://server-ifps.accurateig.com/assets/commodities/4447-quince-1_1625668926.jpg"
},
{
"name": "Champiñones",
"plu": "4647",
"image": "https://server-ifps.accurateig.com/assets/commodities/4647-mushrooms-chanterelles-1_1625681503.jpg"
},
{
"name": "Castañas",
"plu": "4927",
"image": "https://server-ifps.accurateig.com/assets/commodities/4927-chestnuts-italian-02_1625756943.jpg"
}
]

BIN
image_cache/3044.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
image_cache/3087.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
image_cache/3113.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

BIN
image_cache/3279.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
image_cache/3317.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
image_cache/3414.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
image_cache/3437.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
image_cache/3491.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
image_cache/3509.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
image_cache/3524.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
image_cache/3549.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
image_cache/3626.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
image_cache/4068.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

BIN
image_cache/4069.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Some files were not shown because too many files have changed in this diff Show More