Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/YonAnn99/Acrylitec/llms.txt

Use this file to discover all available pages before exploring further.

Acrylitec exposes two dedicated AJAX endpoints consumed by its frontend JavaScript. Both return JSON and are called via fetch() from the order and quotation screens. A third route — /pedidos/nuevo/ — also accepts a JSON POST body when Content-Type: application/json is set, acting as the cart-submission endpoint for the POS order screen.

POST /ajax/calcular/

Calculates the quoted price for a single line item in real time given its dimensions, product type, and optional laser cutting minutes. The quotation form calls this endpoint on every dimension or product change to show a live price preview before the user adds the item to the cart.

Request

URL: /ajax/calcular/
Method: POST
Auth: Login required
Content-Type: application/x-www-form-urlencoded
largo_pza
string | number
required
Piece length in centimetres. Passed as a form field; the view coerces it to Decimal. Send 0 or omit to default to zero.
ancho_pza
string | number
required
Piece width in centimetres. Same coercion rules as largo_pza.
producto
integer
required
Primary key of the Productos record to use. The view looks up the product to read its porcentaje_utilidad and, if set, its precio_fijo. Returns an error response if the PK does not match any product.
minutos_lazer
integer
Number of laser cutting minutes for this piece. Multiplied by ConfiguracionPrecios.tarifa_laser_minuto to produce costo_laser. Defaults to 0 if omitted or blank.

Pricing logic

The view delegates to the internal _calcular_monto() helper:
  1. area_cm2 = largo × ancho
  2. area_m2 = area_cm2 / 10 000
  3. The TabuladorCostos row matching the submitted espesor_mm value provides a factor_costo (cost per m²). If no matching row exists, factor_costo defaults to 0.00.
  4. costo_material = area_m2 × factor_costo
  5. utilidad = costo_material × (porcentaje_utilidad / 100)
  6. costo_laser = minutos_lazer × tarifa_laser_minuto
  7. monto_total = (costo_material + utilidad + costo_laser), rounded to 2 decimal places with ROUND_HALF_UP.
If the selected Productos record has a non-null precio_fijo, the calculation short-circuits: monto_total is set to precio_fijo and all cost components (costo_material, utilidad, costo_laser) are returned as 0.00.

Success response

HTTP 200 with Content-Type: application/json. All monetary values are strings with 2 decimal places.
{
  "ok": true,
  "area": "50.00",
  "costo_material": "4.25",
  "utilidad": "1.70",
  "costo_laser": "75.00",
  "monto_total": "80.95"
}
FieldTypeDescription
okboolAlways true on success.
areastringlargo × ancho in cm².
costo_materialstringRaw material cost before margin.
utilidadstringProfit margin amount.
costo_laserstringLaser cutting cost.
monto_totalstringFinal quoted amount (material + margin + laser).

Error response

Returned when the product PK is invalid or any other exception is raised during calculation.
{ "ok": false, "error": "Productos matching query does not exist." }
If the request method is not POST, the endpoint returns:
{ "ok": false }

Example fetch() call

const params = new URLSearchParams({
  largo_pza: 10,
  ancho_pza: 5,
  producto: 3,
  minutos_lazer: 5,
});

const res = await fetch('/ajax/calcular/', {
  method: 'POST',
  headers: { 'X-CSRFToken': getCookie('csrftoken') },
  body: params,
});
const data = await res.json();
if (data.ok) {
  document.getElementById('precio-preview').textContent = `$${data.monto_total}`;
}

POST /ajax/crear-cliente/

Creates a new Clientes record inline without navigating away from the POS order screen. Called by the quick-create dialog that appears when the operator clicks “Nuevo cliente” inside the order form. After a successful response, the frontend appends the new client to the Select2 dropdown and selects them automatically.

Request

URL: /ajax/crear-cliente/
Method: POST
Auth: Login required
Content-Type: application/json
Send a JSON body in the request:
{
  "nombre": "María García",
  "telefono": "555-1234",
  "email": "maria@example.com",
  "direccion": "Calle Juárez 10, CDMX"
}
nombre
string
required
Client full name. The view validates that this field is present and non-empty; missing or blank values return an error response immediately.
telefono
string
Client phone number. Stored as-is on the Clientes model. Optional.
email
string
Client email address. Optional; no format validation is performed server-side.
direccion
string
Full mailing or delivery address. Optional.

Success response

HTTP 200. Returns the new client’s database primary key and display name so the frontend can add them to the Select2 control.
{ "ok": true, "id_cliente": 42, "nombre": "María García" }
FieldTypeDescription
okboolAlways true on success.
id_clienteintegerPrimary key of the newly created Clientes record.
nombrestringThe saved client name, echoed back for display use.

Error responses

Missing nombre field:
{ "ok": false, "error": "El nombre es obligatorio" }
Wrong HTTP method (anything other than POST):
{ "ok": false, "error": "Método no permitido" }
Any other unexpected exception (e.g. database error):
{ "ok": false, "error": "<exception message>" }

Example fetch() call

const res = await fetch('/ajax/crear-cliente/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRFToken': getCookie('csrftoken'),
  },
  body: JSON.stringify({
    nombre: 'María García',
    telefono: '555-1234',
    email: 'maria@example.com',
    direccion: 'Calle Juárez 10, CDMX',
  }),
});
const data = await res.json();
if (data.ok) {
  // Add and select the new client in the Select2 dropdown
  const option = new Option(data.nombre, data.id_cliente, true, true);
  $('#id_cliente').append(option).trigger('change');
}

POS Order Submission (POST /pedidos/nuevo/)

While /pedidos/nuevo/ is a regular HTML view on GET, it also accepts a JSON POST body when the Content-Type: application/json header is present. This is the mechanism used by the POS cart screen to submit a completed order.
The view checks request.headers.get('Content-Type') == 'application/json' to decide which branch to execute. Standard HTML form submissions (without that header) are ignored by the order-creation branch and fall through to the GET render path.

Request

URL: /pedidos/nuevo/
Method: POST
Auth: Login required
Content-Type: application/json
{
  "cliente_id": 42,
  "monto_abonado": "100.00",
  "estatus": "pendiente",
  "fecha_entrega": "2025-08-30",
  "carrito": [
    {
      "producto_id": 3,
      "material_id": 1,
      "cantidad": 2,
      "largo": "10.00",
      "ancho": "5.00",
      "espesor": "3.00",
      "minutos_laser": 5,
      "subtotal": "161.90"
    }
  ]
}
Top-level fields:
cliente_id
integer
required
Primary key of an existing Clientes record. Returns a 404 response if not found.
monto_abonado
string | number
Advance payment amount. Coerced to Decimal. Defaults to 0 if omitted or blank.
estatus
string
Initial sale status. Accepted values: pendiente, en_produccion, pagada, entregada. Defaults to "pendiente" if omitted.
fecha_entrega
string
Expected delivery date in YYYY-MM-DD format. Stored as null if omitted or blank.
carrito
array
required
Array of one or more cart item objects. Each item creates one DetalleVenta record and decrements the referenced material’s stock_actual by cantidad.
Cart item fields (carrito[]):
carrito[].producto_id
integer
required
Primary key of the Productos record for this line item.
carrito[].material_id
integer
required
Primary key of the Materiales record (acrylic sheet) used for this item.
carrito[].cantidad
integer
required
Number of pieces. Used to compute the stock decrement.
carrito[].largo
string | number
Piece length in cm, stored on DetalleVenta.largo_pza.
carrito[].ancho
string | number
Piece width in cm, stored on DetalleVenta.ancho_pza.
carrito[].espesor
string | number
Material thickness in mm, stored on DetalleVenta.espesor_mm.
carrito[].minutos_laser
integer
Laser cutting minutes for this piece, stored on DetalleVenta.minutos_lazer.
carrito[].subtotal
string | number
Pre-calculated line subtotal (price × quantity). Commas are normalised to dots before Decimal conversion.

Success response

HTTP 200.
{
  "ok": true,
  "venta_id": 15,
  "alertas_stock": [
    { "nombre": "Acrílico 3mm transparente", "actual": 2, "minimo": 5 }
  ]
}
FieldTypeDescription
okboolAlways true on success.
venta_idintegerPrimary key (id_venta) of the newly created Ventas record.
alertas_stockarrayMaterials whose stock_actual fell to or below stock_minimo after this order. Empty array [] when no materials are low.

Stock alert object

FieldTypeDescription
nombrestringMateriales.descripcion of the low-stock item.
actualintegerstock_actual value after decrement.
minimointegerstock_minimo threshold value.

Error response

Any exception during order creation returns:
{ "ok": false, "error": "<exception message>" }
Stock is decremented immediately when the order is saved. There is no reservation step — if two operators submit orders for the same material simultaneously, stock could go negative. The view guards against this by clamping the decrement to max(0, stock_actual - cantidad), but the alertas_stock check will still fire.

Example fetch() call

const payload = {
  cliente_id: clienteId,
  monto_abonado: montoAbonado,
  estatus: 'pendiente',
  fecha_entrega: fechaEntrega,
  carrito: carrito.map(item => ({
    producto_id: item.productoId,
    material_id: item.materialId,
    cantidad: item.cantidad,
    largo: item.largo,
    ancho: item.ancho,
    espesor: item.espesor,
    minutos_laser: item.minutosLaser,
    subtotal: item.subtotal,
  })),
};

const res = await fetch('/pedidos/nuevo/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRFToken': getCookie('csrftoken'),
  },
  body: JSON.stringify(payload),
});
const data = await res.json();
if (data.ok) {
  if (data.alertas_stock.length > 0) {
    mostrarAlertasStock(data.alertas_stock);
  }
  window.location.href = `/ventas/${data.venta_id}/`;
}

Build docs developers (and LLMs) love