Fechamento de Caixa - Tauri 2 initial commit
- Storage: SQLite local via rusqlite - Supabase: REST API via reqwest - Printer: USB ESC/POS for TM-T20 - Frontend: vanilla HTML (form-v9.3 ported) - Build: GitHub Actions for Linux/Windows/macOS
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
//! Plugin de impressão ESC/POS via USB para TM-T20 e compatíveis
|
||||
//! Substitui window.print() — impressão direta sem popup do SO.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use tauri::State;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PrinterError {
|
||||
#[error("Impressora não encontrada. Verifique se está conectada via USB e ligada.")]
|
||||
NotFound,
|
||||
#[error("Falha ao abrir impressora: {0}")]
|
||||
OpenFailed(String),
|
||||
#[error("Falha ao enviar dados: {0}")]
|
||||
WriteFailed(String),
|
||||
#[error("Configuração inválida: {0}")]
|
||||
ConfigError(String),
|
||||
}
|
||||
|
||||
impl serde::Serialize for PrinterError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Comandos ESC/POS via plugins::escpos ────────────────────────────────────
|
||||
use crate::plugins::escpos;
|
||||
|
||||
// ── Struct de dados do recibo ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ReciboData {
|
||||
pub loja: String,
|
||||
pub data: String,
|
||||
pub operador: String,
|
||||
pub turno: String,
|
||||
pub saldo_troco: f64,
|
||||
pub fechamento: f64,
|
||||
pub saldo_esperado: f64,
|
||||
pub diferenca: f64,
|
||||
pub sangrias: f64,
|
||||
pub despesas: f64,
|
||||
pub vales: f64,
|
||||
pub areceber: f64,
|
||||
pub credito: f64,
|
||||
pub debito: f64,
|
||||
pub alimentacao: f64,
|
||||
pub voucher: f64,
|
||||
pub cancelamentos: f64,
|
||||
}
|
||||
|
||||
impl ReciboData {
|
||||
pub fn fmt_money(&self, v: f64) -> String {
|
||||
format!("R$ {:.2}", v).replace('.', ",")
|
||||
}
|
||||
|
||||
/// Gera todos os bytes ESC/POS do recibo 80mm
|
||||
pub fn to_escpos(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Inicializa
|
||||
out.extend(escpos::INIT);
|
||||
|
||||
// Cabeçalho
|
||||
out.extend(escpos::blank_lines(1));
|
||||
out.extend(escpos::title(&self.loja));
|
||||
out.extend(escpos::line_center("Fechamento de Caixa"));
|
||||
out.extend(escpos::blank_lines(1));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Info básica
|
||||
out.extend(escpos::kv("Data:", &self.data));
|
||||
out.extend(escpos::kv("Operador:", &self.operador));
|
||||
out.extend(escpos::kv("Turno:", &self.turno));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Valores de entrada
|
||||
out.extend(escpos::line_bold("ENTRADAS"));
|
||||
out.extend(escpos::kv("Saldo Troco:", &self.fmt_money(self.saldo_troco)));
|
||||
out.extend(escpos::kv("Fechamento:", &self.fmt_money(self.fechamento)));
|
||||
out.extend(escpos::kv("Saldo Esperado:", &self.fmt_money(self.saldo_esperado)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Sangrias / Despesas
|
||||
out.extend(escpos::line_bold("DÉBITOS"));
|
||||
out.extend(escpos::kv("Sangrias:", &self.fmt_money(self.sangrias)));
|
||||
out.extend(escpos::kv("Despesas:", &self.fmt_money(self.despesas)));
|
||||
out.extend(escpos::kv("Vales:", &self.fmt_money(self.vales)));
|
||||
out.extend(escpos::kv("A Receber:", &self.fmt_money(self.areceber)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Cartões
|
||||
out.extend(escpos::line_bold("CARTÕES"));
|
||||
out.extend(escpos::kv("Crédito:", &self.fmt_money(self.credito)));
|
||||
out.extend(escpos::kv("Débito:", &self.fmt_money(self.debito)));
|
||||
out.extend(escpos::kv("Alimentação:", &self.fmt_money(self.alimentacao)));
|
||||
out.extend(escpos::kv("Voucher:", &self.fmt_money(self.voucher)));
|
||||
out.extend(escpos::kv("Cancelamentos:", &self.fmt_money(self.cancelamentos)));
|
||||
out.extend(escpos::divider());
|
||||
|
||||
// Resultado final
|
||||
let diff_str = if self.diferenca >= 0.0 {
|
||||
format!("+{}", self.fmt_money(self.diferenca))
|
||||
} else {
|
||||
self.fmt_money(self.diferenca)
|
||||
};
|
||||
out.extend(escpos::line_bold("DIFERENÇA:"));
|
||||
out.extend(escpos::title(&diff_str));
|
||||
out.extend(escpos::blank_lines(2));
|
||||
|
||||
// Rodapé
|
||||
out.extend(escpos::divider());
|
||||
out.extend(escpos::line_center(&format!("{}", chrono::Local::now().format("%d/%m/%Y %H:%M"))));
|
||||
out.extend(escpos::line_center("Obrigado!"));
|
||||
out.extend(escpos::blank_lines(4));
|
||||
|
||||
// Corta papel
|
||||
out.extend(escpos::FEED_CUT);
|
||||
out.extend(escpos::CUT);
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
// ── USB Printer Manager ─────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod usb_windows {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::ptr;
|
||||
use windows::Win32::Devices::Usb::{
|
||||
CM_Get_Child, CM_Get_Child_Req, CM_Get_Device_IDW, CM_Get_Parent,
|
||||
CR_SUCCESS, DEVINST, GUID, HDEVINFO, DIGCF_PRESENT, DIGCF_DEVICEINTERFACE,
|
||||
};
|
||||
use windows::Win32::Foundation::{BOOL, CloseHandle, HANDLE};
|
||||
use windows::Win32::Storage::FileSystem::{
|
||||
CreateFileW, FILE_ACCESS_FLAGS, FILE_SHARE_READ, FILE_SHARE_WRITE,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
|
||||
};
|
||||
|
||||
pub fn wide_string(s: &str) -> Vec<u16> {
|
||||
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
pub fn close_handle(h: HANDLE) {
|
||||
unsafe { let _ = CloseHandle(h); }
|
||||
}
|
||||
|
||||
pub fn find_printer_device() -> Option<String> {
|
||||
// TMP: returns the first USB printer found
|
||||
// In production you'd enumerate device interfaces
|
||||
// For TM-T20, the USB device interface GUID is the printer GUID
|
||||
let guid = GUID::from_values(
|
||||
0x4d36e979, 0xe325, 0x11ce,
|
||||
[0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18],
|
||||
);
|
||||
|
||||
let devices = unsafe {
|
||||
windows::Win32::Devices::Enumeration::SetupDiGetClassDevsW(
|
||||
Some(&guid),
|
||||
None,
|
||||
None,
|
||||
DIGCF_PRESENT.0 | DIGCF_DEVICEINTERFACE.0,
|
||||
)
|
||||
};
|
||||
|
||||
if devices.is_err() {
|
||||
return None;
|
||||
}
|
||||
let devices = devices.unwrap();
|
||||
|
||||
// Try opening as a file (USB bulk endpoint workaround)
|
||||
// This works for many USB printers when accessed via a file path
|
||||
// The TM-T20 often appears as \\.\USB001
|
||||
let paths = [
|
||||
r"\\.\USB001",
|
||||
r"\\.\USB002",
|
||||
r"\\.\USB003",
|
||||
r"\\.\USB004",
|
||||
r"\\.\LPT1",
|
||||
r"\\.\COM1",
|
||||
r"\\.\COM2",
|
||||
];
|
||||
|
||||
for path in &paths {
|
||||
let wide = wide_string(path);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
windows::core::PCWSTR::from_raw(wide.as_ptr()),
|
||||
FILE_ACCESS_FLAGS(0xC0000000), // GENERIC_READ | GENERIC_WRITE
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
None,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
None,
|
||||
)
|
||||
};
|
||||
if handle.is_ok() {
|
||||
unsafe { let _ = CloseHandle(handle.unwrap()); }
|
||||
return Some((*path).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let _ = windows::Win32::Devices::Enumeration::SetupDiDestroyDeviceInfoList(devices.ok()?);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn open_printer(path: &str) -> Result<HANDLE, String> {
|
||||
let wide = wide_string(path);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
windows::core::PCWSTR::from_raw(wide.as_ptr()),
|
||||
FILE_ACCESS_FLAGS(0xC0000000),
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
None,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
match handle {
|
||||
Ok(h) => Ok(h),
|
||||
Err(e) => Err(format!("CreateFileW failed: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to(handle: HANDLE, data: &[u8]) -> Result<usize, String> {
|
||||
use windows::Win32::Storage::FileSystem::WriteFile;
|
||||
use windows::Win32::Foundation::DWORD;
|
||||
|
||||
let mut written: DWORD = 0;
|
||||
let result = unsafe {
|
||||
WriteFile(
|
||||
handle,
|
||||
data.as_ptr() as *const _,
|
||||
data.len() as DWORD,
|
||||
Some(&mut written),
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
Ok(written as usize)
|
||||
} else {
|
||||
Err(format!("WriteFile failed: {:?}", result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod usb_windows {
|
||||
pub fn find_printer_device() -> Option<String> {
|
||||
// On non-Windows, we'd use libusb or serialport
|
||||
None
|
||||
}
|
||||
pub fn open_printer(_path: &str) -> Result<(), String> {
|
||||
Err("Not supported on this platform".to_string())
|
||||
}
|
||||
pub fn write_to(_handle: (), _data: &[u8]) -> Result<usize, String> {
|
||||
Err("Not supported".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Printer Manager ────────────────────────────────────────────────────────
|
||||
|
||||
pub struct PrinterManager {
|
||||
device_path: Mutex<Option<String>>,
|
||||
printer_type: Mutex<PrinterType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PrinterType {
|
||||
Usb,
|
||||
Serial,
|
||||
Network,
|
||||
}
|
||||
|
||||
impl Default for PrinterManager {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_path: Mutex::new(None),
|
||||
printer_type: Mutex::new(PrinterType::Usb),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrinterManager {
|
||||
/// Detecta impressora USB conectada.
|
||||
pub fn detectar(&self) -> Result<String, PrinterError> {
|
||||
if let Some(path) = usb_windows::find_printer_device() {
|
||||
*self.device_path.lock().unwrap() = Some(path.clone());
|
||||
return Ok(path);
|
||||
}
|
||||
Err(PrinterError::NotFound)
|
||||
}
|
||||
|
||||
/// Configura caminho manual da impressora.
|
||||
pub fn configurar(&self, path: String) -> Result<(), PrinterError> {
|
||||
if path.is_empty() {
|
||||
return Err(PrinterError::ConfigError("Caminho não pode ser vazio".into()));
|
||||
}
|
||||
*self.device_path.lock().unwrap() = Some(path.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Imprime bytes ESC/POS diretamente.
|
||||
pub fn imprimir_bytes(&self, data: &[u8]) -> Result<usize, PrinterError> {
|
||||
let path = self.device_path.lock().unwrap().clone()
|
||||
.ok_or_else(|| PrinterError::ConfigError(
|
||||
"Impressora não configurada. Use detectar() ou configurar().".into()
|
||||
))?;
|
||||
|
||||
let handle = usb_windows::open_printer(&path)
|
||||
.map_err(|e| PrinterError::OpenFailed(e))?;
|
||||
|
||||
let n = usb_windows::write_to(handle, data)
|
||||
.map_err(|e| PrinterError::WriteFailed(e))?;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
unsafe { let _ = CloseHandle(handle); }
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
let _ = handle;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Imprime um recibo a partir dos dados estruturados.
|
||||
pub fn imprimir_recibo(&self, dados: &ReciboData) -> Result<usize, PrinterError> {
|
||||
let bytes = dados.to_escpos();
|
||||
self.imprimir_bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Retorna o caminho configurado.
|
||||
pub fn caminho(&self) -> Option<String> {
|
||||
self.device_path.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tauri Commands ─────────────────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
pub fn detectar_impressora(pm: State<'_, PrinterManager>) -> Result<String, PrinterError> {
|
||||
pm.detectar()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn configurar_impressora(pm: State<'_, PrinterManager>, caminho: String) -> Result<(), PrinterError> {
|
||||
pm.configurar(caminho)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn caminho_impressora(pm: State<'_, PrinterManager>) -> Option<String> {
|
||||
pm.caminho()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn imprimir_recibo(
|
||||
pm: State<'_, PrinterManager>,
|
||||
dados: ReciboData,
|
||||
) -> Result<usize, PrinterError> {
|
||||
pm.imprimir_recibo(&dados)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn teste_impressora(pm: State<'_, PrinterManager>) -> Result<usize, PrinterError> {
|
||||
let mut out = Vec::new();
|
||||
out.extend(escpos::INIT);
|
||||
out.extend(escpos::blank_lines(2));
|
||||
out.extend(escpos::title("TESTE DE IMPRESSAO"));
|
||||
out.extend(escpos::line_center("O Frangao - Fechamento de Caixa"));
|
||||
out.extend(escpos::line_center(&chrono::Local::now().format("%d/%m/%Y %H:%M").to_string()));
|
||||
out.extend(escpos::blank_lines(3));
|
||||
out.extend(escpos::FEED_CUT);
|
||||
out.extend(escpos::CUT);
|
||||
pm.imprimir_bytes(&out)
|
||||
}
|
||||
|
||||
pub fn init() -> PrinterManager {
|
||||
PrinterManager::default()
|
||||
}
|
||||
Reference in New Issue
Block a user