fase 2.2: impuestos configurables por instancia (tasa por servicio/articulo, calculo por linea, desglose en POS/tickets/estado de cuenta, reporte de impuestos, permiso ventas.impuestos) con defaults apagados
This commit is contained in:
@@ -14,8 +14,9 @@ import SucursalesPanel from '../components/SucursalesPanel';
|
||||
import CategoriasPanel from '../components/CategoriasPanel';
|
||||
import TerminologiaPanel from '../components/TerminologiaPanel';
|
||||
import { Card, Button, Input, PageHeader, toast } from '../components/ui';
|
||||
import { odooApi, type Doctor, type ClinicSettings, BRAND_DEFAULT, CLINIC_SETTINGS_DEFAULT } from '../services/odoo';
|
||||
import { odooApi, type Doctor, type ClinicSettings, type ImpuestosSettings, BRAND_DEFAULT, CLINIC_SETTINGS_DEFAULT, IMPUESTOS_DEFAULT } from '../services/odoo';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { publicarClinicSettings } from '../lib/clinic-settings';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'clinica', label: 'Clínica' },
|
||||
@@ -101,7 +102,11 @@ const Configuracion: FC = () => {
|
||||
} catch {
|
||||
localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
}
|
||||
if (alive) setSettings(next);
|
||||
if (alive) setSettings({
|
||||
...CLINIC_SETTINGS_DEFAULT,
|
||||
...next,
|
||||
impuestos: { ...IMPUESTOS_DEFAULT, ...(next.impuestos || {}) },
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('No se pudieron cargar los ajustes de la clínica');
|
||||
@@ -115,7 +120,10 @@ const Configuracion: FC = () => {
|
||||
const save = async () => {
|
||||
try {
|
||||
const res = await odooApi.putClinicSettings({ ...settings, motivo: 'Ajustes desde Configuración' });
|
||||
if (res.status === 'success') setSettings(res.settings);
|
||||
if (res.status === 'success') {
|
||||
setSettings(res.settings);
|
||||
publicarClinicSettings(res.settings);
|
||||
}
|
||||
toast.success('Configuración guardada');
|
||||
} catch (err) {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
@@ -127,6 +135,13 @@ const Configuracion: FC = () => {
|
||||
setSettings((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const updateImpuesto = (patch: Partial<ImpuestosSettings>) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
impuestos: { ...IMPUESTOS_DEFAULT, ...prev.impuestos, ...patch },
|
||||
}));
|
||||
};
|
||||
|
||||
// ---- Tipo de cambio USD/MXN ----
|
||||
const [tcCurrent, setTcCurrent] = useState<{ rate: number; date: string; source: string } | null>(null);
|
||||
const [tcInput, setTcInput] = useState('');
|
||||
@@ -506,6 +521,74 @@ const Configuracion: FC = () => {
|
||||
<p className="text-xs text-theme-muted">
|
||||
Moneda base: <span className="font-medium text-theme-heading">{settings.moneda_base || 'MXN'}</span>
|
||||
</p>
|
||||
|
||||
{/* Impuestos (por instancia). Apagados = comportamiento de
|
||||
siempre: sin cálculo, sin desglose y sin renglón en tickets. */}
|
||||
<div className="border border-theme-border rounded-xl p-3 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Percent size={16} className="text-theme-muted" />
|
||||
<span className="text-sm font-medium text-theme-heading">Impuestos</span>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-theme-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.impuestos.activo}
|
||||
onChange={(e) => updateImpuesto({ activo: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-theme-border-strong"
|
||||
/>
|
||||
Calcular impuestos
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-theme-muted">
|
||||
{settings.impuestos.activo
|
||||
? 'El impuesto se muestra y se cobra en POS, tickets y reportes.'
|
||||
: 'Apagado: los totales, tickets y reportes salen exactamente como hoy.'}
|
||||
</p>
|
||||
{settings.impuestos.activo && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Nombre del impuesto"
|
||||
value={settings.impuestos.nombre}
|
||||
onChange={(e) => updateImpuesto({ nombre: e.target.value })}
|
||||
placeholder="IVA"
|
||||
/>
|
||||
<Input
|
||||
label="Tasa por defecto (%)"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.01}
|
||||
value={String(settings.impuestos.tasa_default)}
|
||||
onChange={(e) => updateImpuesto({ tasa_default: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Opciones de tasa (%)"
|
||||
value={settings.impuestos.tasa_opciones.join(', ')}
|
||||
onChange={(e) => {
|
||||
const opciones = e.target.value
|
||||
.split(',')
|
||||
.map((v) => parseFloat(v.trim()))
|
||||
.filter((v) => !Number.isNaN(v) && v >= 0 && v <= 100);
|
||||
updateImpuesto({ tasa_opciones: opciones.length ? opciones : [0] });
|
||||
}}
|
||||
placeholder="0, 8, 16"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-theme-muted cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.impuestos.precios_incluyen_impuesto}
|
||||
onChange={(e) => updateImpuesto({ precios_incluyen_impuesto: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-theme-border-strong"
|
||||
/>
|
||||
Los precios capturados ya incluyen impuesto
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button onClick={save}>
|
||||
<Save size={16} className="mr-2" />
|
||||
|
||||
Reference in New Issue
Block a user