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 ;
}
Navigate from Vehicle Module
When creating a sale from the vehicle details view, all vehicle and customer information is automatically transferred.
Customer Pre-Selection
The vehicle owner is automatically selected as the customer, ready for invoicing.
Mileage Auto-Fill
Current mileage and next service interval are pre-populated based on the last service record.
Cart Pre-Population
When repeating a service, all products from the previous service are automatically added to the cart.
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)
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 ) } • 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 >
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
Select Customer
Search and select the customer who will receive the invoice. This can be pre-filled from vehicle module.
Configure Document
Choose document type (FACTURA/BOLETA), verify series and correlative number.
Add Vehicle Information
Enter or verify plate number, current mileage, and next service interval.
Add Products
Search and add products to cart, or use frequent product buttons for common services.
Review Totals
Verify subtotal, IGV, and total amounts are correct.
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