Skip to main content

Overview

The Sales & Billing module is MotorDesk’s core transaction engine. It generates SUNAT-compliant electronic invoices (Facturas) and sales receipts (Boletas) with deep integration to vehicle service history, automatic mileage tracking, and intelligent product suggestions.

Sales Features

  • Electronic invoice generation (FACTURA/BOLETA)
  • Vehicle-based prefilling from service history
  • Shopping cart with quantity management
  • Automatic tax calculation (18% IGV)
  • Kilometer tracking per service
  • Frequent product suggestions
  • Multi-payment method support

Document Data Model

Each sale generates a document record with comprehensive information:
interface Sale {
  id: string;
  branchId: string;
  userId: string;
  customerId: string;
  vehicleId: string;
  choferId: string | null;
  tipoComprobante: "FACTURA" | "BOLETA";
  serie: string;
  correlativo: string;
  fechaEmision: string;
  moneda: "PEN";
  subtotal: number;
  igv: number;
  total: number;
  kilometrajeIngreso: number;      // Current mileage at service
  proximoCambioKm: number;         // Next service mileage
  estadoSunat: string;             // "ACEPTADO" | "PENDIENTE" | "RECHAZADO"
  documentoReferenciaId: string | null;
  motivoNotaReferencia: string | null;
  createdAt: string;
  syncedAt: string | null;
  isDeleted: boolean;
}

Sale Detail Records

Each sale contains line items with product details:
interface SaleDetail {
  id: string;
  saleId: string;
  productId: string;
  cantidad: number;
  precioUnitario: number;
  subtotal: number;
}

Document Series and Numbering

MotorDesk maintains sequential numbering for each document type:
// Document initialization (src/hooks/useSales.ts:9)
const [documento, setDocumento] = useState({
  tipo: 'FACTURA',
  serie: db.documentSeries[0].serie,  // e.g., "F001"
  correlativo: String(db.documentSeries[0].correlativoActual + 1).padStart(6, '0'),
  fecha: new Date().toISOString().split('T')[0],
  cliente: db.customers.find(c => c.id === prefill?.customerId) || null,
});
Document series (e.g., F001, B002) are configured per branch and document type to comply with SUNAT requirements for electronic billing.

Vehicle-Based Prefilling

One of MotorDesk’s most powerful features is automatic form prefilling from vehicle service history:
// Prefill data from navigation state (src/hooks/useSales.ts:6)
const location = useLocation();
const prefill = location.state?.prefillData;

// Initialize extras with prefilled data
const [extras, setExtras] = useState({
  placa: prefill?.placa || "",
  ordenCompra: "",
  observaciones: prefill?.observacionSugerida || "",
  condicionPago: "CONTADO",
  metodoPago: "EFECTIVO",
  kilometrajeActual: prefill?.kilometrajeActual || "",
  proximoCambioKm: prefill?.kmProximo || ""
});

Prefill Data Structure

interface PrefillData {
  vehicleId: string;
  customerId: string;
  choferId: string | null;
  placa: string;
  cartItems: Product[];              // Pre-populated shopping cart
  observacionSugerida: string;
  kilometrajeActual: number;
  kmProximo: number;
}
1

Navigate from Vehicle Module

When creating a sale from the vehicle details view, all vehicle and customer information is automatically transferred.
2

Customer Pre-Selection

The vehicle owner is automatically selected as the customer, ready for invoicing.
3

Mileage Auto-Fill

Current mileage and next service interval are pre-populated based on the last service record.
4

Cart Pre-Population

When repeating a service, all products from the previous service are automatically added to the cart.

Shopping Cart Management

The sales module features a dynamic shopping cart:
// Add product to cart (src/hooks/useSales.ts:43)
const addItem = (product: any) => {
  const existing = cart.find(item => item.id === product.id);
  if (existing) {
    // Increment quantity if already in cart
    setCart(cart.map(item =>
      item.id === product.id ? { ...item, cantidad: item.cantidad + 1 } : item
    ));
  } else {
    // Add new item
    setCart([...cart, {
      ...product,
      tempId: crypto.randomUUID(),
      cantidad: 1,
      precioUnitario: product.precioVenta
    }]);
  }
  setProductSearch("");
};

Cart Operations

// Adds product or increments quantity if already in cart
addItem(product);
const removeItem = (tempId: string) => {
  setCart(cart.filter(item => item.tempId !== tempId));
};
const updateQuantity = (tempId: string, qty: number) => {
  setCart(cart.map(item =>
    item.tempId === tempId ? { ...item, cantidad: Math.max(1, qty) } : item
  ));
};

Product Search and Selection

Products can be added via autocomplete search:
// Product autocomplete (src/pages/Sales.tsx:284)
<Autocomplete
  placeholder="Escriba el nombre o código del producto..."
  searchValue={productSearch}
  onSearchChange={setProductSearch}
  selectedItem={null}
  options={filteredProducts}
  getDisplayValue={(p) => p.nombre}
  onSelect={(p) => {
    addItem(p);
    setProductSearch("");
  }}
  onClear={() => setProductSearch("")}
  icon={<PackageSearch className="w-5 h-5 text-blue-600" />}
  inputHeightClass="h-[46px]"
  dropdownPosition="top"
  renderOption={(p) => (
    <div className="flex justify-between items-center w-full">
      <div>
        <div className="font-bold text-slate-800 text-sm">
          {p.nombre}
        </div>
        <div className="text-slate-500 text-xs">
          Cód: {p.codigoBarras}
        </div>
      </div>
      <div className="font-bold text-emerald-600">
        S/ {p.precioVenta.toFixed(2)}
      </div>
    </div>
  )}
/>

Frequent Products

The system suggests frequently used products for quick access:
// Show frequent products when cart is empty (src/pages/Sales.tsx:194)
{productosUsuales.length > 0 && cart.length === 0 && (
  <div className="flex gap-2 mt-2">
    <span className="mt-1 font-bold text-slate-500 text-xs">
      FRECUENTES:
    </span>
    {productosUsuales.map((p: any) => (
      <button
        key={p.id}
        onClick={() => addItem(p)}
        className="bg-blue-50 hover:bg-blue-600 px-3 py-1 border border-blue-100 rounded-md"
      >
        + {p.nombre}
      </button>
    ))}
  </div>
)}

Automatic Tax Calculation

The system calculates subtotal, IGV, and total automatically:
const totals = useMemo(() => {
  const total = cart.reduce((acc, item) => 
    acc + (item.precioUnitario * item.cantidad), 0
  );
  const subtotal = total / 1.18;  // Remove 18% IGV
  const igv = total - subtotal;
  return { subtotal, igv, total };
}, [cart]);

Tax Calculation

  • Total = Sum of all line items (price × quantity)
  • Subtotal = Total ÷ 1.18 (base amount before tax)
  • IGV = Total - Subtotal (18% tax amount)

Inline Action Buttons

The sales form includes inline editable fields for service-specific data:
// Inline input component (src/pages/Sales.tsx:153)
<InlineActionButton
  label="PLACA"
  value={extras.placa?.toString() || ""}
  isEditing={inlineInputs.placa}
  onToggle={() => toggleInlineInput("placa")}
  onBlur={() => blurInlineInput("placa")}
  onChange={(val) => handleExtraChange("placa", val)}
/>

<InlineActionButton
  label="KM ACTUAL"
  value={extras.kilometrajeActual?.toString() || ""}
  isEditing={inlineInputs.kilometrajeActual}
  onToggle={() => toggleInlineInput("kilometrajeActual")}
  onBlur={() => blurInlineInput("kilometrajeActual")}
  onChange={(val) => handleExtraChange("kilometrajeActual", val)}
/>

<InlineActionButton
  label="PRÓX. CAMBIO"
  value={extras.proximoCambioKm?.toString() || ""}
  isEditing={inlineInputs.proximoCambioKm}
  onToggle={() => toggleInlineInput("proximoCambioKm")}
  onBlur={() => blurInlineInput("proximoCambioKm")}
  onChange={(val) => handleExtraChange("proximoCambioKm", val)}
/>
Inline action buttons toggle between display and edit modes, allowing quick data entry without opening modals.

Customer Selection

The customer autocomplete supports search by name or document:
const filteredCustomers = db.customers.filter(
  (c) =>
    c.nombreRazonSocial.toLowerCase().includes(customerSearch.toLowerCase()) ||
    c.numeroDocumento.includes(customerSearch)
);
// Customer autocomplete (src/pages/Sales.tsx:80)
<Autocomplete
  placeholder="Escribe para buscar un cliente..."
  searchValue={customerSearch}
  onSearchChange={setCustomerSearch}
  selectedItem={documento.cliente}
  options={filteredCustomers}
  getDisplayValue={(c) => c.nombreRazonSocial}
  onSelect={(c) => {
    if (setDocumento) setDocumento((prev) => ({ ...prev, cliente: c }));
  }}
  onClear={() => {
    if (setDocumento) setDocumento((prev) => ({ ...prev, cliente: null }));
    setCustomerSearch("");
  }}
  renderOption={(c) => (
    <>
      <div className="font-bold text-slate-800 text-sm">
        {c.nombreRazonSocial}
      </div>
      <div className="text-slate-500 text-xs">
        {c.tipoDocumentoIdentidad}: {c.numeroDocumento}
      </div>
    </>
  )}
/>

Cart Display Table

The shopping cart displays all selected products with quantity controls:
// Cart table (src/pages/Sales.tsx:218)
<table className="w-full text-slate-600 text-sm text-left">
  <thead className="sticky top-0 bg-slate-100">
    <tr>
      <th className="px-6 py-3">Producto</th>
      <th className="px-6 py-3 text-center">Precio Unit.</th>
      <th className="px-6 py-3 text-center">Total</th>
      <th className="px-6 py-3 w-32 text-center">Cantidad</th>
      <th className="px-6 py-3 w-20 text-center"></th>
    </tr>
  </thead>
  <tbody>
    {cart.map((item) => (
      <tr key={item.tempId}>
        <td className="px-6 py-3 font-semibold">{item.nombre}</td>
        <td className="px-6 py-3 text-center">S/ {item.precioUnitario.toFixed(2)}</td>
        <td className="px-6 py-3 text-center font-bold">
          S/ {(item.precioUnitario * item.cantidad).toFixed(2)}
        </td>
        <td className="px-6 py-3">
          <div className="flex justify-center items-center gap-2">
            <button onClick={() => updateQuantity(item.tempId, item.cantidad - 1)}>-</button>
            <span>{item.cantidad}</span>
            <button onClick={() => updateQuantity(item.tempId, item.cantidad + 1)}>+</button>
          </div>
        </td>
        <td className="px-6 py-3 text-center">
          <Button variant="danger" size="sm" onClick={() => removeItem(item.tempId)}>
            Eliminar
          </Button>
        </td>
      </tr>
    ))}
  </tbody>
</table>

Totals Summary

The bottom section displays calculated totals and action buttons:
// Totals display (src/pages/Sales.tsx:317)
<div className="flex justify-between items-center bg-slate-50 p-5 border rounded-xl">
  <div>
    <p className="font-black text-2xl md:text-3xl text-slate-800">
      TOTAL <span className="ml-2 text-blue-600">S/ {totals.total.toFixed(2)}</span>
    </p>
    <p className="mt-1 font-semibold text-slate-500 text-sm">
      Subtotal: S/ {totals.subtotal.toFixed(2)} &nbsp;&bull;&nbsp; IGV: S/ {totals.igv.toFixed(2)}
    </p>
  </div>
  
  <div className="flex gap-4">
    <Button variant="secondary" size="xl">VISTA PREVIA</Button>
    <Button variant="success" size="xl">PROCESAR VENTA</Button>
  </div>
</div>

Proforma Mode

The system supports generating proforma invoices (quotations):
// Proforma checkbox (src/pages/Sales.tsx:68)
<label className="flex items-center gap-2 cursor-pointer">
  <input
    type="checkbox"
    checked={proformaChecked}
    onChange={(e) => setProformaChecked(e.target.checked)}
    className="w-4 h-4"
  />
  PROFORMA
</label>

Sales Workflow

1

Select Customer

Search and select the customer who will receive the invoice. This can be pre-filled from vehicle module.
2

Configure Document

Choose document type (FACTURA/BOLETA), verify series and correlative number.
3

Add Vehicle Information

Enter or verify plate number, current mileage, and next service interval.
4

Add Products

Search and add products to cart, or use frequent product buttons for common services.
5

Review Totals

Verify subtotal, IGV, and total amounts are correct.
6

Process Sale

Click “PROCESAR VENTA” to generate the electronic document and send to SUNAT.

Best Practices

Sales Processing Tips

  • Always verify customer information before processing
  • Update vehicle mileage for accurate maintenance tracking
  • Use the repeat service feature to save time on regular customers
  • Review totals before finalizing the sale
  • Add observations for service details or special instructions
  • Use proforma mode for quotes before final invoicing

Build docs developers (and LLMs) love