From 2b735c27b8d34c9fe371f9d470058ba485cb5602 Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 13:15:05 -0600 Subject: [PATCH 01/16] feat: Implement Code Pedimento Regimens Data Table - Added columns definition for Code Pedimento Regimens including ID, Pedimento Code, Regimen Code, Type, and Actions. - Created DataTableActions component for handling actions on each row. - Developed data-table component to manage pagination and rendering of the data table. - Introduced helper functions for rendering components and snippets in table cells. - Integrated API call in the server-side load function to fetch Code Pedimento Regimens data with pagination support. - Updated package.json to include lucide-svelte dependency for icons. --- .gitignore | 1 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 9 + frontend/src/app.css | 60 +++---- frontend/src/app.html | 2 +- frontend/src/lib/api.ts | 3 +- .../refrence_data/code_pedimento_regimens.ts | 73 ++++++++ .../code_pedimento_regimens/columns.ts | 85 ++++++++++ .../data-table-actions.svelte | 32 ++++ .../code_pedimento_regimens/data-table.svelte | 139 +++++++++++++++ .../src/lib/components/sidebar/modules.ts | 2 +- .../ui/data-table/data-table.svelte.ts | 142 ++++++++++++++++ .../ui/data-table/flex-render.svelte | 40 +++++ .../src/lib/components/ui/data-table/index.ts | 3 + .../ui/data-table/render-helpers.ts | 111 ++++++++++++ frontend/src/lib/components/ui/table/index.ts | 28 +++ .../lib/components/ui/table/table-body.svelte | 20 +++ .../components/ui/table/table-caption.svelte | 20 +++ .../lib/components/ui/table/table-cell.svelte | 23 +++ .../components/ui/table/table-footer.svelte | 20 +++ .../lib/components/ui/table/table-head.svelte | 23 +++ .../components/ui/table/table-header.svelte | 20 +++ .../lib/components/ui/table/table-row.svelte | 23 +++ .../src/lib/components/ui/table/table.svelte | 22 +++ frontend/src/routes/dashboard/+layout.svelte | 33 +++- frontend/src/routes/dashboard/+page.svelte | 160 ++++++++++++++---- .../code_pedimento_regimens/+page.server.ts | 89 ++++++++++ .../code_pedimento_regimens/+page.svelte | 113 +++++++++++++ package.json | 5 + pnpm-lock.yaml | 157 +++++++++++++++++ 30 files changed, 1390 insertions(+), 69 deletions(-) create mode 100644 frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte create mode 100644 frontend/src/lib/components/ui/data-table/data-table.svelte.ts create mode 100644 frontend/src/lib/components/ui/data-table/flex-render.svelte create mode 100644 frontend/src/lib/components/ui/data-table/index.ts create mode 100644 frontend/src/lib/components/ui/data-table/render-helpers.ts create mode 100644 frontend/src/lib/components/ui/table/index.ts create mode 100644 frontend/src/lib/components/ui/table/table-body.svelte create mode 100644 frontend/src/lib/components/ui/table/table-caption.svelte create mode 100644 frontend/src/lib/components/ui/table/table-cell.svelte create mode 100644 frontend/src/lib/components/ui/table/table-footer.svelte create mode 100644 frontend/src/lib/components/ui/table/table-head.svelte create mode 100644 frontend/src/lib/components/ui/table/table-header.svelte create mode 100644 frontend/src/lib/components/ui/table/table-row.svelte create mode 100644 frontend/src/lib/components/ui/table/table.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 471d46fd..2d9ff71d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ wheels/ *.egg-info/ .installed.cfg *.egg +.pnpm-store/ # Environment .env diff --git a/frontend/package.json b/frontend/package.json index 53787930..02dc6bfc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.14", + "@tanstack/table-core": "^8.21.3", "@types/node": "^20", "@vitest/browser": "^3.2.4", "bits-ui": "^2.14.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index eca68e3c..b514b82d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.14 version: 4.1.14(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)) + '@tanstack/table-core': + specifier: ^8.21.3 + version: 8.21.3 '@types/node': specifier: ^20 version: 20.19.22 @@ -723,6 +726,10 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -2492,6 +2499,8 @@ snapshots: tailwindcss: 4.1.14 vite: 7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1) + '@tanstack/table-core@8.21.3': {} + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 diff --git a/frontend/src/app.css b/frontend/src/app.css index 236c256b..8d9a66ab 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -42,36 +42,36 @@ .dark { --background: oklch(0.141 0.005 285.823); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.546 0.245 262.881); - --primary-foreground: oklch(0.379 0.146 265.522); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.488 0.243 264.376); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.546 0.245 262.881); - --sidebar-primary-foreground: oklch(0.379 0.146 265.522); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.488 0.243 264.376); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.546 0.245 262.881); + --primary-foreground: oklch(0.98 0.01 262.881); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.488 0.243 264.376); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.546 0.245 262.881); + --sidebar-primary-foreground: oklch(0.379 0.146 265.522); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.488 0.243 264.376); } diff --git a/frontend/src/app.html b/frontend/src/app.html index 35bd8b2c..e52450df 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4ae6cdf7..a0a996d1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -32,7 +32,8 @@ async function fetchApi( try { const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, - headers + headers, + credentials: 'include' // Importante: envía cookies con cada request }); const data = await response.json(); diff --git a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts index e69de29b..837216f8 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts @@ -0,0 +1,73 @@ +/** + * API Client para Code Pedimento Regimens + * Gestiona las operaciones CRUD para las relaciones entre códigos de pedimento y regímenes + */ +import { api } from '$lib/api'; + +export interface CodePedimentoRegimen { + id: number; + pedimento_code: string; + regimen_code: string | null; + type_code: string | null; +} + +export interface CodePedimentoRegimenListResponse { + items: CodePedimentoRegimen[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateCodePedimentoRegimenData { + pedimento_code: string; + regimen_code?: string; + type_code?: string; +} + +export interface UpdateCodePedimentoRegimenData { + pedimento_code?: string; + regimen_code?: string; + type_code?: string; +} + +/** + * API para Code Pedimento Regimens + */ +export const codePedimentoRegimensApi = { + /** + * Lista todos los code pedimento regimens con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/code-pedimento-regimens?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un code pedimento regimen por ID + * @param id - ID del code pedimento regimen + */ + get: (id: number) => api.get(`/v1/code-pedimento-regimens/${id}`), + + /** + * Crea un nuevo code pedimento regimen + * @param data - Datos del code pedimento regimen a crear + */ + create: (data: CreateCodePedimentoRegimenData) => + api.post('/v1/code-pedimento-regimens', data), + + /** + * Actualiza un code pedimento regimen existente + * @param id - ID del code pedimento regimen a actualizar + * @param data - Datos a actualizar + */ + update: (id: number, data: UpdateCodePedimentoRegimenData) => + api.put(`/v1/code-pedimento-regimens/${id}`, data), + + /** + * Elimina un code pedimento regimen + * @param id - ID del code pedimento regimen a eliminar + */ + delete: (id: number) => api.delete(`/v1/code-pedimento-regimens/${id}`) +}; diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts new file mode 100644 index 00000000..7686563b --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/columns.ts @@ -0,0 +1,85 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; + +export type CodePedimentoRegimen = { + id: number; + pedimento_code: string; + regimen_code: string | null; + type_code: string | null; +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => { + const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { + const { id } = getId(); + return { + render: () => `
${id}
` + }; + }); + return renderSnippet(idSnippet, { id: row.original.id }); + } + }, + { + accessorKey: "pedimento_code", + header: "Código Pedimento", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.pedimento_code }); + } + }, + { + accessorKey: "regimen_code", + header: "Código Régimen", + cell: ({ row }) => { + const regimenSnippet = createRawSnippet<[{ code: string | null }]>((getCode) => { + const { code } = getCode(); + if (code) { + return { + render: () => + `${code}` + }; + } + return { + render: () => `N/A` + }; + }); + return renderSnippet(regimenSnippet, { code: row.original.regimen_code }); + } + }, + { + accessorKey: "type_code", + header: "Tipo", + cell: ({ row }) => { + const typeSnippet = createRawSnippet<[{ type: string | null }]>((getType) => { + const { type } = getType(); + if (type) { + return { + render: () => + `${type}` + }; + } + return { + render: () => `N/A` + }; + }); + return renderSnippet(typeSnippet, { type: row.original.type_code }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original }); + } + } +]; diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte new file mode 100644 index 00000000..22b42007 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte @@ -0,0 +1,32 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + Acciones + navigator.clipboard.writeText(item.id.toString())}> + Copiar ID + + + + Ver detalles + Editar + + Eliminar + + diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte new file mode 100644 index 00000000..c16b5315 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte @@ -0,0 +1,139 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + +
+
+
+ Mostrando {data.length} de {totalItems} registro(s) +
+
+
+ Página {currentPage} de {Math.ceil(totalItems / pageSize)} +
+ + +
+
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 829156eb..8017eb00 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -68,7 +68,7 @@ export const sidebarData: SidebarData = { icon: SquareTerminalIcon, items: [ { - title: "", + title: "Codigos de Pedimento y Régimen", url: "/dashboard/reference_data/code_pedimento_regimens", }, { diff --git a/frontend/src/lib/components/ui/data-table/data-table.svelte.ts b/frontend/src/lib/components/ui/data-table/data-table.svelte.ts new file mode 100644 index 00000000..5b7985e7 --- /dev/null +++ b/frontend/src/lib/components/ui/data-table/data-table.svelte.ts @@ -0,0 +1,142 @@ +import { + type RowData, + type TableOptions, + type TableOptionsResolved, + type TableState, + createTable, +} from "@tanstack/table-core"; + +/** + * Creates a reactive TanStack table object for Svelte. + * @param options Table options to create the table with. + * @returns A reactive table object. + * @example + * ```svelte + * + * + * + * + * {#each table.getHeaderGroups() as headerGroup} + * + * {#each headerGroup.headers as header} + * + * {/each} + * + * {/each} + * + * + *
+ * + *
+ * ``` + */ +export function createSvelteTable(options: TableOptions) { + const resolvedOptions: TableOptionsResolved = mergeObjects( + { + state: {}, + onStateChange() {}, + renderFallbackValue: null, + mergeOptions: ( + defaultOptions: TableOptions, + options: Partial> + ) => { + return mergeObjects(defaultOptions, options); + }, + }, + options + ); + + const table = createTable(resolvedOptions); + let state = $state>(table.initialState); + + function updateOptions() { + table.setOptions((prev) => { + return mergeObjects(prev, options, { + state: mergeObjects(state, options.state || {}), + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onStateChange: (updater: any) => { + if (updater instanceof Function) state = updater(state); + else state = mergeObjects(state, updater); + + options.onStateChange?.(updater); + }, + }); + }); + } + + updateOptions(); + + $effect.pre(() => { + updateOptions(); + }); + + return table; +} + +type MaybeThunk = T | (() => T | null | undefined); +type Intersection = (T extends [infer H, ...infer R] + ? H & Intersection + : unknown) & {}; + +/** + * Lazily merges several objects (or thunks) while preserving + * getter semantics from every source. + * + * Proxy-based to avoid known WebKit recursion issue. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeObjects[]>( + ...sources: Sources +): Intersection<{ [K in keyof Sources]: Sources[K] }> { + const resolve = (src: MaybeThunk): T | undefined => + typeof src === "function" ? (src() ?? undefined) : src; + + const findSourceWithKey = (key: PropertyKey) => { + for (let i = sources.length - 1; i >= 0; i--) { + const obj = resolve(sources[i]); + if (obj && key in obj) return obj; + } + return undefined; + }; + + return new Proxy(Object.create(null), { + get(_, key) { + const src = findSourceWithKey(key); + + return src?.[key as never]; + }, + + has(_, key) { + return !!findSourceWithKey(key); + }, + + ownKeys(): (string | symbol)[] { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const all = new Set(); + for (const s of sources) { + const obj = resolve(s); + if (obj) { + for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) { + all.add(k); + } + } + } + return [...all]; + }, + + getOwnPropertyDescriptor(_, key) { + const src = findSourceWithKey(key); + if (!src) return undefined; + return { + configurable: true, + enumerable: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: (src as any)[key], + writable: true, + }; + }, + }) as Intersection<{ [K in keyof Sources]: Sources[K] }>; +} diff --git a/frontend/src/lib/components/ui/data-table/flex-render.svelte b/frontend/src/lib/components/ui/data-table/flex-render.svelte new file mode 100644 index 00000000..ac82a581 --- /dev/null +++ b/frontend/src/lib/components/ui/data-table/flex-render.svelte @@ -0,0 +1,40 @@ + + +{#if typeof content === "string"} + {content} +{:else if content instanceof Function} + + + {@const result = content(context as any)} + {#if result instanceof RenderComponentConfig} + {@const { component: Component, props } = result} + + {:else if result instanceof RenderSnippetConfig} + {@const { snippet, params } = result} + {@render snippet({ ...params, attach })} + {:else} + {result} + {/if} +{/if} diff --git a/frontend/src/lib/components/ui/data-table/index.ts b/frontend/src/lib/components/ui/data-table/index.ts new file mode 100644 index 00000000..5f4e77ea --- /dev/null +++ b/frontend/src/lib/components/ui/data-table/index.ts @@ -0,0 +1,3 @@ +export { default as FlexRender } from "./flex-render.svelte"; +export { renderComponent, renderSnippet } from "./render-helpers.js"; +export { createSvelteTable } from "./data-table.svelte.js"; diff --git a/frontend/src/lib/components/ui/data-table/render-helpers.ts b/frontend/src/lib/components/ui/data-table/render-helpers.ts new file mode 100644 index 00000000..fa036d62 --- /dev/null +++ b/frontend/src/lib/components/ui/data-table/render-helpers.ts @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, Snippet } from "svelte"; + +/** + * A helper class to make it easy to identify Svelte components in + * `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderComponentConfig} + * {@const { component: Component, props } = result} + * + * {/if} + * ``` + */ +export class RenderComponentConfig { + component: TComponent; + props: ComponentProps | Record; + constructor( + component: TComponent, + props: ComponentProps | Record = {} + ) { + this.component = component; + this.props = props; + } +} + +/** + * A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties. + * + * > NOTE: This class should only be used internally by the adapter. If you're + * reading this and you don't know what this is for, you probably don't need it. + * + * @example + * ```svelte + * {@const result = content(context as any)} + * {#if result instanceof RenderSnippetConfig} + * {@const { snippet, params } = result} + * {@render snippet(params)} + * {/if} + * ``` + */ +export class RenderSnippetConfig { + snippet: Snippet<[TProps]>; + params: TProps; + constructor(snippet: Snippet<[TProps]>, params: TProps) { + this.snippet = snippet; + this.params = params; + } +} + +/** + * A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties. + * + * This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets. + * + * @param component A Svelte component + * @param props The props to pass to `component` + * @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * header: header => renderComponent(SortHeader, { label: 'Name', header }), + * }), + * columnHelper.accessor('state', { + * header: header => renderComponent(SortHeader, { label: 'State', header }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderComponent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Component, + Props extends ComponentProps, +>(component: T, props: Props = {} as Props) { + return new RenderComponentConfig(component, props); +} + +/** + * A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties. + * + * The snippet must only take one parameter. + * + * This is only to be used with Snippets - use `renderComponent` for Svelte Components. + * + * @param snippet + * @param params + * @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet. + * @example + * ```ts + * // +page.svelte + * const defaultColumns = [ + * columnHelper.accessor('name', { + * cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }), + * }), + * columnHelper.accessor('state', { + * cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }), + * }), + * ] + * ``` + * @see {@link https://tanstack.com/table/latest/docs/guide/column-defs} + */ +export function renderSnippet(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) { + return new RenderSnippetConfig(snippet, params); +} diff --git a/frontend/src/lib/components/ui/table/index.ts b/frontend/src/lib/components/ui/table/index.ts new file mode 100644 index 00000000..14695c81 --- /dev/null +++ b/frontend/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/frontend/src/lib/components/ui/table/table-body.svelte b/frontend/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 00000000..29e96875 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-caption.svelte b/frontend/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 00000000..4696cff5 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-cell.svelte b/frontend/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 00000000..1a2f033f --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-footer.svelte b/frontend/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 00000000..b9b14ebf --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-head.svelte b/frontend/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 00000000..e9dd2378 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-header.svelte b/frontend/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 00000000..f47d2597 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-row.svelte b/frontend/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 00000000..0df769e0 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table.svelte b/frontend/src/lib/components/ui/table/table.svelte new file mode 100644 index 00000000..a3349563 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
+ + {@render children?.()} +
+
diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 8b96419d..2d21f0d2 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -1,6 +1,10 @@ -{@render children()} + + + +
+
+ + + + + + + +
+
+
+ + {@render children()} +
+
+
diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index a0031a88..316f62b4 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -1,39 +1,129 @@ - - - -
-
- - - - - - - +
+ +
+

Bienvenido al Dashboard

+

+ Sistema de gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT +

+
+ + +
+ + + Total de Pedimentos + + +
0
+

Registros activos

+
+
+ + + + Datos de Referencia + + +
12
+

Catálogos disponibles

+
+
+ + + + Licencia Activa + + +
+

Cuenta verificada

+
+
+
+ + + + + Accesos Rápidos + Accede a las funciones más utilizadas del sistema + + +
+ + + + + + + + + Código Pedimento - Regímenes + Gestionar relaciones + + +
+ + + + + + Catálogo de Tipos + Próximamente +
+ +
+ + + + + + Reportes + Próximamente +
-
-
-
-
-
-
-
-
-
-
-
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts new file mode 100644 index 00000000..3aabfdd4 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts @@ -0,0 +1,89 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url }) => { + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + console.log('📊 [Code Pedimento Regimens] Fetching data:', { + url: `${baseUrl}v1/code-pedimento-regimens`, + page, + pageSize + }); + + const response = await fetch( + `${baseUrl}v1/code-pedimento-regimens?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Code Pedimento Regimens] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + console.log('📊 [Code Pedimento Regimens] Data loaded:', { + total: data.total, + itemsCount: data.items?.length + }); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Code Pedimento Regimens] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte index e69de29b..85e4e473 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte @@ -0,0 +1,113 @@ + + +
+ +
+
+

Código Pedimento - Regímenes

+

+ Gestiona las relaciones entre códigos de pedimento y regímenes +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Relaciones + Total de registros: {totalItems} +
+ +
+
+ + + + +
+
diff --git a/package.json b/package.json new file mode 100644 index 00000000..24a8af56 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "lucide-svelte": "^0.552.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..47270527 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,157 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + lucide-svelte: + specifier: ^0.552.0 + version: 0.552.0(svelte@5.43.2) + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@sveltejs/acorn-typescript@1.0.6': + resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} + peerDependencies: + acorn: ^8.9.0 + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.1.2: + resolution: {integrity: sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + lucide-svelte@0.552.0: + resolution: {integrity: sha512-zynJ64KOsuQG3I4tSqfvvl7Kc9x4mWkppbxsuyrbegQwma9HFhBp4aE6HuQNF4c3pS0AHWHki5CAMs5m3QXA5w==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + svelte@5.43.2: + resolution: {integrity: sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==} + engines: {node: '>=18'} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@types/estree@1.0.8': {} + + acorn@8.15.0: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + clsx@2.1.1: {} + + esm-env@1.2.2: {} + + esrap@2.1.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + locate-character@3.0.0: {} + + lucide-svelte@0.552.0(svelte@5.43.2): + dependencies: + svelte: 5.43.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + svelte@5.43.2: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.2 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + zimmerframe@1.1.4: {} From 886a3aeab3ef9dd65cfd003b75678e74943d7aba Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 13:48:42 -0600 Subject: [PATCH 02/16] feat: Implement token refresh mechanism and infinite scroll for code pedimento regimens --- frontend/src/lib/api.ts | 151 +++++++++++++++++- frontend/src/lib/auth.ts | 59 +++++++ .../code_pedimento_regimens/data-table.svelte | 126 +++++++-------- .../src/routes/dashboard/+layout.server.ts | 98 ++++++++++-- .../code_pedimento_regimens/+page.server.ts | 5 +- .../code_pedimento_regimens/+page.svelte | 122 +++++++++++--- 6 files changed, 449 insertions(+), 112 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index a0a996d1..d46689f7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2,6 +2,7 @@ * Cliente API para comunicación con el backend */ import { getToken } from './auth'; +import { browser } from '$app/environment'; const API_BASE_URL = import.meta.env.VITE_API_URL; @@ -11,13 +12,121 @@ export interface ApiResponse { status: number; } +let isRefreshing = false; +let refreshSubscribers: ((token: string) => void)[] = []; + /** - * Realiza una petición al API + * Agrega una petición a la cola de espera mientras se refresca el token + */ +function subscribeTokenRefresh(callback: (token: string) => void) { + refreshSubscribers.push(callback); +} + +/** + * Notifica a todas las peticiones en espera que el token se ha refrescado + */ +function onTokenRefreshed(token: string) { + refreshSubscribers.forEach((callback) => callback(token)); + refreshSubscribers = []; +} + +/** + * Intenta refrescar el token usando el refresh token + */ +async function refreshToken(): Promise { + if (!browser) return null; + + const refreshTokenValue = localStorage.getItem('refresh_token'); + if (!refreshTokenValue) { + console.warn('🔄 No refresh token available'); + return null; + } + + try { + console.log('🔄 [API] Intentando refrescar token...'); + + const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ refresh_token: refreshTokenValue }), + credentials: 'include' + }); + + if (!response.ok) { + console.error('❌ [API] Refresh token expirado o inválido'); + // Si el refresh token también está expirado, limpiar todo + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + // Limpiar cookies también + document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC'; + document.cookie = 'refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC'; + // Redirigir al login después de un pequeño delay para que el usuario vea el mensaje + setTimeout(() => { + if (browser) { + window.location.href = '/login'; + } + }, 2000); + return null; + } + + const data = await response.json(); + + // Guardar los nuevos tokens + if (data.access_token) { + console.log('✅ [API] Token refrescado exitosamente'); + localStorage.setItem('access_token', data.access_token); + + if (data.refresh_token) { + localStorage.setItem('refresh_token', data.refresh_token); + } + + // Actualizar también las cookies + const isSecure = window.location.protocol === 'https:'; + const secureFlag = isSecure ? '; Secure' : ''; + + document.cookie = `access_token=${data.access_token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax${secureFlag}`; + if (data.refresh_token) { + document.cookie = `refresh_token=${data.refresh_token}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax${secureFlag}`; + } + + // Actualizar el authStore si está disponible + try { + const { authStore } = await import('./auth'); + authStore.setToken(data.access_token); + } catch (e) { + // Si no se puede importar authStore, no es crítico + console.warn('⚠️ [API] No se pudo actualizar authStore:', e); + } + + return data.access_token; + } + + return null; + } catch (error) { + console.error('❌ [API] Error refreshing token:', error); + return null; + } +} + +/** + * Realiza una petición al API con manejo automático de refresh token */ async function fetchApi( endpoint: string, - options: RequestInit = {} + options: RequestInit = {}, + retryCount = 0 ): Promise> { + // Si ya estamos refrescando el token, esperar + if (isRefreshing && retryCount === 0) { + return new Promise((resolve) => { + subscribeTokenRefresh((newToken) => { + resolve(fetchApi(endpoint, options, 1)); + }); + }); + } + const token = getToken(); const headers: Record = { @@ -36,6 +145,41 @@ async function fetchApi( credentials: 'include' // Importante: envía cookies con cada request }); + // Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token + if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) { + console.warn(`⚠️ [API] ${response.status} recibido en ${endpoint}, intentando refrescar token...`); + console.log(`⚠️ [API] Token actual disponible:`, token ? 'Sí (parcial: ' + token.substring(0, 20) + '...)' : 'No'); + isRefreshing = true; + + try { + const newToken = await refreshToken(); + + if (newToken) { + // Token refrescado exitosamente + console.log(`✅ [API] Reintentando petición a ${endpoint} con nuevo token`); + onTokenRefreshed(newToken); + isRefreshing = false; + // Reintentar la petición original con el nuevo token + return await fetchApi(endpoint, options, 1); + } else { + console.error(`❌ [API] No se pudo refrescar el token para ${endpoint}`); + isRefreshing = false; + // Retornar error 401 para que la capa superior lo maneje + return { + error: 'Sesión expirada. Por favor, inicia sesión nuevamente.', + status: 401 + }; + } + } catch (refreshError) { + console.error(`❌ [API] Error al refrescar token:`, refreshError); + isRefreshing = false; + return { + error: 'Error al refrescar la sesión', + status: 401 + }; + } + } + const data = await response.json(); if (!response.ok) { @@ -50,6 +194,7 @@ async function fetchApi( status: response.status }; } catch (error) { + console.error(`❌ [API] Error de conexión en ${endpoint}:`, error); return { error: 'Error de conexión con el servidor', status: 0 @@ -79,6 +224,8 @@ export const api = { auth: { login: (credentials: { username: string; password: string; tenant_slug: string }) => api.post('/v1/auth/login', credentials), + refresh: (refreshToken: string) => + api.post('/v1/auth/refresh', { refresh_token: refreshToken }), logout: (data: { refresh_token: string }) => api.post('/v1/auth/logout', data), me: () => api.get('/v1/auth/me'), health: () => api.get('/health') diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index ff0b5933..6c1c1da6 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -289,6 +289,9 @@ export const login = async (credentials: { // Guardar en cookies para que el servidor pueda acceder setCookie('access_token', loginData.access_token); + if (loginData.refresh_token) { + setCookie('refresh_token', loginData.refresh_token); + } } // Cargar información del usuario @@ -362,6 +365,8 @@ export const logout = async () => { authStore.reset(); localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); + deleteCookie('access_token'); + deleteCookie('refresh_token'); // Si hay instancia de Keycloak, hacer logout de Keycloak if (keycloakInstance?.authenticated) { @@ -411,6 +416,60 @@ export const getToken = (): string | null => { return null; }; +/** + * Refresca el access token usando el refresh token + */ +export const refreshAccessToken = async (): Promise => { + if (!browser) return false; + + const refreshToken = localStorage.getItem('refresh_token'); + if (!refreshToken) { + console.warn('No refresh token available'); + return false; + } + + try { + const { api } = await import('./api'); + const response = await api.auth.refresh(refreshToken); + + if (response.error || !response.data) { + console.error('Failed to refresh token:', response.error); + // Si falla el refresh, hacer logout + await logout(); + return false; + } + + // Actualizar tokens + const newAccessToken = response.data.access_token; + const newRefreshToken = response.data.refresh_token; + + authStore.setToken(newAccessToken); + localStorage.setItem('access_token', newAccessToken); + + if (newRefreshToken) { + localStorage.setItem('refresh_token', newRefreshToken); + } + + // Actualizar también la cookie + setCookie('access_token', newAccessToken); + + console.log('✅ Token refreshed successfully'); + return true; + } catch (error) { + console.error('Error refreshing token:', error); + await logout(); + return false; + } +}; + +/** + * Obtiene el refresh token + */ +export const getRefreshToken = (): string | null => { + if (!browser) return null; + return localStorage.getItem('refresh_token'); +}; + /** * Obtiene la instancia de Keycloak */ diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte index c16b5315..ef98de23 100644 --- a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte @@ -1,78 +1,68 @@
-
+
- + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} @@ -107,33 +97,27 @@ {/each} + + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más... +
+ {:else} +
+ Desplázate para cargar más +
+ {/if} +
+
+
+ {/if}
-
-
- Mostrando {data.length} de {totalItems} registro(s) -
-
-
- Página {currentPage} de {Math.ceil(totalItems / pageSize)} -
- - -
-
diff --git a/frontend/src/routes/dashboard/+layout.server.ts b/frontend/src/routes/dashboard/+layout.server.ts index b95e0ad3..94db7853 100644 --- a/frontend/src/routes/dashboard/+layout.server.ts +++ b/frontend/src/routes/dashboard/+layout.server.ts @@ -3,7 +3,8 @@ import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { // Verificar si existe el token en las cookies - const token = cookies.get('access_token'); + let token = cookies.get('access_token'); + const refreshToken = cookies.get('refresh_token'); // Si no hay token, redirigir al login if (!token) { @@ -11,20 +12,19 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`); } + // Configurar la URL de la API + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL: asegurar que termine con '/' + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + // Validar el token con el backend para asegurar que sea válido try { - // En Docker, el servidor debe usar el nombre del servicio 'backend' en lugar de 'localhost' - // VITE_API_URL ya incluye '/api/' al final (ej: http://localhost:8000/api/) - let apiUrl = process.env.INTERNAL_API_URL; - if (!apiUrl) { - apiUrl = import.meta.env.VITE_API_URL; - // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) - apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); - } - - // Normalizar la URL: asegurar que termine con '/' - const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; - console.log('🔐 [Dashboard] Validando token con:', `${baseUrl}v1/auth/me`); const response = await fetch(`${baseUrl}v1/auth/me`, { @@ -33,9 +33,76 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { } }); - if (!response.ok) { - // Token inválido, limpiar y redirigir + // Si el token está expirado (401) y tenemos refresh token, intentar refrescar + if (response.status === 401 && refreshToken) { + console.log('🔄 [Dashboard] Token expirado, intentando refrescar...'); + + try { + const refreshResponse = await fetch(`${baseUrl}v1/auth/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ refresh_token: refreshToken }) + }); + + if (refreshResponse.ok) { + const refreshData = await refreshResponse.json(); + + // Actualizar las cookies con los nuevos tokens + cookies.set('access_token', refreshData.access_token, { + path: '/', + httpOnly: false, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7 // 7 días + }); + + if (refreshData.refresh_token) { + cookies.set('refresh_token', refreshData.refresh_token, { + path: '/', + httpOnly: false, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 30 // 30 días + }); + } + + // Usar el nuevo token para obtener la info del usuario + token = refreshData.access_token; + console.log('✅ [Dashboard] Token refrescado exitosamente'); + + // Reintentar la validación con el nuevo token + const retryResponse = await fetch(`${baseUrl}v1/auth/me`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (retryResponse.ok) { + const userData = await retryResponse.json(); + return { + authenticated: true, + user: userData + }; + } + } else { + console.log('❌ [Dashboard] Refresh token también está expirado'); + } + } catch (refreshError) { + console.error('🔐 [Dashboard] Error al refrescar token:', refreshError); + } + + // Si llegamos aquí, el refresh falló cookies.delete('access_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`); + } + + if (!response.ok) { + // Token inválido y no se pudo refrescar, limpiar y redirigir + cookies.delete('access_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`); } @@ -54,6 +121,7 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { // Para cualquier otro error (conexión, etc), limpiar token y redirigir console.error('🔐 [Dashboard] Error validando token:', error); cookies.delete('access_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`); } }; diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts index 3aabfdd4..603f5785 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts @@ -1,6 +1,9 @@ import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ cookies, fetch, url }) => { +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + const token = cookies.get('access_token'); if (!token) { diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte index 85e4e473..4dda50d2 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte @@ -1,32 +1,107 @@ @@ -76,7 +151,9 @@
Listado de Relaciones - Total de registros: {totalItems} + + Mostrando {allItems.length} de {totalItems} registros +
+ + + + + diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte index 22b42007..9dc88ade 100644 --- a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/data-table-actions.svelte @@ -3,8 +3,37 @@ import { Button } from "$lib/components/ui/button/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; import type { CodePedimentoRegimen } from "./columns.js"; + import CreateEditDialog from "./create-edit-dialog.svelte"; + import DetailsDialog from "./details-dialog.svelte"; + import DeleteDialog from "./delete-dialog.svelte"; - let { item }: { item: CodePedimentoRegimen } = $props(); + let { + item, + onSuccess + }: { + item: CodePedimentoRegimen; + onSuccess?: () => void; + } = $props(); + + let showDetailsDialog = $state(false); + let showEditDialog = $state(false); + let showDeleteDialog = $state(false); + + function handleCopyId() { + navigator.clipboard.writeText(item.id.toString()); + } + + function handleViewDetails() { + showDetailsDialog = true; + } + + function handleEdit() { + showEditDialog = true; + } + + function handleDelete() { + showDeleteDialog = true; + } @@ -19,14 +48,19 @@ Acciones - navigator.clipboard.writeText(item.id.toString())}> + Copiar ID - Ver detalles - Editar + Ver detalles + Editar - Eliminar + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/delete-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/delete-dialog.svelte new file mode 100644 index 00000000..73d891e6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/delete-dialog.svelte @@ -0,0 +1,118 @@ + + + + + + ¿Estás seguro? + +

Esta acción no se puede deshacer. Se eliminará permanentemente este registro:

+ {#if item} +
+
+ ID: + {item.id} +
+
+ Código Pedimento: + {item.pedimento_code} +
+ {#if item.regimen_code} +
+ Código Régimen: + {item.regimen_code} +
+ {/if} +
+ {/if} + {#if error} +
+ {error} +
+ {/if} +
+
+ + Cancelar + + {#if loading} + + + + + {/if} + Eliminar + + +
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/details-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/details-dialog.svelte new file mode 100644 index 00000000..0c137903 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/code_pedimento_regimens/details-dialog.svelte @@ -0,0 +1,84 @@ + + + + + + Detalles del Registro + + Información completa de la relación código pedimento - régimen + + + + {#if item} +
+
+
+ ID + {item.id} +
+ +
+ +
+
+ Código Pedimento + + {item.pedimento_code} + +
+ +
+ +
+
+ Código Régimen + {#if item.regimen_code} + + {item.regimen_code} + + {:else} + No especificado + {/if} +
+ +
+ +
+
+ Tipo + {#if item.type_code} + + {item.type_code} + + {:else} + No especificado + {/if} +
+
+
+ {/if} + + + + +
+
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 00000000..a0056912 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 00000000..a7b0cf79 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 00000000..6c3c6046 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 00000000..2ec67dc2 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 00000000..f78b97ab --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 00000000..c8fa7625 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 00000000..a64ee768 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 00000000..7ef2b5fb --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 00000000..b22d1d50 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 00000000..cc281c58 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,39 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Portal = AlertDialogPrimitive.Portal; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/frontend/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 00000000..840b2f68 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 00000000..a647d566 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,43 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/frontend/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 00000000..38450239 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 00000000..e7ff4468 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/frontend/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 00000000..fc90cd9b --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 00000000..f81ad833 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/frontend/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 00000000..067e55ec --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 00000000..9d1e8011 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/index.ts b/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 00000000..dce1d9dc --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +const Root = DialogPrimitive.Root; +const Portal = DialogPrimitive.Portal; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte index cd344faf..a72beb5f 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.svelte @@ -2,7 +2,8 @@ import { onMount } from 'svelte'; import { codePedimentoRegimensApi, type CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens'; import DataTable from '$lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte'; - import { columns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js'; + import { createColumns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js'; + import CreateEditDialog from '$lib/components/dashboard/reference_data/code_pedimento_regimens/create-edit-dialog.svelte'; import * as Card from '$lib/components/ui/card'; import { Button } from '$lib/components/ui/button'; import type { PageData } from './$types'; @@ -10,6 +11,9 @@ // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); + + // Estado para el diálogo de crear + let showCreateDialog = $state(false); // Sincronizar token de cookies a localStorage al montar el componente onMount(() => { @@ -92,6 +96,18 @@ // Reset y recargar desde el principio window.location.reload(); } + + function handleCreateClick() { + showCreateDialog = true; + } + + function handleSuccess() { + // Recargar datos después de crear/editar/eliminar + reloadData(); + } + + // Crear columnas con el callback onSuccess + const columns = createColumns(handleSuccess);
@@ -103,7 +119,7 @@ Gestiona las relaciones entre códigos de pedimento y regímenes

-
+ + + From 4099d7376536576faef6a20b12520fbb8e02fba8 Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 14:47:44 -0600 Subject: [PATCH 05/16] feat(containers): implement CRUD operations and UI for container management --- .../api/dashboard/refrence_data/containers.ts | 69 ++++++ .../reference_data/containers/columns.ts | 50 +++++ .../containers/create-edit-dialog.svelte | 184 ++++++++++++++++ .../containers/data-table-actions.svelte | 66 ++++++ .../containers/data-table.svelte | 123 +++++++++++ .../containers/delete-dialog.svelte | 112 ++++++++++ .../containers/details-dialog.svelte | 57 +++++ .../src/lib/components/sidebar/modules.ts | 6 +- .../src/lib/components/ui/switch/index.ts | 7 + .../lib/components/ui/switch/switch.svelte | 51 +++++ .../reference_data/containers/+page.server.ts | 81 ++++++++ .../reference_data/containers/+page.svelte | 196 ++++++++++++++++++ 12 files changed, 999 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/api/dashboard/refrence_data/containers.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/containers/details-dialog.svelte create mode 100644 frontend/src/lib/components/ui/switch/index.ts create mode 100644 frontend/src/lib/components/ui/switch/switch.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/containers/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/containers/+page.svelte diff --git a/frontend/src/lib/api/dashboard/refrence_data/containers.ts b/frontend/src/lib/api/dashboard/refrence_data/containers.ts new file mode 100644 index 00000000..1bd2a7fc --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/containers.ts @@ -0,0 +1,69 @@ +/** + * API Client para Containers + * Gestiona las operaciones CRUD para los contenedores + */ +import { api } from '$lib/api'; + +export interface Container { + key: string; + description: string; +} + +export interface ContainerListResponse { + items: Container[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateContainerData { + key: string; + description: string; +} + +export interface UpdateContainerData { + key?: string; + description?: string; +} + +/** + * API para Containers + */ +export const containersApi = { + /** + * Lista todos los containers con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/containers?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un container por ID + * @param key - ID del container + */ + get: (key: number) => api.get(`/v1/containers/${key}`), + + /** + * Crea un nuevo container + * @param data - Datos del container a crear + */ + create: (data: CreateContainerData) => + api.post('/v1/containers', data), + + /** + * Actualiza un container existente + * @param key - ID del container a actualizar + * @param data - Datos a actualizar + */ + update: (key: number, data: UpdateContainerData) => + api.put(`/v1/containers/${key}`, data), + + /** + * Elimina un container + * @param key - ID del container a eliminar + */ + delete: (key: number) => api.delete(`/v1/containers/${key}`) +}; diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/columns.ts b/frontend/src/lib/components/dashboard/reference_data/containers/columns.ts new file mode 100644 index 00000000..1af38551 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/columns.ts @@ -0,0 +1,50 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; + +export type Container = { + key: string; + description: string; +}; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: "key", + header: "Código", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.key }); + } + }, + { + accessorKey: "description", + header: "Descripción", + cell: ({ row }) => { + const descSnippet = createRawSnippet<[{ description: string }]>((getDesc) => { + const { description } = getDesc(); + return { + render: () => `
${description}
` + }; + }); + return renderSnippet(descSnippet, { description: row.original.description }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original, onSuccess }); + } + } + ]; +} + +// Mantener compatibilidad hacia atrás +export const columns = createColumns(); diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte new file mode 100644 index 00000000..75aec764 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/create-edit-dialog.svelte @@ -0,0 +1,184 @@ + + + + + + + {isEditing ? "Editar" : "Nuevo"} Contenedor + + + {isEditing + ? "Modifica los datos del contenedor." + : "Completa los datos para crear un nuevo contenedor."} + + + +
+ {#if error} +
+ {error} +
+ {/if} + +
+ + +
+ +
+ + +
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte new file mode 100644 index 00000000..f23c31c8 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/data-table-actions.svelte @@ -0,0 +1,66 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + Acciones + + Copiar ID + + + + Ver detalles + Editar + + Eliminar + + + + + + + diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte new file mode 100644 index 00000000..ef98de23 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/data-table.svelte @@ -0,0 +1,123 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más... +
+ {:else} +
+ Desplázate para cargar más +
+ {/if} +
+
+
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/delete-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/delete-dialog.svelte new file mode 100644 index 00000000..88ddcb9b --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/delete-dialog.svelte @@ -0,0 +1,112 @@ + + + + + + ¿Estás seguro? + +

Esta acción no se puede deshacer. Se eliminará permanentemente este contenedor:

+ {#if item} +
+
+ Código: + {item.key} +
+
+ Descripción: + {item.description} +
+
+ {/if} + {#if error} +
+ {error} +
+ {/if} +
+
+ + Cancelar + + {#if loading} + + + + + {/if} + Eliminar + + +
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/containers/details-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/containers/details-dialog.svelte new file mode 100644 index 00000000..4a7a7bc6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/containers/details-dialog.svelte @@ -0,0 +1,57 @@ + + + + + + Detalles del Contenedor + + Información completa del contenedor + + + + {#if item} +
+
+
+ Código + + {item.key} + +
+ +
+ +
+
+ Descripción +

{item.description}

+
+ +
+
+ {/if} + + + + +
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 8017eb00..151628b8 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -63,7 +63,7 @@ export const sidebarData: SidebarData = { ], navMain: [ { - title: "Catalogos Generales", + title: "Catalogos Fijos", url: "/dashboard", icon: SquareTerminalIcon, items: [ @@ -72,8 +72,8 @@ export const sidebarData: SidebarData = { url: "/dashboard/reference_data/code_pedimento_regimens", }, { - title: "Reportes", - url: "#", + title: "Contenedores", + url: "/dashboard/reference_data/containers", }, ], isActive: true, diff --git a/frontend/src/lib/components/ui/switch/index.ts b/frontend/src/lib/components/ui/switch/index.ts new file mode 100644 index 00000000..f0e5fb79 --- /dev/null +++ b/frontend/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch +}; diff --git a/frontend/src/lib/components/ui/switch/switch.svelte b/frontend/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 00000000..5a512182 --- /dev/null +++ b/frontend/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,51 @@ + + + diff --git a/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts b/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts new file mode 100644 index 00000000..d63ee421 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/containers?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Containers] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Containers] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/containers/+page.svelte b/frontend/src/routes/dashboard/reference_data/containers/+page.svelte new file mode 100644 index 00000000..98af455c --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/containers/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Contenedores

+

+ Gestiona los tipos de contenedores disponibles +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Contenedores + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + From 39365f4e8356f5220bff43cd2919b422b07b4723 Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 15:33:19 -0600 Subject: [PATCH 06/16] feat(sidebar): enhance sidebar with additional navigation items and translations feat(translations): update English and Spanish message files with new keys refactor(layout): simplify layout component by removing loading state logic refactor(page.server): adjust authentication flow to return public page for unauthenticated users --- frontend/messages/en.json | 34 ++- frontend/messages/es.json | 34 ++- .../lib/components/sidebar/app-sidebar.svelte | 5 +- .../src/lib/components/sidebar/modules.ts | 260 +++++++++++------- frontend/src/routes/+layout.svelte | 20 +- frontend/src/routes/+page.server.ts | 6 +- 6 files changed, 238 insertions(+), 121 deletions(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 37a98944..f17f8b15 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,4 +1,36 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from en!" + "hello_world": "Hello, {name} from en!", + "sidebar": { + "catalogos_fijos": "Fixed Catalogs", + "codigos_pedimento_regimen": "Pedimento and Regime Codes", + "contenedores": "Containers", + "paises": "Countries", + "tipos_moneda": "Currency Types", + "secciones_aduanas": "Customs Sections", + "recintos": "Customs Warehouses", + "incoterms": "Incoterms", + "tipos_factura": "Invoice Types", + "tipos_material": "Material Types", + "metodos_pago": "Payment Methods", + "codigos_pedimento": "Pedimento Codes", + "regimenes_pedimentos": "Pedimento Regimes", + "sectores": "Sectors", + "estados": "States", + "metodos_transporte": "Transportation Modes", + "tipos_transporte": "Transportation Types", + "metodos_valoracion": "Valuation Methods", + "inventarios": "Inventories", + "gestionar_inventarios": "Manage Inventories", + "reportes": "Reports", + "pedimentos": "Pedimentos", + "nuevo_pedimento": "New Pedimento", + "consultar": "Search", + "historial": "History", + "configuracion": "Settings", + "general": "General", + "licencia": "License", + "usuarios": "Users", + "ayuda": "Help" + } } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 176345c1..4885e8aa 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,4 +1,36 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from es!" + "hello_world": "Hello, {name} from es!", + "sidebar": { + "catalogos_fijos": "Catálogos Fijos", + "codigos_pedimento_regimen": "Códigos de Pedimento y Régimen", + "contenedores": "Contenedores", + "paises": "Países", + "tipos_moneda": "Tipos de moneda", + "secciones_aduanas": "Secciones de aduanas", + "recintos": "Recintos", + "incoterms": "Incoterms", + "tipos_factura": "Tipos de factura", + "tipos_material": "Tipos de material", + "metodos_pago": "Métodos de pago", + "codigos_pedimento": "Códigos de pedimento", + "regimenes_pedimentos": "Regímenes de pedimentos", + "sectores": "Sectores", + "estados": "Estados", + "metodos_transporte": "Métodos de transporte", + "tipos_transporte": "Tipos de transporte", + "metodos_valoracion": "Métodos de valoración", + "inventarios": "Inventarios", + "gestionar_inventarios": "Gestionar Inventarios", + "reportes": "Reportes", + "pedimentos": "Pedimentos", + "nuevo_pedimento": "Nuevo Pedimento", + "consultar": "Consultar", + "historial": "Historial", + "configuracion": "Configuración", + "general": "General", + "licencia": "Licencia", + "usuarios": "Usuarios", + "ayuda": "Ayuda" + } } diff --git a/frontend/src/lib/components/sidebar/app-sidebar.svelte b/frontend/src/lib/components/sidebar/app-sidebar.svelte index 817f1f44..b723c4a8 100644 --- a/frontend/src/lib/components/sidebar/app-sidebar.svelte +++ b/frontend/src/lib/components/sidebar/app-sidebar.svelte @@ -1,6 +1,6 @@ -{#if initialized} - {@render children?.()} -{:else} -
-
-
-

Cargando Anexo76...

-
-
-{/if} +{@render children?.()} diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index d237e4e6..b8c31c46 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -9,6 +9,8 @@ export const load: PageServerLoad = async ({ cookies }) => { throw redirect(303, '/dashboard'); } - // Si no está autenticado, redirigir al login - throw redirect(303, '/login'); + // Si no está autenticado, mostrar la página principal pública + return { + isAuthenticated: false + }; }; From 27b58805243955c69cf5edfbfd5f216ed0e46b82 Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 15:40:26 -0600 Subject: [PATCH 07/16] feat(countries): implement CRUD operations and UI for country management, including dialogs and data table with infinite scroll --- .../api/dashboard/refrence_data/countries.ts | 78 ++++++ .../reference_data/countries/columns.ts | 94 +++++++ .../countries/create-edit-dialog.svelte | 239 ++++++++++++++++++ .../countries/data-table-actions.svelte | 66 +++++ .../countries/data-table.svelte | 123 +++++++++ .../countries/delete-dialog.svelte | 116 +++++++++ .../countries/details-dialog.svelte | 79 ++++++ .../reference_data/countries/+page.server.ts | 81 ++++++ .../reference_data/countries/+page.svelte | 196 ++++++++++++++ .../currency_types/+page.svelte | 0 10 files changed, 1072 insertions(+) create mode 100644 frontend/src/lib/api/dashboard/refrence_data/countries.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/countries/details-dialog.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/countries/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/countries/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte diff --git a/frontend/src/lib/api/dashboard/refrence_data/countries.ts b/frontend/src/lib/api/dashboard/refrence_data/countries.ts new file mode 100644 index 00000000..66fba25e --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/countries.ts @@ -0,0 +1,78 @@ +/** + * API Client para Countries + * Gestiona las operaciones CRUD para los países + */ +import { api } from '$lib/api'; + +export interface Country { + m3_key: string; + mex_key: string; + ame_key: string; + description_es: string; + description_en: string; +} + +export interface CountryListResponse { + items: Country[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateCountryData { + m3_key: string; + mex_key: string; + ame_key: string; + description_es: string; + description_en: string; +} + +export interface UpdateCountryData { + m3_key?: string; + mex_key?: string; + ame_key?: string; + description_es?: string; + description_en?: string; +} + +/** + * API para Countries + */ +export const countriesApi = { + /** + * Lista todos los países con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/countries?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un país por su clave M3 + * @param m3_key - Clave M3 del país + */ + get: (m3_key: string) => api.get(`/v1/countries/${m3_key}`), + + /** + * Crea un nuevo país + * @param data - Datos del país a crear + */ + create: (data: CreateCountryData) => + api.post('/v1/countries', data), + + /** + * Actualiza un país existente + * @param m3_key - Clave M3 del país a actualizar + * @param data - Datos a actualizar + */ + update: (m3_key: string, data: UpdateCountryData) => + api.put(`/v1/countries/${m3_key}`, data), + + /** + * Elimina un país + * @param m3_key - Clave M3 del país a eliminar + */ + delete: (m3_key: string) => api.delete(`/v1/countries/${m3_key}`) +}; diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/columns.ts b/frontend/src/lib/components/dashboard/reference_data/countries/columns.ts new file mode 100644 index 00000000..2b24e5e3 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/columns.ts @@ -0,0 +1,94 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; + +export type Country = { + m3_key: string; + mex_key: string; + ame_key: string; + description_es: string; + description_en: string; +}; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: "m3_key", + header: "Clave M3", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.m3_key }); + } + }, + { + accessorKey: "mex_key", + header: "Clave MX", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.mex_key }); + } + }, + { + accessorKey: "ame_key", + header: "Clave AME", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.ame_key }); + } + }, + { + accessorKey: "description_es", + header: "Descripción (ES)", + cell: ({ row }) => { + const descSnippet = createRawSnippet<[{ description: string }]>((getDesc) => { + const { description } = getDesc(); + return { + render: () => `
${description}
` + }; + }); + return renderSnippet(descSnippet, { description: row.original.description_es }); + } + }, + { + accessorKey: "description_en", + header: "Descripción (EN)", + cell: ({ row }) => { + const descSnippet = createRawSnippet<[{ description: string }]>((getDesc) => { + const { description } = getDesc(); + return { + render: () => `
${description}
` + }; + }); + return renderSnippet(descSnippet, { description: row.original.description_en }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original, onSuccess }); + } + } + ]; +} + +// Mantener compatibilidad hacia atrás +export const columns = createColumns(); diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte new file mode 100644 index 00000000..10ef56e8 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/create-edit-dialog.svelte @@ -0,0 +1,239 @@ + + + + + + + {isEditing ? "Editar" : "Nuevo"} País + + + {isEditing + ? "Modifica los datos del país." + : "Completa los datos para crear un nuevo país."} + + + +
+ {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte new file mode 100644 index 00000000..1f5a9021 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/data-table-actions.svelte @@ -0,0 +1,66 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + Acciones + + Copiar ID + + + + Ver detalles + Editar + + Eliminar + + + + + + + diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte new file mode 100644 index 00000000..ef98de23 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/data-table.svelte @@ -0,0 +1,123 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más... +
+ {:else} +
+ Desplázate para cargar más +
+ {/if} +
+
+
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/delete-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/delete-dialog.svelte new file mode 100644 index 00000000..da7dbdd0 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/delete-dialog.svelte @@ -0,0 +1,116 @@ + + + + + + ¿Estás seguro? + +

Esta acción no se puede deshacer. Se eliminará permanentemente este país:

+ {#if item} +
+
+ Clave M3: + {item.m3_key} +
+
+ Clave MX: + {item.mex_key} +
+
+ Descripción: + {item.description_es} +
+
+ {/if} + {#if error} +
+ {error} +
+ {/if} +
+
+ + Cancelar + + {#if loading} + + + + + {/if} + Eliminar + + +
+
diff --git a/frontend/src/lib/components/dashboard/reference_data/countries/details-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/countries/details-dialog.svelte new file mode 100644 index 00000000..b13a4c14 --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/countries/details-dialog.svelte @@ -0,0 +1,79 @@ + + + + + + Detalles del País + + Información completa del país + + + + {#if item} +
+
+
+ Clave M3 + + {item.m3_key} + +
+ +
+ Clave MX + + {item.mex_key} + +
+ +
+ Clave AME + + {item.ame_key} + +
+
+ + +
+
+ Descripción en Español +

{item.description_es}

+
+ +
+ +
+
+ Descripción en Inglés +

{item.description_en}

+
+ +
+
+ {/if} + + + + +
+
diff --git a/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts b/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts new file mode 100644 index 00000000..3c8e022c --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/countries?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Countries] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Countries] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/countries/+page.svelte b/frontend/src/routes/dashboard/reference_data/countries/+page.svelte new file mode 100644 index 00000000..fd5ec3ef --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/countries/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Países

+

+ Gestiona los países disponibles en el sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Países + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte new file mode 100644 index 00000000..e69de29b From ab57c6cd79f2029ff2c6ddeb79a6ebd93276faa4 Mon Sep 17 00:00:00 2001 From: acazares Date: Sun, 2 Nov 2025 16:54:27 -0600 Subject: [PATCH 08/16] feat: Implement reference data management for states, transport modes, transport types, and valuation methods - Added server-side loading logic for states, transport modes, transport types, and valuation methods with pagination support. - Created Svelte components for displaying and managing states, transport modes, transport types, and valuation methods. - Implemented infinite scroll functionality for loading more data as the user scrolls. - Added error handling and user feedback for API interactions. - Included dialogs for creating and editing entries in each reference data category. --- .../dashboard/refrence_data/currency_types.ts | 72 ++++++ .../refrence_data/customs_sections.ts | 69 ++++++ .../refrence_data/customs_warehouses.ts | 77 ++++++ .../api/dashboard/refrence_data/incoterms.ts | 72 ++++++ .../dashboard/refrence_data/invoice_types.ts | 75 ++++++ .../dashboard/refrence_data/material_types.ts | 72 ++++++ .../refrence_data/payment_methods.ts | 69 ++++++ .../refrence_data/pedimento_codes.ts | 69 ++++++ .../refrence_data/pedimento_regimens.ts | 69 ++++++ .../api/dashboard/refrence_data/sectors.ts | 72 ++++++ .../lib/api/dashboard/refrence_data/states.ts | 75 ++++++ .../refrence_data/transport_modes.ts | 69 ++++++ .../refrence_data/transport_types.ts | 69 ++++++ .../refrence_data/valuation_methods.ts | 69 ++++++ .../reference_data/currency_types/columns.ts | 64 +++++ .../currency_types/create-edit-dialog.svelte | 206 ++++++++++++++++ .../currency_types/data-table-actions.svelte | 66 ++++++ .../currency_types/data-table.svelte | 123 ++++++++++ .../currency_types/delete-dialog.svelte | 116 +++++++++ .../currency_types/details-dialog.svelte | 65 +++++ .../customs_sections/columns.ts | 50 ++++ .../create-edit-dialog.svelte | 188 +++++++++++++++ .../data-table-actions.svelte | 66 ++++++ .../customs_sections/data-table.svelte | 123 ++++++++++ .../customs_sections/delete-dialog.svelte | 112 +++++++++ .../customs_sections/details-dialog.svelte | 57 +++++ .../customs_warehouses/columns.ts | 64 +++++ .../create-edit-dialog.svelte | 209 ++++++++++++++++ .../data-table-actions.svelte | 66 ++++++ .../customs_warehouses/data-table.svelte | 123 ++++++++++ .../customs_warehouses/delete-dialog.svelte | 116 +++++++++ .../customs_warehouses/details-dialog.svelte | 62 +++++ .../reference_data/incoterms/columns.ts | 64 +++++ .../incoterms/create-edit-dialog.svelte | 207 ++++++++++++++++ .../incoterms/data-table-actions.svelte | 66 ++++++ .../incoterms/data-table.svelte | 123 ++++++++++ .../incoterms/delete-dialog.svelte | 116 +++++++++ .../incoterms/details-dialog.svelte | 65 +++++ .../reference_data/invoice_types/columns.ts | 78 ++++++ .../invoice_types/create-edit-dialog.svelte | 222 +++++++++++++++++ .../invoice_types/data-table-actions.svelte | 66 ++++++ .../invoice_types/data-table.svelte | 123 ++++++++++ .../invoice_types/delete-dialog.svelte | 124 ++++++++++ .../invoice_types/details-dialog.svelte | 77 ++++++ .../reference_data/material_types/columns.ts | 64 +++++ .../material_types/create-edit-dialog.svelte | 206 ++++++++++++++++ .../material_types/data-table-actions.svelte | 66 ++++++ .../material_types/data-table.svelte | 123 ++++++++++ .../material_types/delete-dialog.svelte | 116 +++++++++ .../material_types/details-dialog.svelte | 65 +++++ .../reference_data/payment_methods/columns.ts | 50 ++++ .../payment_methods/create-edit-dialog.svelte | 186 +++++++++++++++ .../payment_methods/data-table-actions.svelte | 66 ++++++ .../payment_methods/data-table.svelte | 123 ++++++++++ .../payment_methods/delete-dialog.svelte | 112 +++++++++ .../payment_methods/details-dialog.svelte | 57 +++++ .../reference_data/pedimento_codes/columns.ts | 50 ++++ .../pedimento_codes/create-edit-dialog.svelte | 188 +++++++++++++++ .../pedimento_codes/data-table-actions.svelte | 66 ++++++ .../pedimento_codes/data-table.svelte | 123 ++++++++++ .../pedimento_codes/delete-dialog.svelte | 112 +++++++++ .../pedimento_codes/details-dialog.svelte | 57 +++++ .../pedimento_regimens/columns.ts | 50 ++++ .../create-edit-dialog.svelte | 186 +++++++++++++++ .../data-table-actions.svelte | 66 ++++++ .../pedimento_regimens/data-table.svelte | 123 ++++++++++ .../pedimento_regimens/delete-dialog.svelte | 112 +++++++++ .../pedimento_regimens/details-dialog.svelte | 57 +++++ .../reference_data/sectors/columns.ts | 67 ++++++ .../sectors/create-edit-dialog.svelte | 223 +++++++++++++++++ .../sectors/data-table-actions.svelte | 66 ++++++ .../reference_data/sectors/data-table.svelte | 123 ++++++++++ .../sectors/delete-dialog.svelte | 119 ++++++++++ .../sectors/details-dialog.svelte | 68 ++++++ .../reference_data/states/columns.ts | 84 +++++++ .../states/create-edit-dialog.svelte | 224 ++++++++++++++++++ .../states/data-table-actions.svelte | 66 ++++++ .../reference_data/states/data-table.svelte | 123 ++++++++++ .../states/delete-dialog.svelte | 124 ++++++++++ .../states/details-dialog.svelte | 85 +++++++ .../reference_data/transport_modes/columns.ts | 50 ++++ .../transport_modes/create-edit-dialog.svelte | 184 ++++++++++++++ .../transport_modes/data-table-actions.svelte | 66 ++++++ .../transport_modes/data-table.svelte | 123 ++++++++++ .../transport_modes/delete-dialog.svelte | 112 +++++++++ .../transport_modes/details-dialog.svelte | 57 +++++ .../reference_data/transport_types/columns.ts | 50 ++++ .../transport_types/create-edit-dialog.svelte | 184 ++++++++++++++ .../transport_types/data-table-actions.svelte | 66 ++++++ .../transport_types/data-table.svelte | 123 ++++++++++ .../transport_types/delete-dialog.svelte | 112 +++++++++ .../transport_types/details-dialog.svelte | 57 +++++ .../valuation_methods/columns.ts | 50 ++++ .../create-edit-dialog.svelte | 184 ++++++++++++++ .../data-table-actions.svelte | 66 ++++++ .../valuation_methods/data-table.svelte | 123 ++++++++++ .../valuation_methods/delete-dialog.svelte | 112 +++++++++ .../valuation_methods/details-dialog.svelte | 57 +++++ .../src/lib/components/sidebar/modules.ts | 4 +- .../src/lib/components/ui/badge/badge.svelte | 50 ++++ frontend/src/lib/components/ui/badge/index.ts | 2 + .../src/lib/components/ui/select/index.ts | 37 +++ .../ui/select/select-content.svelte | 40 ++++ .../ui/select/select-group-heading.svelte | 21 ++ .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 +++ .../components/ui/select/select-label.svelte | 20 ++ .../select/select-scroll-down-button.svelte | 20 ++ .../ui/select/select-scroll-up-button.svelte | 20 ++ .../ui/select/select-separator.svelte | 18 ++ .../ui/select/select-trigger.svelte | 29 +++ .../src/lib/components/ui/textarea/index.ts | 7 + .../components/ui/textarea/textarea.svelte | 23 ++ .../currency_types/+page.server.ts | 81 +++++++ .../currency_types/+page.svelte | 196 +++++++++++++++ .../customs_sections/+page.server.ts | 81 +++++++ .../customs_sections/+page.svelte | 196 +++++++++++++++ .../customs_warehouses/+page.server.ts | 81 +++++++ .../customs_warehouses/+page.svelte | 196 +++++++++++++++ .../reference_data/incoterms/+page.server.ts | 81 +++++++ .../reference_data/incoterms/+page.svelte | 196 +++++++++++++++ .../invoice_types/+page.server.ts | 81 +++++++ .../reference_data/invoice_types/+page.svelte | 196 +++++++++++++++ .../material_types/+page.server.ts | 81 +++++++ .../material_types/+page.svelte | 196 +++++++++++++++ .../payment_methods/+page.server.ts | 81 +++++++ .../payment_methods/+page.svelte | 196 +++++++++++++++ .../pedimento_codes/+page.server.ts | 81 +++++++ .../pedimento_codes/+page.svelte | 196 +++++++++++++++ .../pedimento_regimens/+page.server.ts | 81 +++++++ .../pedimento_regimens/+page.svelte | 196 +++++++++++++++ .../reference_data/sectors/+page.server.ts | 81 +++++++ .../reference_data/sectors/+page.svelte | 196 +++++++++++++++ .../reference_data/states/+page.server.ts | 81 +++++++ .../reference_data/states/+page.svelte | 196 +++++++++++++++ .../transport_modes/+page.server.ts | 81 +++++++ .../transport_modes/+page.svelte | 196 +++++++++++++++ .../transport_types/+page.server.ts | 81 +++++++ .../transport_types/+page.svelte | 196 +++++++++++++++ .../valuation_methods/+page.server.ts | 81 +++++++ .../valuation_methods/+page.svelte | 196 +++++++++++++++ 141 files changed, 13989 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/api/dashboard/refrence_data/currency_types.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/incoterms.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/material_types.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/sectors.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/states.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/transport_types.ts create mode 100644 frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/currency_types/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_sections/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/customs_warehouses/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/incoterms/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/invoice_types/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/material_types/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/payment_methods/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_codes/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/pedimento_regimens/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/sectors/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/states/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_modes/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/transport_types/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/columns.ts create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/reference_data/valuation_methods/details-dialog.svelte create mode 100644 frontend/src/lib/components/ui/badge/badge.svelte create mode 100644 frontend/src/lib/components/ui/badge/index.ts create mode 100644 frontend/src/lib/components/ui/select/index.ts create mode 100644 frontend/src/lib/components/ui/select/select-content.svelte create mode 100644 frontend/src/lib/components/ui/select/select-group-heading.svelte create mode 100644 frontend/src/lib/components/ui/select/select-group.svelte create mode 100644 frontend/src/lib/components/ui/select/select-item.svelte create mode 100644 frontend/src/lib/components/ui/select/select-label.svelte create mode 100644 frontend/src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 frontend/src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 frontend/src/lib/components/ui/select/select-separator.svelte create mode 100644 frontend/src/lib/components/ui/select/select-trigger.svelte create mode 100644 frontend/src/lib/components/ui/textarea/index.ts create mode 100644 frontend/src/lib/components/ui/textarea/textarea.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/material_types/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/sectors/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/states/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/states/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte create mode 100644 frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts create mode 100644 frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte diff --git a/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts new file mode 100644 index 00000000..68fdec93 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts @@ -0,0 +1,72 @@ +/** + * API Client para Currency Types + * Gestiona las operaciones CRUD para los tipos de moneda + */ +import { api } from '$lib/api'; + +export interface CurrencyType { + code: string; + currency_name: string; + country_description: string; +} + +export interface CurrencyTypeListResponse { + items: CurrencyType[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateCurrencyTypeData { + code: string; + currency_name: string; + country_description: string; +} + +export interface UpdateCurrencyTypeData { + code?: string; + currency_name?: string; + country_description?: string; +} + +/** + * API para Currency Types + */ +export const currencyTypesApi = { + /** + * Lista todos los tipos de moneda con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/currency-types?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un tipo de moneda por código + * @param code - Código del tipo de moneda + */ + get: (code: string) => api.get(`/v1/currency-types/${code}`), + + /** + * Crea un nuevo tipo de moneda + * @param data - Datos del tipo de moneda a crear + */ + create: (data: CreateCurrencyTypeData) => + api.post('/v1/currency-types', data), + + /** + * Actualiza un tipo de moneda existente + * @param code - Código del tipo de moneda a actualizar + * @param data - Datos a actualizar + */ + update: (code: string, data: UpdateCurrencyTypeData) => + api.put(`/v1/currency-types/${code}`, data), + + /** + * Elimina un tipo de moneda + * @param code - Código del tipo de moneda a eliminar + */ + delete: (code: string) => api.delete(`/v1/currency-types/${code}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts new file mode 100644 index 00000000..e67620ff --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts @@ -0,0 +1,69 @@ +/** + * API Client para Customs Sections + * Gestiona las operaciones CRUD para las secciones aduaneras + */ +import { api } from '$lib/api'; + +export interface CustomsSection { + customs_code: string; + section_name: string; +} + +export interface CustomsSectionListResponse { + items: CustomsSection[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateCustomsSectionData { + customs_code: string; + section_name: string; +} + +export interface UpdateCustomsSectionData { + customs_code?: string; + section_name?: string; +} + +/** + * API para Customs Sections + */ +export const customsSectionsApi = { + /** + * Lista todas las secciones aduaneras con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/customs-sections?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene una sección aduanera por código + * @param customs_code - Código de la sección aduanera + */ + get: (customs_code: string) => api.get(`/v1/customs-sections/${customs_code}`), + + /** + * Crea una nueva sección aduanera + * @param data - Datos de la sección aduanera a crear + */ + create: (data: CreateCustomsSectionData) => + api.post('/v1/customs-sections', data), + + /** + * Actualiza una sección aduanera existente + * @param customs_code - Código de la sección aduanera a actualizar + * @param data - Datos a actualizar + */ + update: (customs_code: string, data: UpdateCustomsSectionData) => + api.put(`/v1/customs-sections/${customs_code}`, data), + + /** + * Elimina una sección aduanera + * @param customs_code - Código de la sección aduanera a eliminar + */ + delete: (customs_code: string) => api.delete(`/v1/customs-sections/${customs_code}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts new file mode 100644 index 00000000..3ef02d19 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts @@ -0,0 +1,77 @@ +/** + * API Client para Customs Warehouses + * Gestiona las operaciones CRUD para los recintos fiscalizados + */ +import { api } from '$lib/api'; + +export interface CustomsWarehouse { + key: string; + customs: string; + fiscalized_warehouse: string; +} + +export interface CustomsWarehouseListResponse { + items: CustomsWarehouse[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateCustomsWarehouseData { + key: string; + customs: string; + fiscalized_warehouse: string; +} + +export interface UpdateCustomsWarehouseData { + key?: string; + customs?: string; + fiscalized_warehouse?: string; +} + +/** + * API para Customs Warehouses + */ +export const customsWarehousesApi = { + /** + * Lista todos los recintos fiscalizados con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/customs-warehouses?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un recinto fiscalizado por clave compuesta (key + customs) + * @param key - Clave del recinto + * @param customs - Aduana asociada + */ + get: (key: string, customs: string) => + api.get(`/v1/customs-warehouses/${key}/${customs}`), + + /** + * Crea un nuevo recinto fiscalizado + * @param data - Datos del recinto fiscalizado a crear + */ + create: (data: CreateCustomsWarehouseData) => + api.post('/v1/customs-warehouses', data), + + /** + * Actualiza un recinto fiscalizado existente + * @param key - Clave del recinto a actualizar + * @param customs - Aduana asociada + * @param data - Datos a actualizar + */ + update: (key: string, customs: string, data: UpdateCustomsWarehouseData) => + api.put(`/v1/customs-warehouses/${key}/${customs}`, data), + + /** + * Elimina un recinto fiscalizado + * @param key - Clave del recinto a eliminar + * @param customs - Aduana asociada + */ + delete: (key: string, customs: string) => + api.delete(`/v1/customs-warehouses/${key}/${customs}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts new file mode 100644 index 00000000..ece31dd7 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts @@ -0,0 +1,72 @@ +/** + * API Client para Incoterms + * Gestiona las operaciones CRUD para los términos internacionales de comercio + */ +import { api } from '$lib/api'; + +export interface Incoterm { + code: string; + description_es: string; + description_en: string; +} + +export interface IncotermListResponse { + items: Incoterm[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateIncotermData { + code: string; + description_es: string; + description_en: string; +} + +export interface UpdateIncotermData { + code?: string; + description_es?: string; + description_en?: string; +} + +/** + * API para Incoterms + */ +export const incotermsApi = { + /** + * Lista todos los incoterms con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/incoterms?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un incoterm por código + * @param code - Código del incoterm + */ + get: (code: string) => api.get(`/v1/incoterms/${code}`), + + /** + * Crea un nuevo incoterm + * @param data - Datos del incoterm a crear + */ + create: (data: CreateIncotermData) => + api.post('/v1/incoterms', data), + + /** + * Actualiza un incoterm existente + * @param code - Código del incoterm a actualizar + * @param data - Datos a actualizar + */ + update: (code: string, data: UpdateIncotermData) => + api.put(`/v1/incoterms/${code}`, data), + + /** + * Elimina un incoterm + * @param code - Código del incoterm a eliminar + */ + delete: (code: string) => api.delete(`/v1/incoterms/${code}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts new file mode 100644 index 00000000..8cfbfd46 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts @@ -0,0 +1,75 @@ +/** + * API Client para Invoice Types + * Gestiona las operaciones CRUD para los tipos de factura + */ +import { api } from '$lib/api'; + +export interface InvoiceType { + key: string; + description: string; + note?: string; + type?: string; +} + +export interface InvoiceTypeListResponse { + items: InvoiceType[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateInvoiceTypeData { + key: string; + description: string; + note?: string; + type?: string; +} + +export interface UpdateInvoiceTypeData { + key?: string; + description?: string; + note?: string; + type?: string; +} + +/** + * API para Invoice Types + */ +export const invoiceTypesApi = { + /** + * Lista todos los tipos de factura con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/invoice-types?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un tipo de factura por key + * @param key - Clave del tipo de factura + */ + get: (key: string) => api.get(`/v1/invoice-types/${key}`), + + /** + * Crea un nuevo tipo de factura + * @param data - Datos del tipo de factura a crear + */ + create: (data: CreateInvoiceTypeData) => + api.post('/v1/invoice-types', data), + + /** + * Actualiza un tipo de factura existente + * @param key - Clave del tipo de factura a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdateInvoiceTypeData) => + api.put(`/v1/invoice-types/${key}`, data), + + /** + * Elimina un tipo de factura + * @param key - Clave del tipo de factura a eliminar + */ + delete: (key: string) => api.delete(`/v1/invoice-types/${key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/material_types.ts b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts new file mode 100644 index 00000000..d3156e5f --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts @@ -0,0 +1,72 @@ +/** + * API Client para Material Types + * Gestiona las operaciones CRUD para los tipos de material + */ +import { api } from '$lib/api'; + +export interface MaterialType { + key: string; + type: string; + description: string; +} + +export interface MaterialTypeListResponse { + items: MaterialType[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateMaterialTypeData { + key: string; + type: string; + description: string; +} + +export interface UpdateMaterialTypeData { + key?: string; + type?: string; + description?: string; +} + +/** + * API para Material Types + */ +export const materialTypesApi = { + /** + * Lista todos los tipos de material con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/material-types?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un tipo de material por key + * @param key - Clave del tipo de material + */ + get: (key: string) => api.get(`/v1/material-types/${key}`), + + /** + * Crea un nuevo tipo de material + * @param data - Datos del tipo de material a crear + */ + create: (data: CreateMaterialTypeData) => + api.post('/v1/material-types', data), + + /** + * Actualiza un tipo de material existente + * @param key - Clave del tipo de material a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdateMaterialTypeData) => + api.put(`/v1/material-types/${key}`, data), + + /** + * Elimina un tipo de material + * @param key - Clave del tipo de material a eliminar + */ + delete: (key: string) => api.delete(`/v1/material-types/${key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts new file mode 100644 index 00000000..5706765b --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts @@ -0,0 +1,69 @@ +/** + * API Client para Payment Methods + * Gestiona las operaciones CRUD para los métodos de pago + */ +import { api } from '$lib/api'; + +export interface PaymentMethod { + key: string; + description: string; +} + +export interface PaymentMethodListResponse { + items: PaymentMethod[]; + total: number; + page: number; + page_size: number; +} + +export interface CreatePaymentMethodData { + key: string; + description: string; +} + +export interface UpdatePaymentMethodData { + key?: string; + description?: string; +} + +/** + * API para Payment Methods + */ +export const paymentMethodsApi = { + /** + * Lista todos los métodos de pago con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/payment-methods?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un método de pago por key + * @param key - Clave del método de pago + */ + get: (key: string) => api.get(`/v1/payment-methods/${key}`), + + /** + * Crea un nuevo método de pago + * @param data - Datos del método de pago a crear + */ + create: (data: CreatePaymentMethodData) => + api.post('/v1/payment-methods', data), + + /** + * Actualiza un método de pago existente + * @param key - Clave del método de pago a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdatePaymentMethodData) => + api.put(`/v1/payment-methods/${key}`, data), + + /** + * Elimina un método de pago + * @param key - Clave del método de pago a eliminar + */ + delete: (key: string) => api.delete(`/v1/payment-methods/${key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts new file mode 100644 index 00000000..c50c3d59 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts @@ -0,0 +1,69 @@ +/** + * API Client para Pedimento Codes + * Gestiona las operaciones CRUD para las claves de pedimento + */ +import { api } from '$lib/api'; + +export interface PedimentoCode { + code: string; + description: string; +} + +export interface PedimentoCodeListResponse { + items: PedimentoCode[]; + total: number; + page: number; + page_size: number; +} + +export interface CreatePedimentoCodeData { + code: string; + description: string; +} + +export interface UpdatePedimentoCodeData { + code?: string; + description?: string; +} + +/** + * API para Pedimento Codes + */ +export const pedimentoCodesApi = { + /** + * Lista todas las claves de pedimento con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/pedimento-codes?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene una clave de pedimento por code + * @param code - Código de la clave de pedimento + */ + get: (code: string) => api.get(`/v1/pedimento-codes/${code}`), + + /** + * Crea una nueva clave de pedimento + * @param data - Datos de la clave de pedimento a crear + */ + create: (data: CreatePedimentoCodeData) => + api.post('/v1/pedimento-codes', data), + + /** + * Actualiza una clave de pedimento existente + * @param code - Código de la clave de pedimento a actualizar + * @param data - Datos a actualizar + */ + update: (code: string, data: UpdatePedimentoCodeData) => + api.put(`/v1/pedimento-codes/${code}`, data), + + /** + * Elimina una clave de pedimento + * @param code - Código de la clave de pedimento a eliminar + */ + delete: (code: string) => api.delete(`/v1/pedimento-codes/${code}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts new file mode 100644 index 00000000..0a3dbe0b --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts @@ -0,0 +1,69 @@ +/** + * API Client para Pedimento Regimens + * Gestiona las operaciones CRUD para los regímenes de pedimento + */ +import { api } from '$lib/api'; + +export interface PedimentoRegimen { + code: string; + description: string; +} + +export interface PedimentoRegimenListResponse { + items: PedimentoRegimen[]; + total: number; + page: number; + page_size: number; +} + +export interface CreatePedimentoRegimenData { + code: string; + description: string; +} + +export interface UpdatePedimentoRegimenData { + code?: string; + description?: string; +} + +/** + * API para Pedimento Regimens + */ +export const pedimentoRegimensApi = { + /** + * Lista todos los regímenes de pedimento con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/pedimento-regimens?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un régimen de pedimento por code + * @param code - Código del régimen de pedimento + */ + get: (code: string) => api.get(`/v1/pedimento-regimens/${code}`), + + /** + * Crea un nuevo régimen de pedimento + * @param data - Datos del régimen de pedimento a crear + */ + create: (data: CreatePedimentoRegimenData) => + api.post('/v1/pedimento-regimens', data), + + /** + * Actualiza un régimen de pedimento existente + * @param code - Código del régimen de pedimento a actualizar + * @param data - Datos a actualizar + */ + update: (code: string, data: UpdatePedimentoRegimenData) => + api.put(`/v1/pedimento-regimens/${code}`, data), + + /** + * Elimina un régimen de pedimento + * @param code - Código del régimen de pedimento a eliminar + */ + delete: (code: string) => api.delete(`/v1/pedimento-regimens/${code}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/sectors.ts b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts new file mode 100644 index 00000000..01013760 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts @@ -0,0 +1,72 @@ +/** + * API Client para Sectors + * Gestiona las operaciones CRUD para los sectores + */ +import { api } from '$lib/api'; + +export interface Sector { + key: string; + description: string; + authorized: number; +} + +export interface SectorListResponse { + items: Sector[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateSectorData { + key: string; + description: string; + authorized: number; +} + +export interface UpdateSectorData { + key?: string; + description?: string; + authorized?: number; +} + +/** + * API para Sectors + */ +export const sectorsApi = { + /** + * Lista todos los sectores con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/sectors?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un sector por key + * @param key - Clave del sector + */ + get: (key: string) => api.get(`/v1/sectors/${key}`), + + /** + * Crea un nuevo sector + * @param data - Datos del sector a crear + */ + create: (data: CreateSectorData) => + api.post('/v1/sectors', data), + + /** + * Actualiza un sector existente + * @param key - Clave del sector a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdateSectorData) => + api.put(`/v1/sectors/${key}`, data), + + /** + * Elimina un sector + * @param key - Clave del sector a eliminar + */ + delete: (key: string) => api.delete(`/v1/sectors/${key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/states.ts b/frontend/src/lib/api/dashboard/refrence_data/states.ts new file mode 100644 index 00000000..339a65c0 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/states.ts @@ -0,0 +1,75 @@ +/** + * API Client para States + * Gestiona las operaciones CRUD para los estados + */ +import { api } from '$lib/api'; + +export interface State { + m3_key: string; + description: string; + mex_key?: string | null; + ame_key?: string | null; +} + +export interface StateListResponse { + items: State[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateStateData { + m3_key: string; + description: string; + mex_key?: string | null; + ame_key?: string | null; +} + +export interface UpdateStateData { + m3_key?: string; + description?: string; + mex_key?: string | null; + ame_key?: string | null; +} + +/** + * API para States + */ +export const statesApi = { + /** + * Lista todos los estados con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/states?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un estado por m3_key + * @param m3Key - Clave M3 del estado + */ + get: (m3Key: string) => api.get(`/v1/states/${m3Key}`), + + /** + * Crea un nuevo estado + * @param data - Datos del estado a crear + */ + create: (data: CreateStateData) => + api.post('/v1/states', data), + + /** + * Actualiza un estado existente + * @param m3Key - Clave M3 del estado a actualizar + * @param data - Datos a actualizar + */ + update: (m3Key: string, data: UpdateStateData) => + api.put(`/v1/states/${m3Key}`, data), + + /** + * Elimina un estado + * @param m3Key - Clave M3 del estado a eliminar + */ + delete: (m3Key: string) => api.delete(`/v1/states/${m3Key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts new file mode 100644 index 00000000..277df5e8 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts @@ -0,0 +1,69 @@ +/** + * API Client para Transport Modes + * Gestiona las operaciones CRUD para los modos de transporte + */ +import { api } from '$lib/api'; + +export interface TransportMode { + key: string; + name: string; +} + +export interface TransportModeListResponse { + items: TransportMode[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateTransportModeData { + key: string; + name: string; +} + +export interface UpdateTransportModeData { + key?: string; + name?: string; +} + +/** + * API para Transport Modes + */ +export const transportModesApi = { + /** + * Lista todos los modos de transporte con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/transport-modes?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un modo de transporte por key + * @param key - Clave del modo de transporte + */ + get: (key: string) => api.get(`/v1/transport-modes/${key}`), + + /** + * Crea un nuevo modo de transporte + * @param data - Datos del modo de transporte a crear + */ + create: (data: CreateTransportModeData) => + api.post('/v1/transport-modes', data), + + /** + * Actualiza un modo de transporte existente + * @param key - Clave del modo de transporte a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdateTransportModeData) => + api.put(`/v1/transport-modes/${key}`, data), + + /** + * Elimina un modo de transporte + * @param key - Clave del modo de transporte a eliminar + */ + delete: (key: string) => api.delete(`/v1/transport-modes/${key}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts new file mode 100644 index 00000000..5b09910b --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts @@ -0,0 +1,69 @@ +/** + * API Client para Transport Types + * Gestiona las operaciones CRUD para los tipos de transporte + */ +import { api } from '$lib/api'; + +export interface TransportType { + transport_code: string; + description: string; +} + +export interface TransportTypeListResponse { + items: TransportType[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateTransportTypeData { + transport_code: string; + description: string; +} + +export interface UpdateTransportTypeData { + transport_code?: string; + description?: string; +} + +/** + * API para Transport Types + */ +export const transportTypesApi = { + /** + * Lista todos los tipos de transporte con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/transport-types?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un tipo de transporte por transport_code + * @param transportCode - Código del tipo de transporte + */ + get: (transportCode: string) => api.get(`/v1/transport-types/${transportCode}`), + + /** + * Crea un nuevo tipo de transporte + * @param data - Datos del tipo de transporte a crear + */ + create: (data: CreateTransportTypeData) => + api.post('/v1/transport-types', data), + + /** + * Actualiza un tipo de transporte existente + * @param transportCode - Código del tipo de transporte a actualizar + * @param data - Datos a actualizar + */ + update: (transportCode: string, data: UpdateTransportTypeData) => + api.put(`/v1/transport-types/${transportCode}`, data), + + /** + * Elimina un tipo de transporte + * @param transportCode - Código del tipo de transporte a eliminar + */ + delete: (transportCode: string) => api.delete(`/v1/transport-types/${transportCode}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts new file mode 100644 index 00000000..54f729ad --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts @@ -0,0 +1,69 @@ +/** + * API Client para Valuation Methods + * Gestiona las operaciones CRUD para los métodos de valoración + */ +import { api } from '$lib/api'; + +export interface ValuationMethod { + key: string; + description: string; +} + +export interface ValuationMethodListResponse { + items: ValuationMethod[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateValuationMethodData { + key: string; + description: string; +} + +export interface UpdateValuationMethodData { + key?: string; + description?: string; +} + +/** + * API para Valuation Methods + */ +export const valuationMethodsApi = { + /** + * Lista todos los métodos de valoración con paginación + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + */ + list: (page = 1, pageSize = 50) => + api.get( + `/v1/valuation-methods?page=${page}&page_size=${pageSize}` + ), + + /** + * Obtiene un método de valoración por key + * @param key - Clave del método de valoración + */ + get: (key: string) => api.get(`/v1/valuation-methods/${key}`), + + /** + * Crea un nuevo método de valoración + * @param data - Datos del método de valoración a crear + */ + create: (data: CreateValuationMethodData) => + api.post('/v1/valuation-methods', data), + + /** + * Actualiza un método de valoración existente + * @param key - Clave del método de valoración a actualizar + * @param data - Datos a actualizar + */ + update: (key: string, data: UpdateValuationMethodData) => + api.put(`/v1/valuation-methods/${key}`, data), + + /** + * Elimina un método de valoración + * @param key - Clave del método de valoración a eliminar + */ + delete: (key: string) => api.delete(`/v1/valuation-methods/${key}`) +}; diff --git a/frontend/src/lib/components/dashboard/reference_data/currency_types/columns.ts b/frontend/src/lib/components/dashboard/reference_data/currency_types/columns.ts new file mode 100644 index 00000000..eb7ab8be --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/currency_types/columns.ts @@ -0,0 +1,64 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; + +export type CurrencyType = { + code: string; + currency_name: string; + country_description: string; +}; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: "code", + header: "Código", + cell: ({ row }) => { + const codeSnippet = createRawSnippet<[{ code: string }]>((getCode) => { + const { code } = getCode(); + return { + render: () => + `${code}` + }; + }); + return renderSnippet(codeSnippet, { code: row.original.code }); + } + }, + { + accessorKey: "currency_name", + header: "Nombre de Moneda", + cell: ({ row }) => { + const nameSnippet = createRawSnippet<[{ name: string }]>((getName) => { + const { name } = getName(); + return { + render: () => `
${name}
` + }; + }); + return renderSnippet(nameSnippet, { name: row.original.currency_name }); + } + }, + { + accessorKey: "country_description", + header: "País / Descripción", + cell: ({ row }) => { + const descSnippet = createRawSnippet<[{ description: string }]>((getDesc) => { + const { description } = getDesc(); + return { + render: () => `
${description}
` + }; + }); + return renderSnippet(descSnippet, { description: row.original.country_description }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original, onSuccess }); + } + } + ]; +} + +// Mantener compatibilidad hacia atrás +export const columns = createColumns(); diff --git a/frontend/src/lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte new file mode 100644 index 00000000..0dd6c3cf --- /dev/null +++ b/frontend/src/lib/components/dashboard/reference_data/currency_types/create-edit-dialog.svelte @@ -0,0 +1,206 @@ + + + + + + + {isEditing ? "Editar" : "Nuevo"} Tipo de Moneda + + + {isEditing + ? "Modifica los datos del tipo de moneda." + : "Completa los datos para crear un nuevo tipo de moneda."} + + + +
+ {#if error} +
+ {error} +
+ {/if} + +
+ + +

Código ISO de 3 caracteres

+
+ +
+ + +
+ +
+ + diff --git a/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts new file mode 100644 index 00000000..ac813d0b --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/currency-types?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [CurrencyTypes] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [CurrencyTypes] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte index e69de29b..f47264c8 100644 --- a/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte +++ b/frontend/src/routes/dashboard/reference_data/currency_types/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Tipos de Moneda

+

+ Gestiona los tipos de moneda disponibles en el sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Tipos de Moneda + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts new file mode 100644 index 00000000..fecf64d7 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/customs-sections?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [CustomsSections] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [CustomsSections] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte new file mode 100644 index 00000000..a45491b7 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Secciones Aduanales

+

+ Gestiona las secciones aduanales del sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Secciones Aduanales + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts new file mode 100644 index 00000000..cb9e87b3 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/customs-warehouses?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [CustomsWarehouses] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [CustomsWarehouses] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte new file mode 100644 index 00000000..82b7c178 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Recintos Fiscalizados

+

+ Gestiona los recintos fiscalizados del sistema aduanal +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Recintos Fiscalizados + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts new file mode 100644 index 00000000..d5b9421b --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/incoterms?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Incoterms] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Incoterms] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte b/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte new file mode 100644 index 00000000..217d1c6c --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/incoterms/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Incoterms

+

+ Gestiona los términos internacionales de comercio (International Commercial Terms) +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Incoterms + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts new file mode 100644 index 00000000..f3b965a7 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/invoice-types?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Invoice Types] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Invoice Types] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte new file mode 100644 index 00000000..1ba35154 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Tipos de Factura

+

+ Gestiona los tipos de facturas del sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Tipos de Factura + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts new file mode 100644 index 00000000..f55dc6b3 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/material-types?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Material Types] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Material Types] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte new file mode 100644 index 00000000..66589b3f --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/material_types/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Tipos de Material

+

+ Gestiona los tipos de materiales del sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Tipos de Material + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts new file mode 100644 index 00000000..5cabd2a5 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/payment-methods?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Payment Methods] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Payment Methods] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte new file mode 100644 index 00000000..860a48f3 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Métodos de Pago

+

+ Gestiona las formas de pago disponibles en el sistema +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Métodos de Pago + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts new file mode 100644 index 00000000..d4cfdcc6 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/pedimento-codes?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Pedimento Codes] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Pedimento Codes] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte new file mode 100644 index 00000000..dec3a448 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Claves de Pedimento

+

+ Gestiona las claves de pedimento del sistema aduanero +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Claves de Pedimento + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts new file mode 100644 index 00000000..842031d1 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/pedimento-regimens?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Pedimento Regimens] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Pedimento Regimens] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte new file mode 100644 index 00000000..39d1a31f --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Regímenes de Pedimento

+

+ Gestiona los regímenes aduaneros de pedimento +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Regímenes de Pedimento + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts b/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts new file mode 100644 index 00000000..fbed6e9e --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/sectors?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Sectors] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Sectors] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte b/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte new file mode 100644 index 00000000..3621eb21 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/sectors/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Sectores

+

+ Gestiona los sectores económicos +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Sectores + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/states/+page.server.ts b/frontend/src/routes/dashboard/reference_data/states/+page.server.ts new file mode 100644 index 00000000..0d62247b --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/states/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/states?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [States] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [States] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/states/+page.svelte b/frontend/src/routes/dashboard/reference_data/states/+page.svelte new file mode 100644 index 00000000..b164088c --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/states/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Estados

+

+ Gestiona los estados y sus claves de identificación +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Estados + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts new file mode 100644 index 00000000..68aa9999 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/transport-modes?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Transport Modes] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Transport Modes] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte new file mode 100644 index 00000000..774372d8 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Modos de Transporte

+

+ Gestiona los modos de transporte disponibles +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Modos de Transporte + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts new file mode 100644 index 00000000..2704feed --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/transport-types?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Transport Types] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Transport Types] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte b/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte new file mode 100644 index 00000000..28c96469 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/transport_types/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Tipos de Transporte

+

+ Gestiona los tipos de transporte según código SAT +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Tipos de Transporte + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts new file mode 100644 index 00000000..def89f09 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts @@ -0,0 +1,81 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + await parent(); + + const token = cookies.get('access_token'); + + if (!token) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch( + `${baseUrl}v1/valuation-methods?page=${page}&page_size=${pageSize}`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Valuation Methods] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null + }; + } catch (error) { + console.error('📊 [Valuation Methods] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50 + }; + } +}; diff --git a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte new file mode 100644 index 00000000..81e18284 --- /dev/null +++ b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.svelte @@ -0,0 +1,196 @@ + + +
+ +
+
+

Métodos de Valoración

+

+ Gestiona los métodos de valoración aduanera +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Métodos de Valoración + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + From f886498bacba614542030f8096a39bd987b6d3e7 Mon Sep 17 00:00:00 2001 From: acazares Date: Tue, 4 Nov 2025 16:56:51 -0600 Subject: [PATCH 09/16] feat(nav-user): add language toggle functionality to user menu --- .../src/lib/components/sidebar/nav-user.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/lib/components/sidebar/nav-user.svelte b/frontend/src/lib/components/sidebar/nav-user.svelte index 5ee9452b..a29971cb 100644 --- a/frontend/src/lib/components/sidebar/nav-user.svelte +++ b/frontend/src/lib/components/sidebar/nav-user.svelte @@ -9,14 +9,26 @@ import CreditCardIcon from "@lucide/svelte/icons/credit-card"; import LogOutIcon from "@lucide/svelte/icons/log-out"; import SparklesIcon from "@lucide/svelte/icons/sparkles"; + import LanguagesIcon from "@lucide/svelte/icons/languages"; import { logout } from "$lib/auth"; + import { setLocale, locales } from "$lib/paraglide/runtime"; + import { page } from "$app/state"; let { user }: { user: { name: string; email: string; avatar: string } } = $props(); const sidebar = useSidebar(); + // Estado reactivo del idioma actual + let currentLocale = $derived(page.data.locale || 'en'); + async function handleLogout() { await logout(); } + + function toggleLanguage() { + // Alternar entre 'en' y 'es' + const newLocale = currentLocale === 'en' ? 'es' : 'en'; + setLocale(newLocale); + } @@ -82,6 +94,11 @@ + + + Language: {currentLocale.toUpperCase()} + + Log out From dd566f408bc675da0a7b0d88b4300ce17fa6e48f Mon Sep 17 00:00:00 2001 From: acazares Date: Tue, 4 Nov 2025 21:40:48 -0600 Subject: [PATCH 10/16] Refactor sidebar navigation and update translations - Updated sidebar navigation titles and URLs to use new reference data keys for consistency. - Added additional items to the general catalogs section in the sidebar. - Changed the user avatar fallback text from "CN" to "AS" for better representation. --- frontend/.gitignore | 1 + frontend/README.md | 3 + frontend/messages/en.json | 105 ++++++--- frontend/messages/es.json | 110 +++++++--- .../cache/plugins/2sy648wh9sugi | 1 - .../project.inlang/cache/plugins/ygx0uiahq6uw | 16 -- .../src/lib/components/sidebar/modules.ts | 200 +++++++++++++++--- .../lib/components/sidebar/nav-user.svelte | 23 +- 8 files changed, 340 insertions(+), 119 deletions(-) delete mode 100644 frontend/project.inlang/cache/plugins/2sy648wh9sugi delete mode 100644 frontend/project.inlang/cache/plugins/ygx0uiahq6uw diff --git a/frontend/.gitignore b/frontend/.gitignore index 11ad1db9..96a2ef4a 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -25,3 +25,4 @@ vite.config.ts.timestamp-* # Paraglide src/lib/paraglide +frontend/project.inlang/cache/ \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 75842c40..47da320e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -12,6 +12,9 @@ npx sv create # create a new project in my-app npx sv create my-app + +# compile paraglide +cd frontend && sudo rm -rf src/lib/paraglide && pnpm paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide ``` ## Developing diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f17f8b15..f474d318 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -2,35 +2,80 @@ "$schema": "https://inlang.com/schema/inlang-message-format", "hello_world": "Hello, {name} from en!", "sidebar": { - "catalogos_fijos": "Fixed Catalogs", - "codigos_pedimento_regimen": "Pedimento and Regime Codes", - "contenedores": "Containers", - "paises": "Countries", - "tipos_moneda": "Currency Types", - "secciones_aduanas": "Customs Sections", - "recintos": "Customs Warehouses", - "incoterms": "Incoterms", - "tipos_factura": "Invoice Types", - "tipos_material": "Material Types", - "metodos_pago": "Payment Methods", - "codigos_pedimento": "Pedimento Codes", - "regimenes_pedimentos": "Pedimento Regimes", - "sectores": "Sectors", - "estados": "States", - "metodos_transporte": "Transportation Modes", - "tipos_transporte": "Transportation Types", - "metodos_valoracion": "Valuation Methods", - "inventarios": "Inventories", - "gestionar_inventarios": "Manage Inventories", - "reportes": "Reports", - "pedimentos": "Pedimentos", - "nuevo_pedimento": "New Pedimento", - "consultar": "Search", - "historial": "History", - "configuracion": "Settings", - "general": "General", - "licencia": "License", - "usuarios": "Users", - "ayuda": "Help" + "reference_data": { + "title": "Fixed Catalogs", + "codes_pedimento_regimen": "Pedimento and Regime Codes", + "containers": "Containers", + "countries": "Countries", + "currency_types": "Currency Types", + "customs_sections": "Customs Sections", + "customs_warehouses": "Customs Warehouses", + "incoterms": "Incoterms", + "invoice_types": "Invoice Types", + "material_types": "Material Types", + "payment_methods": "Payment Methods", + "pedimento_codes": "Pedimento Codes", + "pedimento_regimes": "Pedimento Regimes", + "sectors": "Sectors", + "states": "States", + "transportation_modes": "Transportation Modes", + "transportation_types": "Transportation Types", + "valuation_methods": "Valuation Methods", + "configuracion": "Settings", + "general": "General", + "licencia": "License", + "usuarios": "Users", + "ayuda": "Help" + }, + "general_catalogs": { + "title": "General Catalogs", + "company_information": "Company Information", + "packages": "Packages", + "concepts": "Concepts", + "classification": "Classification", + "identifiers": "Identifiers", + "incoterms": "Incoterms", + "inpc": "I.N.P.C", + "fixed_legends": "Fixed Legends", + "seals": "Seals", + "valuation_methods": "Valuation Methods", + "countries": "Countries", + "ports": "Ports", + "unit_measures": "Unit Measures", + "um_customs_mex": "U.M. Customs Mexico", + "um_customs_ame": "U.M. Customs America", + "um_ace": "U.M. ACE", + "um_oma": "U.M. OMA", + "conversions": "Conversions", + "equivalences": "Equivalences", + "exchange_rates": "Exchange Rates", + "currency_types": "Currency Types", + "multi_currency": "Multi Currency", + "invoice_types": "Invoice Types", + "electronic_signatures": "Electronic Signatures", + "billing_errors": "Billing Errors", + "customs_warehouses": "Customs Warehouses", + "locations": "Locations", + "doda": "DODA", + "packing_list": "Packing List", + "prevalidators": "Prevalidators", + "electronic_notices": "Electronic Notices", + "back_flush": "Back Flush", + "crossing_notice": "Crossing Notice" + }, + "pedimentos": { + "title": "Pedimentos", + "pedimento_management": "Pedimento Management", + "pedimento_codes": "Pedimento Codes", + "customs_regimes": "Customs Regimes", + "payment_methods": "Payment Methods", + "customs_sections": "Customs Sections", + "anexo_22_app_31": "Anexo 22 App 3" + }, + "nav_user": { + "profile": "Profile", + "settings": "Settings", + "logout": "Logout" + } } } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 4885e8aa..e879f1fe 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,36 +1,76 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from es!", - "sidebar": { - "catalogos_fijos": "Catálogos Fijos", - "codigos_pedimento_regimen": "Códigos de Pedimento y Régimen", - "contenedores": "Contenedores", - "paises": "Países", - "tipos_moneda": "Tipos de moneda", - "secciones_aduanas": "Secciones de aduanas", - "recintos": "Recintos", - "incoterms": "Incoterms", - "tipos_factura": "Tipos de factura", - "tipos_material": "Tipos de material", - "metodos_pago": "Métodos de pago", - "codigos_pedimento": "Códigos de pedimento", - "regimenes_pedimentos": "Regímenes de pedimentos", - "sectores": "Sectores", - "estados": "Estados", - "metodos_transporte": "Métodos de transporte", - "tipos_transporte": "Tipos de transporte", - "metodos_valoracion": "Métodos de valoración", - "inventarios": "Inventarios", - "gestionar_inventarios": "Gestionar Inventarios", - "reportes": "Reportes", - "pedimentos": "Pedimentos", - "nuevo_pedimento": "Nuevo Pedimento", - "consultar": "Consultar", - "historial": "Historial", - "configuracion": "Configuración", - "general": "General", - "licencia": "Licencia", - "usuarios": "Usuarios", - "ayuda": "Ayuda" - } -} + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from es!", + "sidebar": { + "reference_data": { + "title": "Catálogos Fijos", + "codes_pedimento_regimen": "Códigos de Pedimento y Régimen", + "containers": "Contenedores", + "countries": "Países", + "currency_types": "Tipos de moneda", + "customs_sections": "Secciones de aduanas", + "customs_warehouses": "Recintos", + "incoterms": "Incoterms", + "invoice_types": "Tipos de factura", + "material_types": "Tipos de material", + "payment_methods": "Métodos de pago", + "pedimento_codes": "Códigos de pedimento", + "pedimento_regimes": "Regímenes de pedimentos", + "sectors": "Sectores", + "states": "Estados", + "transportation_modes": "Métodos de transporte", + "transportation_types": "Tipos de transporte", + "valuation_methods": "Métodos de valoración", + "configuracion": "Configuración", + "general": "General", + "licencia": "Licencia", + "usuarios": "Usuarios", + "ayuda": "Ayuda" + }, + "general_catalogs": { + "title": "Catalogos Generales", + "company_information": "Información de la empresa", + "packages": "Bultos", + "concepts": "Conceptos", + "classification": "Clasificación", + "identifiers": "Identificadores", + "incoterms": "Incoterms", + "inpc": "I.N.P.C", + "fixed_legends": "Leyendas fijas", + "seals": "Precintos", + "valuation_methods": "Metódos de valoración", + "countries": "Países", + "ports": "Puertos", + "unit_measures": "Unidades de medida", + "um_customs_mex": "U.M. Aduanas Mexicanas", + "um_customs_ame": "U.M. Aduanas Americanas", + "um_ace": "U.M. ACE", + "um_oma": "U.M. OMA", + "conversions": "Conversiones", + "equivalences": "Equivalencias", + "exchange_rates": "Tipos de cambio", + "currency_types": "Tipos de moneda", + "multi_currency": "Multi Moneda", + "invoice_types": "Tipos de factura", + "electronic_signatures": "Firmas electrónicas", + "billing_errors": "Errores de facturación", + "customs_warehouses": "Recintos", + "locations": "Localizaciones", + "doda": "DODA", + "packing_list": "Packing List", + "prevalidators": "Prevalidadores", + "electronic_notices": "Avisos electrónicos", + "back_flush": "Back Flush", + "crossing_notice": "Aviso de cruce" + }, + "pedimentos": { + "title": "Pedimentos", + "pedimento_management": "Gestión de Pedimentos", + "pedimento_codes": "Claves de Pedimento", + "customs_regimes": "Regímenes Aduaneros", + "payment_methods": "Formas de Pago", + "customs_sections": "Secciones Aduaneras", + "anexo_22_app_31": "Anexo 22 App 3" + } + } +} \ No newline at end of file diff --git a/frontend/project.inlang/cache/plugins/2sy648wh9sugi b/frontend/project.inlang/cache/plugins/2sy648wh9sugi deleted file mode 100644 index 5b07e0dd..00000000 --- a/frontend/project.inlang/cache/plugins/2sy648wh9sugi +++ /dev/null @@ -1 +0,0 @@ -var Un=Object.create;var Xe=Object.defineProperty;var Pn=Object.getOwnPropertyDescriptor;var vn=Object.getOwnPropertyNames;var Nn=Object.getPrototypeOf,Sn=Object.prototype.hasOwnProperty;var Rn=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var xn=(s,e,i,u)=>{if(e&&typeof e=="object"||typeof e=="function")for(let p of vn(e))!Sn.call(s,p)&&p!==i&&Xe(s,p,{get:()=>e[p],enumerable:!(u=Pn(e,p))||u.enumerable});return s};var jn=(s,e,i)=>(i=s!=null?Un(Nn(s)):{},xn(e||!s||!s.__esModule?Xe(i,"default",{value:s,enumerable:!0}):i,s));var he=Rn(o=>{"use strict";Object.defineProperty(o,"__esModule",{value:!0});o.Type=o.JsonType=o.JavaScriptTypeBuilder=o.JsonTypeBuilder=o.TypeBuilder=o.TypeBuilderError=o.TransformEncodeBuilder=o.TransformDecodeBuilder=o.TemplateLiteralDslParser=o.TemplateLiteralGenerator=o.TemplateLiteralGeneratorError=o.TemplateLiteralFinite=o.TemplateLiteralFiniteError=o.TemplateLiteralParser=o.TemplateLiteralParserError=o.TemplateLiteralResolver=o.TemplateLiteralPattern=o.TemplateLiteralPatternError=o.UnionResolver=o.KeyArrayResolver=o.KeyArrayResolverError=o.KeyResolver=o.ObjectMap=o.Intrinsic=o.IndexedAccessor=o.TypeClone=o.TypeExtends=o.TypeExtendsResult=o.TypeExtendsError=o.ExtendsUndefined=o.TypeGuard=o.TypeGuardUnknownTypeError=o.ValueGuard=o.FormatRegistry=o.TypeBoxError=o.TypeRegistry=o.PatternStringExact=o.PatternNumberExact=o.PatternBooleanExact=o.PatternString=o.PatternNumber=o.PatternBoolean=o.Kind=o.Hint=o.Optional=o.Readonly=o.Transform=void 0;o.Transform=Symbol.for("TypeBox.Transform");o.Readonly=Symbol.for("TypeBox.Readonly");o.Optional=Symbol.for("TypeBox.Optional");o.Hint=Symbol.for("TypeBox.Hint");o.Kind=Symbol.for("TypeBox.Kind");o.PatternBoolean="(true|false)";o.PatternNumber="(0|[1-9][0-9]*)";o.PatternString="(.*)";o.PatternBooleanExact=`^${o.PatternBoolean}$`;o.PatternNumberExact=`^${o.PatternNumber}$`;o.PatternStringExact=`^${o.PatternString}$`;var Ve;(function(s){let e=new Map;function i(){return new Map(e)}s.Entries=i;function u(){return e.clear()}s.Clear=u;function p(y){return e.delete(y)}s.Delete=p;function l(y){return e.has(y)}s.Has=l;function c(y,b){e.set(y,b)}s.Set=c;function T(y){return e.get(y)}s.Get=T})(Ve||(o.TypeRegistry=Ve={}));var D=class extends Error{constructor(e){super(e)}};o.TypeBoxError=D;var Ze;(function(s){let e=new Map;function i(){return new Map(e)}s.Entries=i;function u(){return e.clear()}s.Clear=u;function p(y){return e.delete(y)}s.Delete=p;function l(y){return e.has(y)}s.Has=l;function c(y,b){e.set(y,b)}s.Set=c;function T(y){return e.get(y)}s.Get=T})(Ze||(o.FormatRegistry=Ze={}));var I;(function(s){function e(m){return Array.isArray(m)}s.IsArray=e;function i(m){return typeof m=="bigint"}s.IsBigInt=i;function u(m){return typeof m=="boolean"}s.IsBoolean=u;function p(m){return m instanceof globalThis.Date}s.IsDate=p;function l(m){return m===null}s.IsNull=l;function c(m){return typeof m=="number"}s.IsNumber=c;function T(m){return typeof m=="object"&&m!==null}s.IsObject=T;function y(m){return typeof m=="string"}s.IsString=y;function b(m){return m instanceof globalThis.Uint8Array}s.IsUint8Array=b;function g(m){return m===void 0}s.IsUndefined=g})(I||(o.ValueGuard=I={}));var ze=class extends D{};o.TypeGuardUnknownTypeError=ze;var a;(function(s){function e(r){try{return new RegExp(r),!0}catch{return!1}}function i(r){if(!I.IsString(r))return!1;for(let L=0;L=7&&B<=13||B===27||B===127)return!1}return!0}function u(r){return c(r)||C(r)}function p(r){return I.IsUndefined(r)||I.IsBigInt(r)}function l(r){return I.IsUndefined(r)||I.IsNumber(r)}function c(r){return I.IsUndefined(r)||I.IsBoolean(r)}function T(r){return I.IsUndefined(r)||I.IsString(r)}function y(r){return I.IsUndefined(r)||I.IsString(r)&&i(r)&&e(r)}function b(r){return I.IsUndefined(r)||I.IsString(r)&&i(r)}function g(r){return I.IsUndefined(r)||C(r)}function m(r){return S(r,"Any")&&T(r.$id)}s.TAny=m;function U(r){return S(r,"Array")&&r.type==="array"&&T(r.$id)&&C(r.items)&&l(r.minItems)&&l(r.maxItems)&&c(r.uniqueItems)&&g(r.contains)&&l(r.minContains)&&l(r.maxContains)}s.TArray=U;function d(r){return S(r,"AsyncIterator")&&r.type==="AsyncIterator"&&T(r.$id)&&C(r.items)}s.TAsyncIterator=d;function O(r){return S(r,"BigInt")&&r.type==="bigint"&&T(r.$id)&&p(r.exclusiveMaximum)&&p(r.exclusiveMinimum)&&p(r.maximum)&&p(r.minimum)&&p(r.multipleOf)}s.TBigInt=O;function v(r){return S(r,"Boolean")&&r.type==="boolean"&&T(r.$id)}s.TBoolean=v;function N(r){return S(r,"Constructor")&&r.type==="Constructor"&&T(r.$id)&&I.IsArray(r.parameters)&&r.parameters.every(L=>C(L))&&C(r.returns)}s.TConstructor=N;function j(r){return S(r,"Date")&&r.type==="Date"&&T(r.$id)&&l(r.exclusiveMaximumTimestamp)&&l(r.exclusiveMinimumTimestamp)&&l(r.maximumTimestamp)&&l(r.minimumTimestamp)&&l(r.multipleOfTimestamp)}s.TDate=j;function R(r){return S(r,"Function")&&r.type==="Function"&&T(r.$id)&&I.IsArray(r.parameters)&&r.parameters.every(L=>C(L))&&C(r.returns)}s.TFunction=R;function A(r){return S(r,"Integer")&&r.type==="integer"&&T(r.$id)&&l(r.exclusiveMaximum)&&l(r.exclusiveMinimum)&&l(r.maximum)&&l(r.minimum)&&l(r.multipleOf)}s.TInteger=A;function K(r){return S(r,"Intersect")&&!(I.IsString(r.type)&&r.type!=="object")&&I.IsArray(r.allOf)&&r.allOf.every(L=>C(L)&&!oe(L))&&T(r.type)&&(c(r.unevaluatedProperties)||g(r.unevaluatedProperties))&&T(r.$id)}s.TIntersect=K;function pe(r){return S(r,"Iterator")&&r.type==="Iterator"&&T(r.$id)&&C(r.items)}s.TIterator=pe;function S(r,L){return ee(r)&&r[o.Kind]===L}s.TKindOf=S;function ee(r){return I.IsObject(r)&&o.Kind in r&&I.IsString(r[o.Kind])}s.TKind=ee;function ne(r){return V(r)&&I.IsString(r.const)}s.TLiteralString=ne;function Te(r){return V(r)&&I.IsNumber(r.const)}s.TLiteralNumber=Te;function Ke(r){return V(r)&&I.IsBoolean(r.const)}s.TLiteralBoolean=Ke;function V(r){return S(r,"Literal")&&T(r.$id)&&(I.IsBoolean(r.const)||I.IsNumber(r.const)||I.IsString(r.const))}s.TLiteral=V;function fe(r){return S(r,"Never")&&I.IsObject(r.not)&&Object.getOwnPropertyNames(r.not).length===0}s.TNever=fe;function $(r){return S(r,"Not")&&C(r.not)}s.TNot=$;function te(r){return S(r,"Null")&&r.type==="null"&&T(r.$id)}s.TNull=te;function re(r){return S(r,"Number")&&r.type==="number"&&T(r.$id)&&l(r.exclusiveMaximum)&&l(r.exclusiveMinimum)&&l(r.maximum)&&l(r.minimum)&&l(r.multipleOf)}s.TNumber=re;function _(r){return S(r,"Object")&&r.type==="object"&&T(r.$id)&&I.IsObject(r.properties)&&u(r.additionalProperties)&&l(r.minProperties)&&l(r.maxProperties)&&Object.entries(r.properties).every(([L,B])=>i(L)&&C(B))}s.TObject=_;function ie(r){return S(r,"Promise")&&r.type==="Promise"&&T(r.$id)&&C(r.item)}s.TPromise=ie;function de(r){return S(r,"Record")&&r.type==="object"&&T(r.$id)&&u(r.additionalProperties)&&I.IsObject(r.patternProperties)&&(L=>{let B=Object.getOwnPropertyNames(L.patternProperties);return B.length===1&&e(B[0])&&I.IsObject(L.patternProperties)&&C(L.patternProperties[B[0]])})(r)}s.TRecord=de;function Ee(r){return I.IsObject(r)&&o.Hint in r&&r[o.Hint]==="Recursive"}s.TRecursive=Ee;function ye(r){return S(r,"Ref")&&T(r.$id)&&I.IsString(r.$ref)}s.TRef=ye;function me(r){return S(r,"String")&&r.type==="string"&&T(r.$id)&&l(r.minLength)&&l(r.maxLength)&&y(r.pattern)&&b(r.format)}s.TString=me;function ge(r){return S(r,"Symbol")&&r.type==="symbol"&&T(r.$id)}s.TSymbol=ge;function z(r){return S(r,"TemplateLiteral")&&r.type==="string"&&I.IsString(r.pattern)&&r.pattern[0]==="^"&&r.pattern[r.pattern.length-1]==="$"}s.TTemplateLiteral=z;function Ie(r){return S(r,"This")&&T(r.$id)&&I.IsString(r.$ref)}s.TThis=Ie;function oe(r){return I.IsObject(r)&&o.Transform in r}s.TTransform=oe;function F(r){return S(r,"Tuple")&&r.type==="array"&&T(r.$id)&&I.IsNumber(r.minItems)&&I.IsNumber(r.maxItems)&&r.minItems===r.maxItems&&(I.IsUndefined(r.items)&&I.IsUndefined(r.additionalItems)&&r.minItems===0||I.IsArray(r.items)&&r.items.every(L=>C(L)))}s.TTuple=F;function be(r){return S(r,"Undefined")&&r.type==="undefined"&&T(r.$id)}s.TUndefined=be;function Be(r){return q(r)&&r.anyOf.every(L=>ne(L)||Te(L))}s.TUnionLiteral=Be;function q(r){return S(r,"Union")&&T(r.$id)&&I.IsObject(r)&&I.IsArray(r.anyOf)&&r.anyOf.every(L=>C(L))}s.TUnion=q;function W(r){return S(r,"Uint8Array")&&r.type==="Uint8Array"&&T(r.$id)&&l(r.minByteLength)&&l(r.maxByteLength)}s.TUint8Array=W;function E(r){return S(r,"Unknown")&&T(r.$id)}s.TUnknown=E;function Oe(r){return S(r,"Unsafe")}s.TUnsafe=Oe;function se(r){return S(r,"Void")&&r.type==="void"&&T(r.$id)}s.TVoid=se;function Me(r){return I.IsObject(r)&&r[o.Readonly]==="Readonly"}s.TReadonly=Me;function De(r){return I.IsObject(r)&&r[o.Optional]==="Optional"}s.TOptional=De;function C(r){return I.IsObject(r)&&(m(r)||U(r)||v(r)||O(r)||d(r)||N(r)||j(r)||R(r)||A(r)||K(r)||pe(r)||V(r)||fe(r)||$(r)||te(r)||re(r)||_(r)||ie(r)||de(r)||ye(r)||me(r)||ge(r)||z(r)||Ie(r)||F(r)||be(r)||q(r)||W(r)||E(r)||Oe(r)||se(r)||ee(r)&&Ve.Has(r[o.Kind]))}s.TSchema=C})(a||(o.TypeGuard=a={}));var Ge;(function(s){function e(i){return i[o.Kind]==="Intersect"?i.allOf.every(u=>e(u)):i[o.Kind]==="Union"?i.anyOf.some(u=>e(u)):i[o.Kind]==="Undefined"?!0:i[o.Kind]==="Not"?!e(i.not):!1}s.Check=e})(Ge||(o.ExtendsUndefined=Ge={}));var Ue=class extends D{};o.TypeExtendsError=Ue;var f;(function(s){s[s.Union=0]="Union",s[s.True=1]="True",s[s.False=2]="False"})(f||(o.TypeExtendsResult=f={}));var J;(function(s){function e(n){return n===f.False?n:f.True}function i(n){throw new Ue(n)}function u(n){return a.TNever(n)||a.TIntersect(n)||a.TUnion(n)||a.TUnknown(n)||a.TAny(n)}function p(n,t){return a.TNever(t)?S(n,t):a.TIntersect(t)?R(n,t):a.TUnion(t)?ke(n,t):a.TUnknown(t)?Qe(n,t):a.TAny(t)?l(n,t):i("StructuralRight")}function l(n,t){return f.True}function c(n,t){return a.TIntersect(t)?R(n,t):a.TUnion(t)&&t.anyOf.some(x=>a.TAny(x)||a.TUnknown(x))?f.True:a.TUnion(t)?f.Union:a.TUnknown(t)||a.TAny(t)?f.True:f.Union}function T(n,t){return a.TUnknown(n)?f.False:a.TAny(n)?f.Union:a.TNever(n)?f.True:f.False}function y(n,t){return a.TObject(t)&&z(t)?f.True:u(t)?p(n,t):a.TArray(t)?e(w(n.items,t.items)):f.False}function b(n,t){return u(t)?p(n,t):a.TAsyncIterator(t)?e(w(n.items,t.items)):f.False}function g(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TBigInt(t)?f.True:f.False}function m(n,t){return a.TLiteral(n)&&I.IsBoolean(n.const)||a.TBoolean(n)?f.True:f.False}function U(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TBoolean(t)?f.True:f.False}function d(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TConstructor(t)?n.parameters.length>t.parameters.length?f.False:n.parameters.every((x,M)=>e(w(t.parameters[M],x))===f.True)?e(w(n.returns,t.returns)):f.False:f.False}function O(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TDate(t)?f.True:f.False}function v(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TFunction(t)?n.parameters.length>t.parameters.length?f.False:n.parameters.every((x,M)=>e(w(t.parameters[M],x))===f.True)?e(w(n.returns,t.returns)):f.False:f.False}function N(n,t){return a.TLiteral(n)&&I.IsNumber(n.const)||a.TNumber(n)||a.TInteger(n)?f.True:f.False}function j(n,t){return a.TInteger(t)||a.TNumber(t)?f.True:u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):f.False}function R(n,t){return t.allOf.every(x=>w(n,x)===f.True)?f.True:f.False}function A(n,t){return n.allOf.some(x=>w(x,t)===f.True)?f.True:f.False}function K(n,t){return u(t)?p(n,t):a.TIterator(t)?e(w(n.items,t.items)):f.False}function pe(n,t){return a.TLiteral(t)&&t.const===n.const?f.True:u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TString(t)?se(n,t):a.TNumber(t)?V(n,t):a.TInteger(t)?N(n,t):a.TBoolean(t)?m(n,t):f.False}function S(n,t){return f.False}function ee(n,t){return f.True}function ne(n){let[t,x]=[n,0];for(;a.TNot(t);)t=t.not,x+=1;return x%2===0?t:o.Type.Unknown()}function Te(n,t){return a.TNot(n)?w(ne(n),t):a.TNot(t)?w(n,ne(t)):i("Invalid fallthrough for Not")}function Ke(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TNull(t)?f.True:f.False}function V(n,t){return a.TLiteralNumber(n)||a.TNumber(n)||a.TInteger(n)?f.True:f.False}function fe(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TInteger(t)||a.TNumber(t)?f.True:f.False}function $(n,t){return Object.getOwnPropertyNames(n.properties).length===t}function te(n){return z(n)}function re(n){return $(n,0)||$(n,1)&&"description"in n.properties&&a.TUnion(n.properties.description)&&n.properties.description.anyOf.length===2&&(a.TString(n.properties.description.anyOf[0])&&a.TUndefined(n.properties.description.anyOf[1])||a.TString(n.properties.description.anyOf[1])&&a.TUndefined(n.properties.description.anyOf[0]))}function _(n){return $(n,0)}function ie(n){return $(n,0)}function de(n){return $(n,0)}function Ee(n){return $(n,0)}function ye(n){return z(n)}function me(n){let t=o.Type.Number();return $(n,0)||$(n,1)&&"length"in n.properties&&e(w(n.properties.length,t))===f.True}function ge(n){return $(n,0)}function z(n){let t=o.Type.Number();return $(n,0)||$(n,1)&&"length"in n.properties&&e(w(n.properties.length,t))===f.True}function Ie(n){let t=o.Type.Function([o.Type.Any()],o.Type.Any());return $(n,0)||$(n,1)&&"then"in n.properties&&e(w(n.properties.then,t))===f.True}function oe(n,t){return w(n,t)===f.False||a.TOptional(n)&&!a.TOptional(t)?f.False:f.True}function F(n,t){return a.TUnknown(n)?f.False:a.TAny(n)?f.Union:a.TNever(n)||a.TLiteralString(n)&&te(t)||a.TLiteralNumber(n)&&_(t)||a.TLiteralBoolean(n)&&ie(t)||a.TSymbol(n)&&re(t)||a.TBigInt(n)&&de(t)||a.TString(n)&&te(t)||a.TSymbol(n)&&re(t)||a.TNumber(n)&&_(t)||a.TInteger(n)&&_(t)||a.TBoolean(n)&&ie(t)||a.TUint8Array(n)&&ye(t)||a.TDate(n)&&Ee(t)||a.TConstructor(n)&&ge(t)||a.TFunction(n)&&me(t)?f.True:a.TRecord(n)&&a.TString(q(n))?t[o.Hint]==="Record"?f.True:f.False:a.TRecord(n)&&a.TNumber(q(n))?$(t,0)?f.True:f.False:f.False}function be(n,t){return u(t)?p(n,t):a.TRecord(t)?E(n,t):a.TObject(t)?(()=>{for(let x of Object.getOwnPropertyNames(t.properties)){if(!(x in n.properties)&&!a.TOptional(t.properties[x]))return f.False;if(a.TOptional(t.properties[x]))return f.True;if(oe(n.properties[x],t.properties[x])===f.False)return f.False}return f.True})():f.False}function Be(n,t){return u(t)?p(n,t):a.TObject(t)&&Ie(t)?f.True:a.TPromise(t)?e(w(n.item,t.item)):f.False}function q(n){return o.PatternNumberExact in n.patternProperties?o.Type.Number():o.PatternStringExact in n.patternProperties?o.Type.String():i("Unknown record key pattern")}function W(n){return o.PatternNumberExact in n.patternProperties?n.patternProperties[o.PatternNumberExact]:o.PatternStringExact in n.patternProperties?n.patternProperties[o.PatternStringExact]:i("Unable to get record value schema")}function E(n,t){let[x,M]=[q(t),W(t)];return a.TLiteralString(n)&&a.TNumber(x)&&e(w(n,M))===f.True?f.True:a.TUint8Array(n)&&a.TNumber(x)||a.TString(n)&&a.TNumber(x)||a.TArray(n)&&a.TNumber(x)?w(n,M):a.TObject(n)?(()=>{for(let On of Object.getOwnPropertyNames(n.properties))if(oe(M,n.properties[On])===f.False)return f.False;return f.True})():f.False}function Oe(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?w(W(n),W(t)):f.False}function se(n,t){return a.TLiteral(n)&&I.IsString(n.const)||a.TString(n)?f.True:f.False}function Me(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TString(t)?f.True:f.False}function De(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TSymbol(t)?f.True:f.False}function C(n,t){return a.TTemplateLiteral(n)?w(k.Resolve(n),t):a.TTemplateLiteral(t)?w(n,k.Resolve(t)):i("Invalid fallthrough for TemplateLiteral")}function r(n,t){return a.TArray(t)&&n.items!==void 0&&n.items.every(x=>w(x,t.items)===f.True)}function L(n,t){return a.TNever(n)?f.True:a.TUnknown(n)?f.False:a.TAny(n)?f.Union:f.False}function B(n,t){return u(t)?p(n,t):a.TObject(t)&&z(t)||a.TArray(t)&&r(n,t)?f.True:a.TTuple(t)?I.IsUndefined(n.items)&&!I.IsUndefined(t.items)||!I.IsUndefined(n.items)&&I.IsUndefined(t.items)?f.False:I.IsUndefined(n.items)&&!I.IsUndefined(t.items)||n.items.every((x,M)=>w(x,t.items[M])===f.True)?f.True:f.False:f.False}function fn(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TUint8Array(t)?f.True:f.False}function dn(n,t){return u(t)?p(n,t):a.TObject(t)?F(n,t):a.TRecord(t)?E(n,t):a.TVoid(t)?gn(n,t):a.TUndefined(t)?f.True:f.False}function ke(n,t){return t.anyOf.some(x=>w(n,x)===f.True)?f.True:f.False}function yn(n,t){return n.anyOf.every(x=>w(x,t)===f.True)?f.True:f.False}function Qe(n,t){return f.True}function mn(n,t){return a.TNever(t)?S(n,t):a.TIntersect(t)?R(n,t):a.TUnion(t)?ke(n,t):a.TAny(t)?l(n,t):a.TString(t)?se(n,t):a.TNumber(t)?V(n,t):a.TInteger(t)?N(n,t):a.TBoolean(t)?m(n,t):a.TArray(t)?T(n,t):a.TTuple(t)?L(n,t):a.TObject(t)?F(n,t):a.TUnknown(t)?f.True:f.False}function gn(n,t){return a.TUndefined(n)||a.TUndefined(n)?f.True:f.False}function In(n,t){return a.TIntersect(t)?R(n,t):a.TUnion(t)?ke(n,t):a.TUnknown(t)?Qe(n,t):a.TAny(t)?l(n,t):a.TObject(t)?F(n,t):a.TVoid(t)?f.True:f.False}function w(n,t){return a.TTemplateLiteral(n)||a.TTemplateLiteral(t)?C(n,t):a.TNot(n)||a.TNot(t)?Te(n,t):a.TAny(n)?c(n,t):a.TArray(n)?y(n,t):a.TBigInt(n)?g(n,t):a.TBoolean(n)?U(n,t):a.TAsyncIterator(n)?b(n,t):a.TConstructor(n)?d(n,t):a.TDate(n)?O(n,t):a.TFunction(n)?v(n,t):a.TInteger(n)?j(n,t):a.TIntersect(n)?A(n,t):a.TIterator(n)?K(n,t):a.TLiteral(n)?pe(n,t):a.TNever(n)?ee(n,t):a.TNull(n)?Ke(n,t):a.TNumber(n)?fe(n,t):a.TObject(n)?be(n,t):a.TRecord(n)?Oe(n,t):a.TString(n)?Me(n,t):a.TSymbol(n)?De(n,t):a.TTuple(n)?B(n,t):a.TPromise(n)?Be(n,t):a.TUint8Array(n)?fn(n,t):a.TUndefined(n)?dn(n,t):a.TUnion(n)?yn(n,t):a.TUnknown(n)?mn(n,t):a.TVoid(n)?In(n,t):i(`Unknown left type operand '${n[o.Kind]}'`)}function bn(n,t){return w(n,t)}s.Extends=bn})(J||(o.TypeExtends=J={}));var P;(function(s){function e(y){return y.map(b=>l(b))}function i(y){return new Date(y.getTime())}function u(y){return new Uint8Array(y)}function p(y){let b=Object.getOwnPropertyNames(y).reduce((m,U)=>({...m,[U]:l(y[U])}),{}),g=Object.getOwnPropertySymbols(y).reduce((m,U)=>({...m,[U]:l(y[U])}),{});return{...b,...g}}function l(y){return I.IsArray(y)?e(y):I.IsDate(y)?i(y):I.IsUint8Array(y)?u(y):I.IsObject(y)?p(y):y}function c(y){return y.map(b=>T(b))}s.Rest=c;function T(y,b={}){return{...l(y),...b}}s.Type=T})(P||(o.TypeClone=P={}));var qe;(function(s){function e(d){return d.map(O=>{let{[o.Optional]:v,...N}=P.Type(O);return N})}function i(d){return d.every(O=>a.TOptional(O))}function u(d){return d.some(O=>a.TOptional(O))}function p(d){return i(d.allOf)?o.Type.Optional(o.Type.Intersect(e(d.allOf))):d}function l(d){return u(d.anyOf)?o.Type.Optional(o.Type.Union(e(d.anyOf))):d}function c(d){return d[o.Kind]==="Intersect"?p(d):d[o.Kind]==="Union"?l(d):d}function T(d,O){let v=d.allOf.reduce((N,j)=>{let R=m(j,O);return R[o.Kind]==="Never"?N:[...N,R]},[]);return c(o.Type.Intersect(v))}function y(d,O){let v=d.anyOf.map(N=>m(N,O));return c(o.Type.Union(v))}function b(d,O){let v=d.properties[O];return I.IsUndefined(v)?o.Type.Never():o.Type.Union([v])}function g(d,O){let v=d.items;if(I.IsUndefined(v))return o.Type.Never();let N=v[O];return I.IsUndefined(N)?o.Type.Never():N}function m(d,O){return d[o.Kind]==="Intersect"?T(d,O):d[o.Kind]==="Union"?y(d,O):d[o.Kind]==="Object"?b(d,O):d[o.Kind]==="Tuple"?g(d,O):o.Type.Never()}function U(d,O,v={}){let N=O.map(j=>m(d,j.toString()));return c(o.Type.Union(N,v))}s.Resolve=U})(qe||(o.IndexedAccessor=qe={}));var Y;(function(s){function e(g){let[m,U]=[g.slice(0,1),g.slice(1)];return`${m.toLowerCase()}${U}`}function i(g){let[m,U]=[g.slice(0,1),g.slice(1)];return`${m.toUpperCase()}${U}`}function u(g){return g.toUpperCase()}function p(g){return g.toLowerCase()}function l(g,m){let U=X.ParseExact(g.pattern);if(!Z.Check(U))return{...g,pattern:c(g.pattern,m)};let v=[...G.Generate(U)].map(R=>o.Type.Literal(R)),N=T(v,m),j=o.Type.Union(N);return o.Type.TemplateLiteral([j])}function c(g,m){return typeof g=="string"?m==="Uncapitalize"?e(g):m==="Capitalize"?i(g):m==="Uppercase"?u(g):m==="Lowercase"?p(g):g:g.toString()}function T(g,m){if(g.length===0)return[];let[U,...d]=g;return[b(U,m),...T(d,m)]}function y(g,m){return a.TTemplateLiteral(g)?l(g,m):a.TUnion(g)?o.Type.Union(T(g.anyOf,m)):a.TLiteral(g)?o.Type.Literal(c(g.const,m)):g}function b(g,m){return y(g,m)}s.Map=b})(Y||(o.Intrinsic=Y={}));var Q;(function(s){function e(c,T){return o.Type.Intersect(c.allOf.map(y=>p(y,T)),{...c})}function i(c,T){return o.Type.Union(c.anyOf.map(y=>p(y,T)),{...c})}function u(c,T){return T(c)}function p(c,T){return c[o.Kind]==="Intersect"?e(c,T):c[o.Kind]==="Union"?i(c,T):c[o.Kind]==="Object"?u(c,T):c}function l(c,T,y){return{...p(P.Type(c),T),...y}}s.Map=l})(Q||(o.ObjectMap=Q={}));var Pe;(function(s){function e(b){return b[0]==="^"&&b[b.length-1]==="$"?b.slice(1,b.length-1):b}function i(b,g){return b.allOf.reduce((m,U)=>[...m,...c(U,g)],[])}function u(b,g){let m=b.anyOf.map(U=>c(U,g));return[...m.reduce((U,d)=>d.map(O=>m.every(v=>v.includes(O))?U.add(O):U)[0],new Set)]}function p(b,g){return Object.getOwnPropertyNames(b.properties)}function l(b,g){return g.includePatterns?Object.getOwnPropertyNames(b.patternProperties):[]}function c(b,g){return a.TIntersect(b)?i(b,g):a.TUnion(b)?u(b,g):a.TObject(b)?p(b,g):a.TRecord(b)?l(b,g):[]}function T(b,g){return[...new Set(c(b,g))]}s.ResolveKeys=T;function y(b){return`^(${T(b,{includePatterns:!0}).map(U=>`(${e(U)})`).join("|")})$`}s.ResolvePattern=y})(Pe||(o.KeyResolver=Pe={}));var ve=class extends D{};o.KeyArrayResolverError=ve;var ae;(function(s){function e(i){return Array.isArray(i)?i:a.TUnionLiteral(i)?i.anyOf.map(u=>u.const.toString()):a.TLiteral(i)?[i.const]:a.TTemplateLiteral(i)?(()=>{let u=X.ParseExact(i.pattern);if(!Z.Check(u))throw new ve("Cannot resolve keys from infinite template expression");return[...G.Generate(u)]})():[]}s.Resolve=e})(ae||(o.KeyArrayResolver=ae={}));var Je;(function(s){function*e(u){for(let p of u.anyOf)p[o.Kind]==="Union"?yield*e(p):yield p}function i(u){return o.Type.Union([...e(u)],{...u})}s.Resolve=i})(Je||(o.UnionResolver=Je={}));var Ne=class extends D{};o.TemplateLiteralPatternError=Ne;var Se;(function(s){function e(l){throw new Ne(l)}function i(l){return l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function u(l,c){return a.TTemplateLiteral(l)?l.pattern.slice(1,l.pattern.length-1):a.TUnion(l)?`(${l.anyOf.map(T=>u(T,c)).join("|")})`:a.TNumber(l)?`${c}${o.PatternNumber}`:a.TInteger(l)?`${c}${o.PatternNumber}`:a.TBigInt(l)?`${c}${o.PatternNumber}`:a.TString(l)?`${c}${o.PatternString}`:a.TLiteral(l)?`${c}${i(l.const.toString())}`:a.TBoolean(l)?`${c}${o.PatternBoolean}`:e(`Unexpected Kind '${l[o.Kind]}'`)}function p(l){return`^${l.map(c=>u(c,"")).join("")}$`}s.Create=p})(Se||(o.TemplateLiteralPattern=Se={}));var k;(function(s){function e(i){let u=X.ParseExact(i.pattern);if(!Z.Check(u))return o.Type.String();let p=[...G.Generate(u)].map(l=>o.Type.Literal(l));return o.Type.Union(p)}s.Resolve=e})(k||(o.TemplateLiteralResolver=k={}));var ue=class extends D{};o.TemplateLiteralParserError=ue;var X;(function(s){function e(d,O,v){return d[O]===v&&d.charCodeAt(O-1)!==92}function i(d,O){return e(d,O,"(")}function u(d,O){return e(d,O,")")}function p(d,O){return e(d,O,"|")}function l(d){if(!(i(d,0)&&u(d,d.length-1)))return!1;let O=0;for(let v=0;v0&&N.push(m(A)),v=R+1}let j=d.slice(v);return j.length>0&&N.push(m(j)),N.length===0?{type:"const",const:""}:N.length===1?N[0]:{type:"or",expr:N}}function g(d){function O(j,R){if(!i(j,R))throw new ue("TemplateLiteralParser: Index must point to open parens");let A=0;for(let K=R;K0&&N.push(m(K)),j=A-1}return N.length===0?{type:"const",const:""}:N.length===1?N[0]:{type:"and",expr:N}}function m(d){return l(d)?m(c(d)):T(d)?b(d):y(d)?g(d):{type:"const",const:d}}s.Parse=m;function U(d){return m(d.slice(1,d.length-1))}s.ParseExact=U})(X||(o.TemplateLiteralParser=X={}));var Re=class extends D{};o.TemplateLiteralFiniteError=Re;var Z;(function(s){function e(c){throw new Re(c)}function i(c){return c.type==="or"&&c.expr.length===2&&c.expr[0].type==="const"&&c.expr[0].const==="0"&&c.expr[1].type==="const"&&c.expr[1].const==="[1-9][0-9]*"}function u(c){return c.type==="or"&&c.expr.length===2&&c.expr[0].type==="const"&&c.expr[0].const==="true"&&c.expr[1].type==="const"&&c.expr[1].const==="false"}function p(c){return c.type==="const"&&c.const===".*"}function l(c){return u(c)?!0:i(c)||p(c)?!1:c.type==="and"?c.expr.every(T=>l(T)):c.type==="or"?c.expr.every(T=>l(T)):c.type==="const"?!0:e("Unknown expression type")}s.Check=l})(Z||(o.TemplateLiteralFinite=Z={}));var xe=class extends D{};o.TemplateLiteralGeneratorError=xe;var G;(function(s){function*e(c){if(c.length===1)return yield*c[0];for(let T of c[0])for(let y of e(c.slice(1)))yield`${T}${y}`}function*i(c){return yield*e(c.expr.map(T=>[...l(T)]))}function*u(c){for(let T of c.expr)yield*l(T)}function*p(c){return yield c.const}function*l(c){return c.type==="and"?yield*i(c):c.type==="or"?yield*u(c):c.type==="const"?yield*p(c):(()=>{throw new xe("Unknown expression")})()}s.Generate=l})(G||(o.TemplateLiteralGenerator=G={}));var He;(function(s){function*e(l){let c=l.trim().replace(/"|'/g,"");return c==="boolean"?yield o.Type.Boolean():c==="number"?yield o.Type.Number():c==="bigint"?yield o.Type.BigInt():c==="string"?yield o.Type.String():yield(()=>{let T=c.split("|").map(y=>o.Type.Literal(y.trim()));return T.length===0?o.Type.Never():T.length===1?T[0]:o.Type.Union(T)})()}function*i(l){if(l[1]!=="{"){let c=o.Type.Literal("$"),T=u(l.slice(1));return yield*[c,...T]}for(let c=2;c{let l={Encode:c=>i[o.Transform].Encode(e(c)),Decode:c=>this.decode(i[o.Transform].Decode(c))};return{...i,[o.Transform]:l}})():(()=>{let u={Decode:this.decode,Encode:e};return{...i,[o.Transform]:u}})()}};o.TransformEncodeBuilder=we;var wn=0,Le=class extends D{};o.TypeBuilderError=Le;var Ae=class{Create(e){return e}Throw(e){throw new Le(e)}Discard(e,i){return i.reduce((u,p)=>{let{[p]:l,...c}=u;return c},e)}Strict(e){return JSON.parse(JSON.stringify(e))}};o.TypeBuilder=Ae;var le=class extends Ae{ReadonlyOptional(e){return this.Readonly(this.Optional(e))}Readonly(e){return{...P.Type(e),[o.Readonly]:"Readonly"}}Optional(e){return{...P.Type(e),[o.Optional]:"Optional"}}Any(e={}){return this.Create({...e,[o.Kind]:"Any"})}Array(e,i={}){return this.Create({...i,[o.Kind]:"Array",type:"array",items:P.Type(e)})}Boolean(e={}){return this.Create({...e,[o.Kind]:"Boolean",type:"boolean"})}Capitalize(e,i={}){return{...Y.Map(P.Type(e),"Capitalize"),...i}}Composite(e,i){let u=o.Type.Intersect(e,{}),l=Pe.ResolveKeys(u,{includePatterns:!1}).reduce((c,T)=>({...c,[T]:o.Type.Index(u,[T])}),{});return o.Type.Object(l,i)}Enum(e,i={}){if(I.IsUndefined(e))return this.Throw("Enum undefined or empty");let u=Object.getOwnPropertyNames(e).filter(c=>isNaN(c)).map(c=>e[c]),l=[...new Set(u)].map(c=>o.Type.Literal(c));return this.Union(l,{...i,[o.Hint]:"Enum"})}Extends(e,i,u,p,l={}){switch(J.Extends(e,i)){case f.Union:return this.Union([P.Type(u,l),P.Type(p,l)]);case f.True:return P.Type(u,l);case f.False:return P.Type(p,l)}}Exclude(e,i,u={}){return a.TTemplateLiteral(e)?this.Exclude(k.Resolve(e),i,u):a.TTemplateLiteral(i)?this.Exclude(e,k.Resolve(i),u):a.TUnion(e)?(()=>{let p=e.anyOf.filter(l=>J.Extends(l,i)===f.False);return p.length===1?P.Type(p[0],u):this.Union(p,u)})():J.Extends(e,i)!==f.False?this.Never(u):P.Type(e,u)}Extract(e,i,u={}){return a.TTemplateLiteral(e)?this.Extract(k.Resolve(e),i,u):a.TTemplateLiteral(i)?this.Extract(e,k.Resolve(i),u):a.TUnion(e)?(()=>{let p=e.anyOf.filter(l=>J.Extends(l,i)!==f.False);return p.length===1?P.Type(p[0],u):this.Union(p,u)})():J.Extends(e,i)!==f.False?P.Type(e,u):this.Never(u)}Index(e,i,u={}){return a.TArray(e)&&a.TNumber(i)?P.Type(e.items,u):a.TTuple(e)&&a.TNumber(i)?(()=>{let l=(I.IsUndefined(e.items)?[]:e.items).map(c=>P.Type(c));return this.Union(l,u)})():(()=>{let p=ae.Resolve(i),l=P.Type(e);return qe.Resolve(l,p,u)})()}Integer(e={}){return this.Create({...e,[o.Kind]:"Integer",type:"integer"})}Intersect(e,i={}){if(e.length===0)return o.Type.Never();if(e.length===1)return P.Type(e[0],i);e.some(c=>a.TTransform(c))&&this.Throw("Cannot intersect transform types");let u=e.every(c=>a.TObject(c)),p=P.Rest(e),l=a.TSchema(i.unevaluatedProperties)?{unevaluatedProperties:P.Type(i.unevaluatedProperties)}:{};return i.unevaluatedProperties===!1||a.TSchema(i.unevaluatedProperties)||u?this.Create({...i,...l,[o.Kind]:"Intersect",type:"object",allOf:p}):this.Create({...i,...l,[o.Kind]:"Intersect",allOf:p})}KeyOf(e,i={}){return a.TRecord(e)?(()=>{let u=Object.getOwnPropertyNames(e.patternProperties)[0];return u===o.PatternNumberExact?this.Number(i):u===o.PatternStringExact?this.String(i):this.Throw("Unable to resolve key type from Record key pattern")})():a.TTuple(e)?(()=>{let p=(I.IsUndefined(e.items)?[]:e.items).map((l,c)=>o.Type.Literal(c.toString()));return this.Union(p,i)})():a.TArray(e)?this.Number(i):(()=>{let u=Pe.ResolveKeys(e,{includePatterns:!1});if(u.length===0)return this.Never(i);let p=u.map(l=>this.Literal(l));return this.Union(p,i)})()}Literal(e,i={}){return this.Create({...i,[o.Kind]:"Literal",const:e,type:typeof e})}Lowercase(e,i={}){return{...Y.Map(P.Type(e),"Lowercase"),...i}}Never(e={}){return this.Create({...e,[o.Kind]:"Never",not:{}})}Not(e,i){return this.Create({...i,[o.Kind]:"Not",not:P.Type(e)})}Null(e={}){return this.Create({...e,[o.Kind]:"Null",type:"null"})}Number(e={}){return this.Create({...e,[o.Kind]:"Number",type:"number"})}Object(e,i={}){let u=Object.getOwnPropertyNames(e),p=u.filter(y=>a.TOptional(e[y])),l=u.filter(y=>!p.includes(y)),c=a.TSchema(i.additionalProperties)?{additionalProperties:P.Type(i.additionalProperties)}:{},T=u.reduce((y,b)=>({...y,[b]:P.Type(e[b])}),{});return l.length>0?this.Create({...i,...c,[o.Kind]:"Object",type:"object",properties:T,required:l}):this.Create({...i,...c,[o.Kind]:"Object",type:"object",properties:T})}Omit(e,i,u={}){let p=ae.Resolve(i);return Q.Map(this.Discard(P.Type(e),["$id",o.Transform]),l=>{I.IsArray(l.required)&&(l.required=l.required.filter(c=>!p.includes(c)),l.required.length===0&&delete l.required);for(let c of Object.getOwnPropertyNames(l.properties))p.includes(c)&&delete l.properties[c];return this.Create(l)},u)}Partial(e,i={}){return Q.Map(this.Discard(P.Type(e),["$id",o.Transform]),u=>{let p=Object.getOwnPropertyNames(u.properties).reduce((l,c)=>({...l,[c]:this.Optional(u.properties[c])}),{});return this.Object(p,this.Discard(u,["required"]))},i)}Pick(e,i,u={}){let p=ae.Resolve(i);return Q.Map(this.Discard(P.Type(e),["$id",o.Transform]),l=>{I.IsArray(l.required)&&(l.required=l.required.filter(c=>p.includes(c)),l.required.length===0&&delete l.required);for(let c of Object.getOwnPropertyNames(l.properties))p.includes(c)||delete l.properties[c];return this.Create(l)},u)}Record(e,i,u={}){return a.TTemplateLiteral(e)?(()=>{let p=X.ParseExact(e.pattern);return Z.Check(p)?this.Object([...G.Generate(p)].reduce((l,c)=>({...l,[c]:P.Type(i)}),{}),u):this.Create({...u,[o.Kind]:"Record",type:"object",patternProperties:{[e.pattern]:P.Type(i)}})})():a.TUnion(e)?(()=>{let p=Je.Resolve(e);if(a.TUnionLiteral(p)){let l=p.anyOf.reduce((c,T)=>({...c,[T.const]:P.Type(i)}),{});return this.Object(l,{...u,[o.Hint]:"Record"})}else this.Throw("Record key of type union contains non-literal types")})():a.TLiteral(e)?I.IsString(e.const)||I.IsNumber(e.const)?this.Object({[e.const]:P.Type(i)},u):this.Throw("Record key of type literal is not of type string or number"):a.TInteger(e)||a.TNumber(e)?this.Create({...u,[o.Kind]:"Record",type:"object",patternProperties:{[o.PatternNumberExact]:P.Type(i)}}):a.TString(e)?(()=>{let p=I.IsUndefined(e.pattern)?o.PatternStringExact:e.pattern;return this.Create({...u,[o.Kind]:"Record",type:"object",patternProperties:{[p]:P.Type(i)}})})():this.Never()}Recursive(e,i={}){I.IsUndefined(i.$id)&&(i.$id=`T${wn++}`);let u=e({[o.Kind]:"This",$ref:`${i.$id}`});return u.$id=i.$id,this.Create({...i,[o.Hint]:"Recursive",...u})}Ref(e,i={}){return I.IsString(e)?this.Create({...i,[o.Kind]:"Ref",$ref:e}):(I.IsUndefined(e.$id)&&this.Throw("Reference target type must specify an $id"),this.Create({...i,[o.Kind]:"Ref",$ref:e.$id}))}Required(e,i={}){return Q.Map(this.Discard(P.Type(e),["$id",o.Transform]),u=>{let p=Object.getOwnPropertyNames(u.properties).reduce((l,c)=>({...l,[c]:this.Discard(u.properties[c],[o.Optional])}),{});return this.Object(p,u)},i)}Rest(e){return a.TTuple(e)&&!I.IsUndefined(e.items)?P.Rest(e.items):a.TIntersect(e)?P.Rest(e.allOf):a.TUnion(e)?P.Rest(e.anyOf):[]}String(e={}){return this.Create({...e,[o.Kind]:"String",type:"string"})}TemplateLiteral(e,i={}){let u=I.IsString(e)?Se.Create(He.Parse(e)):Se.Create(e);return this.Create({...i,[o.Kind]:"TemplateLiteral",type:"string",pattern:u})}Transform(e){return new je(e)}Tuple(e,i={}){let[u,p,l]=[!1,e.length,e.length],c=P.Rest(e),T=e.length>0?{...i,[o.Kind]:"Tuple",type:"array",items:c,additionalItems:u,minItems:p,maxItems:l}:{...i,[o.Kind]:"Tuple",type:"array",minItems:p,maxItems:l};return this.Create(T)}Uncapitalize(e,i={}){return{...Y.Map(P.Type(e),"Uncapitalize"),...i}}Union(e,i={}){return a.TTemplateLiteral(e)?k.Resolve(e):(()=>{let u=e;if(u.length===0)return this.Never(i);if(u.length===1)return this.Create(P.Type(u[0],i));let p=P.Rest(u);return this.Create({...i,[o.Kind]:"Union",anyOf:p})})()}Unknown(e={}){return this.Create({...e,[o.Kind]:"Unknown"})}Unsafe(e={}){return this.Create({...e,[o.Kind]:e[o.Kind]||"Unsafe"})}Uppercase(e,i={}){return{...Y.Map(P.Type(e),"Uppercase"),...i}}};o.JsonTypeBuilder=le;var Fe=class extends le{AsyncIterator(e,i={}){return this.Create({...i,[o.Kind]:"AsyncIterator",type:"AsyncIterator",items:P.Type(e)})}Awaited(e,i={}){let u=p=>p.length>0?(()=>{let[l,...c]=p;return[this.Awaited(l),...u(c)]})():p;return a.TIntersect(e)?o.Type.Intersect(u(e.allOf)):a.TUnion(e)?o.Type.Union(u(e.anyOf)):a.TPromise(e)?this.Awaited(e.item):P.Type(e,i)}BigInt(e={}){return this.Create({...e,[o.Kind]:"BigInt",type:"bigint"})}ConstructorParameters(e,i={}){return this.Tuple([...e.parameters],{...i})}Constructor(e,i,u){let[p,l]=[P.Rest(e),P.Type(i)];return this.Create({...u,[o.Kind]:"Constructor",type:"Constructor",parameters:p,returns:l})}Date(e={}){return this.Create({...e,[o.Kind]:"Date",type:"Date"})}Function(e,i,u){let[p,l]=[P.Rest(e),P.Type(i)];return this.Create({...u,[o.Kind]:"Function",type:"Function",parameters:p,returns:l})}InstanceType(e,i={}){return P.Type(e.returns,i)}Iterator(e,i={}){return this.Create({...i,[o.Kind]:"Iterator",type:"Iterator",items:P.Type(e)})}Parameters(e,i={}){return this.Tuple(e.parameters,{...i})}Promise(e,i={}){return this.Create({...i,[o.Kind]:"Promise",type:"Promise",item:P.Type(e)})}RegExp(e,i={}){let u=I.IsString(e)?e:e.source;return this.Create({...i,[o.Kind]:"String",type:"string",pattern:u})}RegEx(e,i={}){return this.RegExp(e,i)}ReturnType(e,i={}){return P.Type(e.returns,i)}Symbol(e){return this.Create({...e,[o.Kind]:"Symbol",type:"symbol"})}Undefined(e={}){return this.Create({...e,[o.Kind]:"Undefined",type:"undefined"})}Uint8Array(e={}){return this.Create({...e,[o.Kind]:"Uint8Array",type:"Uint8Array"})}Void(e={}){return this.Create({...e,[o.Kind]:"Void",type:"void"})}};o.JavaScriptTypeBuilder=Fe;o.JsonType=new le;o.Type=new Fe});var ce=jn(he(),1),en=ce.Type.String({pattern:".*\\{languageTag|locale\\}.*\\.json$",examples:["./messages/{locale}.json","./i18n/{locale}.json"],title:"Path to language files",description:"Specify the pathPattern to locate resource files in your repository. It must include `{locale}` and end with `.json`."}),Ln=ce.Type.Array(en,{title:"Paths to language files",description:"Specify multiple pathPatterns to locate resource files in your repository. Each must include `{locale}` and end with `.json`."}),Ce=ce.Type.Object({pathPattern:ce.Type.Union([en,Ln])});var nn=s=>s.map(e=>{switch(e.type){case"Text":return e.value;case"VariableReference":return`{${e.name}}`}}).join("");var tn=s=>{let e={};for(let i of s.variants){if(e[i.languageTag]!==void 0)throw new Error(`The message "${s.id}" has multiple variants for the language tag "${i.languageTag}". The inlang-message-format plugin does not support multiple variants for the same language tag at the moment.`);e[i.languageTag]=nn(i.pattern)}return e};var rn=s=>{let e=/\{([^}]+)\}/g,i,u=0,p=[];for(;(i=e.exec(s))!==null;){let c=i[1],T=s.slice(u,i.index);T.length>0&&p.push({type:"Text",value:T}),p.push({type:"VariableReference",name:c}),u=i.index+i[0].length}let l=s.slice(Math.max(0,u));return l.length>0&&p.push({type:"Text",value:l}),p};var _e=s=>({id:s.key,alias:{},selectors:[],variants:[{languageTag:s.languageTag,match:[],pattern:rn(s.value)}]});var An="plugin.inlang.messageFormat",H={id:An,displayName:"Inlang Message Format",description:"A plugin for the inlang SDK that uses a JSON file per language tag to store translations.",key:"inlang-message-format",settingsSchema:Ce,loadMessages:async({settings:s,nodeishFs:e})=>{await $n({settings:s,nodeishFs:e});let i={};for(let u of s.languageTags)try{let p=await e.readFile(s["plugin.inlang.messageFormat"].pathPattern.replace("{languageTag}",u),{encoding:"utf-8"}),l=JSON.parse(p);for(let c in l)c!=="$schema"&&(i[c]?i[c].variants=[...i[c].variants,..._e({key:c,value:l[c],languageTag:u}).variants]:i[c]=_e({key:c,value:l[c],languageTag:u}))}catch(p){if(p?.code!=="ENOENT")throw p}return Object.values(i)},saveMessages:async({settings:s,nodeishFs:e,messages:i})=>{let u={};for(let p of i){let l=tn(p);for(let[c,T]of Object.entries(l))u[c]===void 0&&(u[c]={}),u[c][p.id]=T}for(let[p,l]of Object.entries(u)){let c=s["plugin.inlang.messageFormat"].pathPattern.replace("{languageTag}",p);await Fn({path:c,nodeishFs:e}),await e.writeFile(s["plugin.inlang.messageFormat"].pathPattern.replace("{languageTag}",p),(T=>JSON.stringify(T,void 0," "))({$schema:"https://inlang.com/schema/inlang-message-format",...l}))}}},Fn=async s=>{try{await s.nodeishFs.mkdir(Cn(s.path),{recursive:!0})}catch{}};function Cn(s){if(s.length===0)return".";let e=s.charCodeAt(0),i=e===47,u=-1,p=!0;for(let l=s.length-1;l>=1;--l)if(e=s.charCodeAt(l),e===47){if(!p){u=l;break}}else p=!1;return u===-1?i?"/":".":i&&u===1?"//":s.slice(0,u)}var $n=async s=>{if(s.settings["plugin.inlang.messageFormat"].filePath!=null)try{let e=await s.nodeishFs.readFile(s.settings["plugin.inlang.messageFormat"].filePath,{encoding:"utf-8"});await H.saveMessages?.({messages:JSON.parse(e).data,nodeishFs:s.nodeishFs,settings:s.settings}),console.log("Migration to v2 of the inlang-message-format plugin was successful. Please delete the old messages.json file and the filePath property in the settings file of the project.")}catch{}};var on=async({settings:s})=>{let e=[],i=s[h]?.pathPattern?Array.isArray(s[h].pathPattern)?s[h].pathPattern:[s[h].pathPattern]:[];for(let u of i)for(let p of s.locales)e.push({locale:p,path:u.replace(/{(locale|languageTag)}/,p)});return e};function sn(s){return s&&s.constructor&&typeof s.constructor.isBuffer=="function"&&s.constructor.isBuffer(s)}function an(s){return s}function We(s,e){e=e||{};let i=e.delimiter||".",u=e.maxDepth,p=e.transformKey||an,l={};function c(T,y,b){b=b||1,Object.keys(T).forEach(function(g){let m=T[g],U=e.safe&&Array.isArray(m),d=Object.prototype.toString.call(m),O=sn(m),v=d==="[object Object]"||d==="[object Array]",N=y?y+i+p(g):p(g);if(!U&&!O&&v&&Object.keys(m).length&&(!e.maxDepth||b0&&(U=T(m.shift()),d=T(m[0]))}O[U]=Ye(s[g],e)}),l}var ln=async({files:s})=>{let e=[],i=[],u=[];for(let p of s){let l=JSON.parse(new TextDecoder().decode(p.content)),c=We(l,{safe:!0});for(let T in c){if(T==="$schema")continue;let y=Kn(T,p.locale,c[T]);i.push(y.message),u.push(...y.variants);let b=e.find(g=>g.id===y.bundle.id);b===void 0?e.push(y.bundle):b.declarations=$e([...b.declarations,...y.bundle.declarations])}}return{bundles:e,messages:i,variants:u}};function Kn(s,e,i){let u=En(s,e,i),p=$e(u.declarations),l=$e(u.selectors),c=l.filter(T=>p.find(y=>y.name===T.name)===void 0);for(let T of c)p.push({type:"input-variable",name:T.name});return{bundle:{id:s,declarations:p},message:{bundleId:s,selectors:l,locale:e},variants:u.variants}}function En(s,e,i){if(typeof i=="string"){let y=un(i);return{variants:[{messageBundleId:s,messageLocale:e,matches:[],pattern:y.pattern}],declarations:y.declarations,selectors:[]}}let u=i[0],p=[],l=(u.selectors??[]).map(y=>({type:"variable-reference",name:y})),c=new Set;for(let y of u.declarations??[])c.add(Mn(y));let T=new Set;for(let[y,b]of Object.entries(u.match)){let g=un(b),m=Bn(y);for(let U of g.declarations){let d=!1;for(let O of c)if(O.name===U.name){d=!0;break}if(d)break;c.add(U)}for(let U of m.selectors)T.add(U);p.push({messageBundleId:s,messageLocale:e,matches:m.matches,pattern:g.pattern})}return{variants:p,declarations:Array.from(c),selectors:$e([...l,...Array.from(T)])}}function un(s){let e=[],i=[],u=s.split(/(\{.*?\})/).filter(p=>p!=="");for(let p of u)if((p.startsWith("{")&&p.endsWith("}"))===!1)e.push({type:"text",value:p});else{let l=p.slice(1,-1);i.push({type:"input-variable",name:l}),e.push({type:"expression",arg:{type:"variable-reference",name:l}})}return{declarations:i,pattern:e}}function Bn(s){let e=s.replace(" ",""),i=[],u=[],p=e.split(",");for(let l of p){let[c,T]=l.split("=");!c||!T||(T==="*"?i.push({type:"catchall-match",key:c}):i.push({type:"literal-match",key:c,value:T}),u.push({type:"variable-reference",name:c}))}return{matches:i,selectors:u}}var $e=s=>[...new Set(s.map(e=>JSON.stringify(e)))].map(e=>JSON.parse(e));function Mn(s){if(s.startsWith("input"))return{type:"input-variable",name:s.slice(6).trim()};if(s.startsWith("local")){let e=s.match(/local (\w+) = (\w+): (\w+)(.*)/),[,i,u,p,l]=e,c=l?.trim().split(/\s+/).map(T=>{let[y,b]=T.split("=");return y&&b?{name:y,value:{type:"literal",value:b}}:null}).filter(Boolean);return{type:"local-variable",name:i.trim(),value:{type:"expression",arg:{type:"variable-reference",name:u.trim()},annotation:p?{type:"function-reference",name:p.trim(),options:c??[]}:void 0}}}throw new Error("Unsupported declaration type")}var pn=async({bundles:s,messages:e,variants:i})=>{let u={};for(let l of e){let c=s.find(y=>y.id===l.bundleId),T=[...i.reduce((y,b)=>(b.messageId===l.id&&y.set(JSON.stringify(b.matches),b),y),new Map).values()];u[l.locale]={...u[l.locale],...Dn(c,l,T)}}let p=[];for(let l in u)p.push({locale:l,content:new TextEncoder().encode(JSON.stringify(Ye({$schema:"https://inlang.com/schema/inlang-message-format",...u[l]}),void 0," ")),name:l+".json"});return p};function Dn(s,e,i){let u=e.bundleId,p=kn(s,e,i);return{[u]:p}}function kn(s,e,i){if(i.length===1&&e.selectors.length===0&&s.declarations.some(p=>p.type!=="input-variable")===!1)return cn(i[0].pattern);let u=[];for(let p of i){if(p.matches.length===0)for(let T of p.pattern)T.type==="expression"&&T.arg.type==="variable-reference"&&p.matches.push({key:T.arg.name,type:"catchall-match"});let l=cn(p.pattern),c=Vn(p.matches);u.push([c,l])}return[{declarations:s.declarations.sort((p,l)=>p.name.localeCompare(l.name)).map(zn).sort(),selectors:e.selectors.map(p=>p.name).sort(),match:Object.fromEntries(u)}]}function cn(s){let e="";for(let i of s)if(i.type==="text")e+=i.value;else if(i.arg.type==="variable-reference")e+=`{${i.arg.name}}`;else throw new Error("Unsupported expression type");return e}function Vn(s){return s.sort((i,u)=>i.key.localeCompare(u.key)).map(i=>i.type==="literal-match"?`${i.key}=${i.value}`:`${i.key}=*`).join(", ")}function zn(s){if(s.type==="input-variable")return`input ${s.name}`;if(s.type==="local-variable"){let e="";if(s.value.arg.type==="variable-reference"?e=`local ${s.name} = ${s.value.arg.name}`:s.value.arg.type==="literal"&&(e=`local ${s.name} = "${s.value.arg.value}"`),s.value.annotation&&(e+=`: ${s.value.annotation.name}`),s.value.annotation?.options)for(let i of s.value?.annotation?.options??[]){if(i.value.type!=="literal")throw new Error("Unsupported option type");e+=` ${i.name}=${i.value.value}`}return e}throw new Error("Unsupported declaration type")}var h="plugin.inlang.messageFormat",Tn={key:h,id:H.id,displayName:H.displayName,description:H.description,loadMessages:H.loadMessages,saveMessages:H.saveMessages,settingsSchema:Ce,toBeImportedFiles:on,importFiles:ln,exportFiles:pn};var It=Tn;export{It as default}; diff --git a/frontend/project.inlang/cache/plugins/ygx0uiahq6uw b/frontend/project.inlang/cache/plugins/ygx0uiahq6uw deleted file mode 100644 index 8ce3dc57..00000000 --- a/frontend/project.inlang/cache/plugins/ygx0uiahq6uw +++ /dev/null @@ -1,16 +0,0 @@ -var Vt=Object.create;var It=Object.defineProperty;var Ht=Object.getOwnPropertyDescriptor;var Xt=Object.getOwnPropertyNames;var Yt=Object.getPrototypeOf,tn=Object.prototype.hasOwnProperty;var nn=(l,c)=>()=>(c||l((c={exports:{}}).exports,c),c.exports);var rn=(l,c,p,u)=>{if(c&&typeof c=="object"||typeof c=="function")for(let f of Xt(c))!tn.call(l,f)&&f!==p&&It(l,f,{get:()=>c[f],enumerable:!(u=Ht(c,f))||u.enumerable});return l};var en=(l,c,p)=>(p=l!=null?Vt(Yt(l)):{},rn(c||!l||!l.__esModule?It(p,"default",{value:l,enumerable:!0}):p,l));var Lt=nn((J,gt)=>{(function(l,c){typeof J=="object"&&typeof gt=="object"?gt.exports=c():typeof define=="function"&&define.amd?define([],c):typeof J=="object"?J.Parsimmon=c():l.Parsimmon=c()})(typeof self<"u"?self:J,function(){return function(l){var c={};function p(u){if(c[u])return c[u].exports;var f=c[u]={i:u,l:!1,exports:{}};return l[u].call(f.exports,f,f.exports,p),f.l=!0,f.exports}return p.m=l,p.c=c,p.d=function(u,f,Z){p.o(u,f)||Object.defineProperty(u,f,{configurable:!1,enumerable:!0,get:Z})},p.r=function(u){Object.defineProperty(u,"__esModule",{value:!0})},p.n=function(u){var f=u&&u.__esModule?function(){return u.default}:function(){return u};return p.d(f,"a",f),f},p.o=function(u,f){return Object.prototype.hasOwnProperty.call(u,f)},p.p="",p(p.s=0)}([function(l,c,p){"use strict";function u(t){if(!(this instanceof u))return new u(t);this._=t}var f=u.prototype;function Z(t,n){for(var r=0;r>7),buf:function(o){var i=I(function(a,s,d,y){return a.concat(d===y.length-1?Buffer.from([s,0]).readUInt16BE(0):y.readUInt16BE(d))},[],o);return Buffer.from(j(function(a){return(a<<1&65535)>>8},i))}(r.buf)}}),r}function dt(){return typeof Buffer<"u"}function C(){if(!dt())throw new Error("Buffer global does not exist; please use webpack if you need to parse Buffers in the browser.")}function ht(t){C();var n=I(function(i,a){return i+a},0,t);if(n%8!=0)throw new Error("The bits ["+t.join(", ")+"] add up to "+n+" which is not an even number of bytes; the total should be divisible by 8");var r,e=n/8,o=(r=function(i){return i>48},I(function(i,a){return i||(r(a)?a:i)},null,t));if(o)throw new Error(o+" bit range requested exceeds 48 bit (6 byte) Number max.");return new u(function(i,a){var s=e+a;return s>i.length?b(a,e.toString()+" bytes"):h(s,I(function(d,y){var v=At(y,d.buf);return{coll:d.coll.concat(v.v),buf:v.buf}},{coll:[],buf:i.slice(a,s)},t).coll)})}function E(t,n){return new u(function(r,e){return C(),e+n>r.length?b(e,n+" bytes for "+t):h(e+n,r.slice(e,e+n))})}function K(t,n){if(typeof(r=n)!="number"||Math.floor(r)!==r||n<0||n>6)throw new Error(t+" requires integer length in range [0, 6].");var r}function V(t){return K("uintBE",t),E("uintBE("+t+")",t).map(function(n){return n.readUIntBE(0,t)})}function H(t){return K("uintLE",t),E("uintLE("+t+")",t).map(function(n){return n.readUIntLE(0,t)})}function X(t){return K("intBE",t),E("intBE("+t+")",t).map(function(n){return n.readIntBE(0,t)})}function Y(t){return K("intLE",t),E("intLE("+t+")",t).map(function(n){return n.readIntLE(0,t)})}function U(t){return t instanceof u}function q(t){return{}.toString.call(t)==="[object Array]"}function W(t){return dt()&&Buffer.isBuffer(t)}function h(t,n){return{status:!0,index:t,value:n,furthest:-1,expected:[]}}function b(t,n){return q(n)||(n=[n]),{status:!1,index:-1,value:null,furthest:t,expected:n}}function w(t,n){if(!n||t.furthest>n.furthest)return t;var r=t.furthest===n.furthest?function(e,o){if(function(){if(u._supportsSet!==void 0)return u._supportsSet;var S=typeof Set<"u";return u._supportsSet=S,S}()&&Array.from){for(var i=new Set(e),a=0;a=0;){if(a in r){e=r[a].line,i===0&&(i=r[a].lineStart);break}(t.charAt(a)===` -`||t.charAt(a)==="\r"&&t.charAt(a+1)!==` -`)&&(o++,i===0&&(i=a+1)),a--}var s=e+o,d=n-i;return r[n]={line:s,lineStart:i},{offset:n,line:s+1,column:d+1}}function A(t){if(!U(t))throw new Error("not a parser: "+t)}function nt(t,n){return typeof t=="string"?t.charAt(n):t[n]}function F(t){if(typeof t!="number")throw new Error("not a number: "+t)}function L(t){if(typeof t!="function")throw new Error("not a function: "+t)}function T(t){if(typeof t!="string")throw new Error("not a string: "+t)}var Ft=2,Nt=3,O=8,Rt=5*O,zt=4*O,vt=" ";function rt(t,n){return new Array(n+1).join(t)}function et(t,n,r){var e=n-t.length;return e<=0?t:rt(r,e)+t}function yt(t,n,r,e){return{from:t-n>0?t-n:0,to:t+r>e?e:t+r}}function Dt(t,n){var r,e,o,i,a,s=n.index,d=s.offset,y=1;if(d===t.length)return"Got the end of the input";if(W(t)){var v=d-d%O,_=d-v,x=yt(v,Rt,zt+O,t.length),S=j(function(m){return j(function(R){return et(R.toString(16),2,"0")},m)},function(m,R){var z=m.length,M=[],D=0;if(z<=R)return[m.slice()];for(var Q=0;Q=4&&(r+=1),y=2,o=j(function(m){return m.length<=4?m.join(" "):m.slice(0,4).join(" ")+" "+m.slice(4).join(" ")},S),(a=(8*(i.to>0?i.to-1:i.to)).toString(16).length)<2&&(a=2)}else{var N=t.split(/\r\n|[\n\r\u2028\u2029]/);r=s.column-1,e=s.line-1,i=yt(e,Ft,Nt,N.length),o=N.slice(i.from,i.to),a=i.to.toString().length}var Kt=e-i.from;return W(t)&&(a=(8*(i.to>0?i.to-1:i.to)).toString(16).length)<2&&(a=2),I(function(m,R,z){var M,D=z===Kt,Q=D?"> ":vt;return M=W(t)?et((8*(i.from+z)).toString(16),a,"0"):et((i.from+z+1).toString(),a," "),[].concat(m,[Q+M+" | "+R],D?[vt+rt(" ",a)+" | "+et("",r," ")+rt("^",y)]:[])},[],o).join(` -`)}function bt(t,n){return[` -`,"-- PARSING FAILED "+rt("-",50),` - -`,Dt(t,n),` - -`,(r=n.expected,r.length===1?`Expected: - -`+r[0]:`Expected one of the following: - -`+r.join(", ")),` -`].join("");var r}function xt(t){return t.flags!==void 0?t.flags:[t.global?"g":"",t.ignoreCase?"i":"",t.multiline?"m":"",t.unicode?"u":"",t.sticky?"y":""].join("")}function ut(){for(var t=[].slice.call(arguments),n=t.length,r=0;r=2?F(n):n=0;var r=function(o){return RegExp("^(?:"+o.source+")",xt(o))}(t),e=""+t;return u(function(o,i){var a=r.exec(o.slice(i));if(a){if(0<=n&&n<=a.length){var s=a[0],d=a[n];return h(i+s.length,d)}return b(i,"valid match group (0 to "+a.length+") in "+e)}return b(i,e)})}function P(t){return u(function(n,r){return h(r,t)})}function it(t){return u(function(n,r){return b(r,t)})}function at(t){if(U(t))return u(function(n,r){var e=t._(n,r);return e.index=r,e.value="",e});if(typeof t=="string")return at($(t));if(t instanceof RegExp)return at(B(t));throw new Error("not a string, regexp, or parser: "+t)}function Et(t){return A(t),u(function(n,r){var e=t._(n,r),o=n.slice(r,e.index);return e.status?b(r,'not "'+o+'"'):h(r,null)})}function ft(t){return L(t),u(function(n,r){var e=nt(n,r);return r=t.length?b(n,"any character/byte"):h(n+1,nt(t,n))}),Ut=u(function(t,n){return h(t.length,t.slice(n))}),pt=u(function(t,n){return n=0}).desc(n)},u.optWhitespace=Jt,u.Parser=u,u.range=function(t,n){return ft(function(r){return t<=r&&r<=n}).desc(t+"-"+n)},u.regex=B,u.regexp=B,u.sepBy=wt,u.sepBy1=st,u.seq=ut,u.seqMap=k,u.seqObj=function(){for(var t,n={},r=0,e=(t=arguments,Array.prototype.slice.call(t)),o=e.length,i=0;i255)throw new Error("Value specified to byte constructor ("+t+"=0x"+t.toString(16)+") is larger in value than a single byte.");var n=(t>15?"0x":"0x0")+t.toString(16);return u(function(r,e){var o=nt(r,e);return o===t?h(e+1,o):b(e,n)})},buffer:function(t){return E("buffer",t).map(function(n){return Buffer.from(n)})},encodedString:function(t,n){return E("string",n).map(function(r){return r.toString(t)})},uintBE:V,uint8BE:V(1),uint16BE:V(2),uint32BE:V(4),uintLE:H,uint8LE:H(1),uint16LE:H(2),uint32LE:H(4),intBE:X,int8BE:X(1),int16BE:X(2),int32BE:X(4),intLE:Y,int8LE:Y(1),int16LE:Y(2),int32LE:Y(4),floatBE:E("floatBE",4).map(function(t){return t.readFloatBE(0)}),floatLE:E("floatLE",4).map(function(t){return t.readFloatLE(0)}),doubleBE:E("doubleBE",8).map(function(t){return t.readDoubleBE(0)}),doubleLE:E("doubleLE",8).map(function(t){return t.readDoubleLE(0)})},l.exports=u}])})});var g=en(Lt(),1),un=()=>g.default.createLanguage({entry:l=>g.default.alt(l.findReference,g.default.any).many().map(c=>c.flatMap(p=>p)).map(c=>c.filter(p=>typeof p=="object").flat().filter(p=>p!==null)),findReference:function(l){return g.default.seq(g.default.regex(/(import \* as m)|(import { m })/),l.findMessage.many())},dotNotation:()=>g.default.seqMap(g.default.string("."),g.default.index,g.default.regex(/\w+/),g.default.index,(l,c,p,u)=>({messageId:p,start:c,end:u})),doubleQuote:()=>g.default.seqMap(g.default.string('"'),g.default.index,g.default.regex(/[\w.]+/),g.default.string('"'),(l,c,p)=>({messageId:p,start:c})),singleQuote:()=>g.default.seqMap(g.default.string("'"),g.default.index,g.default.regex(/[\w.]+/),g.default.string("'"),(l,c,p)=>({messageId:p,start:c})),bracketNotation:l=>g.default.seqMap(g.default.string("["),g.default.alt(l.doubleQuote,l.singleQuote),g.default.string("]"),g.default.index,(c,p,u,f)=>({messageId:p.messageId,start:p.start,end:f})),findMessage:l=>g.default.seqMap(g.default.regex(/.*?(?p===null?null:{messageId:`${p.messageId}`,position:{start:{line:p.start.line,character:p.start.column},end:{line:p.end.line,character:p.end.column+u.length}}})});function kt(l){try{return un().entry.tryParse(l)}catch{return[]}}function ct(l){let c=l.trim().replace(/[^a-zA-Z0-9\s_.]/g,"").replace(/[\s.]+/g,"_");return/^[0-9]/.test(c)&&(c="_"+c),c}var Pt={messageReferenceMatchers:[async l=>kt(l.documentText)],extractMessageOptions:[{callback:l=>{let c=ct(l.bundleId);return{bundleId:c,messageReplacement:`{m.${c}()}`}}},{callback:l=>{let c=ct(l.bundleId);return{bundleId:c,messageReplacement:`m.${c}()`}}}],documentSelectors:[{language:"typescriptreact"},{language:"javascript"},{language:"typescript"},{language:"svelte"},{language:"astro"},{language:"vue"}]};var Mt="plugin.inlang.mFunctionMatcher",qt={id:Mt,displayName:"Inlang M Function Matcher",description:"A plugin for the inlang SDK that uses a JSON file per language tag to store translations.",key:Mt,meta:{"app.inlang.ideExtension":Pt}};var yn=qt;export{yn as default}; diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 429331ca..ee9b1269 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -65,130 +65,266 @@ export function getSidebarData(): SidebarData { ], navMain: [ { - title: m["sidebar.catalogos_fijos"](), + title: m["sidebar.reference_data.title"](), url: "/dashboard", icon: SquareTerminalIcon, items: [ { - title: m["sidebar.codigos_pedimento_regimen"](), + title: m["sidebar.reference_data.codes_pedimento_regimen"](), url: "/dashboard/reference_data/code_pedimento_regimens", }, { - title: m["sidebar.contenedores"](), + title: m["sidebar.reference_data.containers"](), url: "/dashboard/reference_data/containers", }, { - title: m["sidebar.paises"](), + title: m["sidebar.reference_data.countries"](), url: "/dashboard/reference_data/countries", }, { - title: m["sidebar.tipos_moneda"](), + title: m["sidebar.reference_data.currency_types"](), url: "/dashboard/reference_data/currency_types", }, { - title: m["sidebar.secciones_aduanas"](), + title: m["sidebar.reference_data.customs_sections"](), url: "/dashboard/reference_data/customs_sections", }, { - title: m["sidebar.recintos"](), + title: m["sidebar.reference_data.customs_warehouses"](), url: "/dashboard/reference_data/customs_warehouses", }, { - title: m["sidebar.incoterms"](), + title: m["sidebar.reference_data.incoterms"](), url: "/dashboard/reference_data/incoterms", }, { - title: m["sidebar.tipos_factura"](), + title: m["sidebar.reference_data.invoice_types"](), url: "/dashboard/reference_data/invoice_types", }, { - title: m["sidebar.tipos_material"](), + title: m["sidebar.reference_data.material_types"](), url: "/dashboard/reference_data/material_types", }, { - title: m["sidebar.metodos_pago"](), + title: m["sidebar.reference_data.payment_methods"](), url: "/dashboard/reference_data/payment_methods", }, { - title: m["sidebar.codigos_pedimento"](), + title: m["sidebar.reference_data.pedimento_codes"](), url: "/dashboard/reference_data/pedimento_codes", }, { - title: m["sidebar.regimenes_pedimentos"](), + title: m["sidebar.reference_data.pedimento_regimes"](), url: "/dashboard/reference_data/pedimento_regimens", }, { - title: m["sidebar.sectores"](), + title: m["sidebar.reference_data.sectors"](), url: "/dashboard/reference_data/sectors", }, { - title: m["sidebar.estados"](), + title: m["sidebar.reference_data.states"](), url: "/dashboard/reference_data/states", }, { - title: m["sidebar.metodos_transporte"](), + title: m["sidebar.reference_data.transportation_modes"](), url: "/dashboard/reference_data/transport_modes", }, { - title: m["sidebar.tipos_transporte"](), + title: m["sidebar.reference_data.transportation_types"](), url: "/dashboard/reference_data/transport_types", }, { - title: m["sidebar.metodos_valoracion"](), + title: m["sidebar.reference_data.valuation_methods"](), url: "/dashboard/reference_data/valuation_methods", }, ], - isActive: true, + isActive: false, }, { - title: m["sidebar.inventarios"](), + title: m["sidebar.general_catalogs.title"](), url: "#", icon: BotIcon, items: [ { - title: m["sidebar.gestionar_inventarios"](), + title: m["sidebar.general_catalogs.company_information"](), url: "#", }, { - title: m["sidebar.reportes"](), + title: m["sidebar.general_catalogs.packages"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.concepts"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.classification"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.identifiers"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.incoterms"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.inpc"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.fixed_legends"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.seals"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.valuation_methods"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.countries"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.ports"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.unit_measures"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.um_customs_mex"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.um_customs_ame"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.um_ace"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.um_oma"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.conversions"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.equivalences"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.exchange_rates"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.currency_types"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.multi_currency"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.invoice_types"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.electronic_signatures"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.billing_errors"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.customs_warehouses"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.locations"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.doda"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.packing_list"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.prevalidators"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.electronic_notices"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.back_flush"](), + url: "#", + }, + { + title: m["sidebar.general_catalogs.crossing_notice"](), url: "#", }, ], }, { - title: m["sidebar.pedimentos"](), + title: m["sidebar.pedimentos.title"](), url: "#", icon: BookOpenIcon, items: [ { - title: m["sidebar.nuevo_pedimento"](), + title: m["sidebar.pedimentos.pedimento_management"](), url: "#", }, { - title: m["sidebar.consultar"](), + title: m["sidebar.pedimentos.pedimento_codes"](), url: "#", }, { - title: m["sidebar.historial"](), + title: m["sidebar.pedimentos.customs_regimes"](), + url: "#", + }, + { + title: m["sidebar.pedimentos.payment_methods"](), + url: "#", + }, + { + title: m["sidebar.pedimentos.customs_sections"](), + url: "#", + }, + { + title: m["sidebar.pedimentos.anexo_22_app_31"](), url: "#", }, ], }, { - title: m["sidebar.configuracion"](), + title: m["sidebar.reference_data.configuracion"](), url: "#", icon: Settings2Icon, items: [ { - title: m["sidebar.general"](), + title: m["sidebar.reference_data.general"](), url: "#", }, { - title: m["sidebar.licencia"](), + title: m["sidebar.reference_data.licencia"](), url: "#", }, { - title: m["sidebar.usuarios"](), + title: m["sidebar.reference_data.usuarios"](), url: "#", }, ], @@ -196,12 +332,12 @@ export function getSidebarData(): SidebarData { ], projects: [ { - name: m["sidebar.reportes"](), + name: m["sidebar.reference_data.usuarios"](), url: "#", icon: ChartPieIcon, }, { - name: m["sidebar.ayuda"](), + name: m["sidebar.reference_data.ayuda"](), url: "#", icon: FrameIcon, }, diff --git a/frontend/src/lib/components/sidebar/nav-user.svelte b/frontend/src/lib/components/sidebar/nav-user.svelte index a29971cb..151259a8 100644 --- a/frontend/src/lib/components/sidebar/nav-user.svelte +++ b/frontend/src/lib/components/sidebar/nav-user.svelte @@ -11,8 +11,9 @@ import SparklesIcon from "@lucide/svelte/icons/sparkles"; import LanguagesIcon from "@lucide/svelte/icons/languages"; import { logout } from "$lib/auth"; - import { setLocale, locales } from "$lib/paraglide/runtime"; + import { cookieName } from "$lib/paraglide/runtime"; import { page } from "$app/state"; + import { browser } from "$app/environment"; let { user }: { user: { name: string; email: string; avatar: string } } = $props(); const sidebar = useSidebar(); @@ -25,9 +26,21 @@ } function toggleLanguage() { - // Alternar entre 'en' y 'es' - const newLocale = currentLocale === 'en' ? 'es' : 'en'; - setLocale(newLocale); + if (!browser) return; + + // Leer la cookie actual para obtener el idioma real + const cookies = document.cookie.split(';').map(c => c.trim()); + const localeCookie = cookies.find(c => c.startsWith(`${cookieName}=`)); + const current = localeCookie ? localeCookie.split('=')[1] : 'en'; + + // Alternar el idioma + const newLocale = current === 'en' ? 'es' : 'en'; + + // Establecer la cookie del idioma + document.cookie = `${cookieName}=${newLocale}; path=/; max-age=34560000; SameSite=Lax`; + + // Recargar la página para que el servidor procese el nuevo idioma + window.location.reload(); } @@ -43,7 +56,7 @@ > - CN + AS
{user.name} From 38d86531e8e3f88a82056ad82f2787b153b9b5c7 Mon Sep 17 00:00:00 2001 From: Kevin Rosales Date: Tue, 4 Nov 2025 21:48:05 -0600 Subject: [PATCH 11/16] feat: Add comprehensive A76 modules with database relationships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ New Features: - Company module: Single company management with comprehensive business info - Client & Provider module: Manages clients/providers with address/program relationships - GParts module: Parts/components management for SCAII, SCAF, and WINSAAI systems - GClass module: Class classifications for SCAII and SCAF with tariff information 🔗 Database Relationships: - GPart ↔ GClass: Composite key relationship (client_key, part_class ↔ class_code) - GPart → Country: Foreign key to public.countries (country_of_origin) - GPart → CurrencyType: Foreign key to public.currency_types (currency_key) - GClass → MaterialType: Foreign key to public.material_types (material_key) 📊 API Endpoints Added: Company Module (/company): - POST / - Create company - GET / - Get single company Client & Provider Module (/clients-providers): - POST / - Create client/provider - GET / - List all with pagination - GET /clients - List only clients - GET /providers - List only providers - GET /search/rfc/{rfc} - Search by RFC - GET /{client_id} - Get by ID - PUT /{client_id} - Update client/provider - DELETE /{client_id} - Delete client/provider - PATCH /{client_id}/toggle-status - Toggle status - GET /{client_id}/address - Get address info - GET /{client_id}/programs - Get programs info - GET /{client_id}/basic - Get basic info GParts Module (/parts): - POST / - Create part - GET / - List all with pagination and filters - GET /client/{client_key} - Get parts by client - GET /search/fraction/{fraction} - Search by tariff fraction - GET /search/supplier/{supplier} - Search by supplier - GET /search/country/{country_code} - Search by country - GET /statistics - Get parts statistics - GET /{client_key}/{part_number} - Get specific part - PUT /{client_key}/{part_number} - Update part - DELETE /{client_key}/{part_number} - Delete part - PATCH /{client_key}/{part_number}/toggle-status - Toggle status - GET /{client_key}/{part_number}/basic - Get basic info - GET /{client_key}/{part_number}/regulatory - Get regulatory info GClass Module (/classes): - POST / - Create class - GET / - List all with pagination and filters - GET /client/{client_key} - Get classes by client - GET /search/fraction/{fraction} - Search by tariff fraction - GET /search/material/{material_key} - Search by material - GET /search/unit-measure/{unit_of_measure} - Search by unit of measure - GET /search/physical-review/{physical_review} - Search by physical review status - GET /statistics - Get class statistics - GET /{client_key}/{class_code} - Get specific class - PUT /{client_key}/{class_code} - Update class - DELETE /{client_key}/{class_code} - Delete class - GET /{client_key}/{class_code}/basic - Get basic info - GET /{client_key}/{class_code}/tariff - Get tariff information 🏗️ Architecture: - Modular design with models, DTOs, services, and routes for each entity - English field names with composite primary keys where applicable - Comprehensive CRUD operations with specialized search endpoints - SQLAlchemy relationships with proper foreign key constraints - Type-safe DTOs with Pydantic validation 📝 Documentation: - RELATIONSHIPS.md: Complete documentation of database relationships - Detailed type hints and comprehensive service methods - Consistent patterns across all modules for maintainability --- .gitignore | 2 +- backend/api/v1/modules/a76/GClass/__init__.py | 6 + backend/api/v1/modules/a76/GClass/dto.py | 97 ++++++ backend/api/v1/modules/a76/GClass/models.py | 60 ++++ backend/api/v1/modules/a76/GClass/routes.py | 261 +++++++++++++++ backend/api/v1/modules/a76/GClass/service.py | 294 +++++++++++++++++ backend/api/v1/modules/a76/GParts/__init__.py | 6 + backend/api/v1/modules/a76/GParts/dto.py | 182 +++++++++++ backend/api/v1/modules/a76/GParts/models.py | 87 +++++ backend/api/v1/modules/a76/GParts/routes.py | 273 ++++++++++++++++ backend/api/v1/modules/a76/GParts/service.py | 15 + .../modules/a76/client_&_provider/__init__.py | 6 + .../v1/modules/a76/client_&_provider/dto.py | 165 ++++++++++ .../modules/a76/client_&_provider/models.py | 105 ++++++ .../modules/a76/client_&_provider/routes.py | 221 +++++++++++++ .../modules/a76/client_&_provider/service.py | 309 ++++++++++++++++++ .../api/v1/modules/a76/company/__init__.py | 6 + backend/api/v1/modules/a76/company/dto.py | 157 +++++++++ backend/api/v1/modules/a76/company/models.py | 68 ++++ backend/api/v1/modules/a76/company/routes.py | 176 ++++++++++ backend/api/v1/modules/a76/company/service.py | 184 +++++++++++ backend/api/v1/router.py | 12 + docs/RELATIONSHIPS.md | 107 ++++++ 23 files changed, 2798 insertions(+), 1 deletion(-) create mode 100644 backend/api/v1/modules/a76/GClass/__init__.py create mode 100644 backend/api/v1/modules/a76/GClass/dto.py create mode 100644 backend/api/v1/modules/a76/GClass/models.py create mode 100644 backend/api/v1/modules/a76/GClass/routes.py create mode 100644 backend/api/v1/modules/a76/GClass/service.py create mode 100644 backend/api/v1/modules/a76/GParts/__init__.py create mode 100644 backend/api/v1/modules/a76/GParts/dto.py create mode 100644 backend/api/v1/modules/a76/GParts/models.py create mode 100644 backend/api/v1/modules/a76/GParts/routes.py create mode 100644 backend/api/v1/modules/a76/GParts/service.py create mode 100644 backend/api/v1/modules/a76/client_&_provider/__init__.py create mode 100644 backend/api/v1/modules/a76/client_&_provider/dto.py create mode 100644 backend/api/v1/modules/a76/client_&_provider/models.py create mode 100644 backend/api/v1/modules/a76/client_&_provider/routes.py create mode 100644 backend/api/v1/modules/a76/client_&_provider/service.py create mode 100644 backend/api/v1/modules/a76/company/__init__.py create mode 100644 backend/api/v1/modules/a76/company/dto.py create mode 100644 backend/api/v1/modules/a76/company/models.py create mode 100644 backend/api/v1/modules/a76/company/routes.py create mode 100644 backend/api/v1/modules/a76/company/service.py create mode 100644 docs/RELATIONSHIPS.md diff --git a/.gitignore b/.gitignore index 2d9ff71d..14c07767 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,7 @@ wheels/ # Environment .env .env.local - +backend/SCRIPTS/ # IDEs .vscode/ .idea/ diff --git a/backend/api/v1/modules/a76/GClass/__init__.py b/backend/api/v1/modules/a76/GClass/__init__.py new file mode 100644 index 00000000..7c89dc70 --- /dev/null +++ b/backend/api/v1/modules/a76/GClass/__init__.py @@ -0,0 +1,6 @@ +""" +Módulo de Tenants +""" +from .routes import router + +__all__ = ["router"] diff --git a/backend/api/v1/modules/a76/GClass/dto.py b/backend/api/v1/modules/a76/GClass/dto.py new file mode 100644 index 00000000..a0e9e786 --- /dev/null +++ b/backend/api/v1/modules/a76/GClass/dto.py @@ -0,0 +1,97 @@ +""" +DTOs (Data Transfer Objects) para módulo de clases SCAII y SCAF +Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS +""" +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + + +class ClassCreateDTO(BaseModel): + """DTO para crear una clase""" + client_key: int = Field(..., description="Client key") + class_code: str = Field(..., max_length=8, description="Class code") + description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") + description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + material_key: Optional[str] = Field(None, max_length=10, description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)") + unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)") + fraction: Optional[str] = Field(None, max_length=10, description="Mexican tariff fraction") + us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + sub_key: Optional[str] = Field(None, max_length=5, description="Sub classification key") + physical_review: Optional[int] = Field(None, description="Physical review indicator") + iva_exempt_fraction: Optional[str] = Field(None, max_length=4, description="IVA exempt fraction") + + class Config: + from_attributes = True + + +class ClassUpdateDTO(BaseModel): + """DTO para actualizar una clase""" + description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") + description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + material_key: Optional[str] = Field(None, max_length=10, description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)") + unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)") + fraction: Optional[str] = Field(None, max_length=10, description="Mexican tariff fraction") + us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + sub_key: Optional[str] = Field(None, max_length=5, description="Sub classification key") + physical_review: Optional[int] = Field(None, description="Physical review indicator") + iva_exempt_fraction: Optional[str] = Field(None, max_length=4, description="IVA exempt fraction") + + class Config: + from_attributes = True + + +class ClassResponseDTO(BaseModel): + """DTO para respuesta de clase""" + client_key: int + class_code: str + description_spanish: Optional[str] = None + description_english: Optional[str] = None + material_key: Optional[str] = None + unit_of_measure: Optional[str] = None + fraction: Optional[str] = None + us_fraction: Optional[str] = None + sub_key: Optional[str] = None + physical_review: Optional[int] = None + iva_exempt_fraction: Optional[str] = None + + class Config: + from_attributes = True + + +class ClassBasicDTO(BaseModel): + """DTO para información básica de clase""" + client_key: int + class_code: str + description_spanish: Optional[str] = None + description_english: Optional[str] = None + material_key: Optional[str] = None + fraction: Optional[str] = None + + class Config: + from_attributes = True + + +class ClassListDTO(BaseModel): + """DTO para lista de clases""" + classes: list[ClassBasicDTO] + total: int + page: int + size: int + + class Config: + from_attributes = True + + +class ClassSearchDTO(BaseModel): + """DTO para búsqueda de clases""" + client_key: Optional[int] = Field(None, description="Filter by client key") + class_code: Optional[str] = Field(None, description="Search by class code") + description: Optional[str] = Field(None, description="Search in descriptions") + material_key: Optional[str] = Field(None, description="Filter by material key") + fraction: Optional[str] = Field(None, description="Filter by tariff fraction") + physical_review: Optional[int] = Field(None, description="Filter by physical review indicator") + + class Config: + from_attributes = True + diff --git a/backend/api/v1/modules/a76/GClass/models.py b/backend/api/v1/modules/a76/GClass/models.py new file mode 100644 index 00000000..157b3bee --- /dev/null +++ b/backend/api/v1/modules/a76/GClass/models.py @@ -0,0 +1,60 @@ +""" +Modelos ORM para gestión de clases SCAII y SCAF +""" +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Numeric, SmallInteger, ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from core.database import Base +import enum + +# Importar modelos relacionados para type hints y relationships +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from api.v1.modules.a76.GParts.models import GPart + from api.v1.modules.public.reference_data.material_types.models import MaterialType + + +class GClass(Base): + """ + Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF + """ + __tablename__ = "gclasses" + + # Primary key compuesta + client_key = Column(Integer, primary_key=True, nullable=False) + class_code = Column(String(8), primary_key=True, nullable=False) + + # Basic information + description_spanish = Column(String(500), nullable=True) + description_english = Column(String(500), nullable=True) + + # Material and measurement + material_key = Column(String(10), ForeignKey('public.material_types.key'), nullable=True) # CLAVEMAT - homologated from TIPOMAT/TIPOMATEQUIPO + unit_of_measure = Column(String(5), nullable=True) # UNIMED - homologated from UNIMEDIDA + + # Tariff fractions + fraction = Column(String(10), nullable=True) # Mexican tariff fraction + us_fraction = Column(String(16), nullable=True) # FRACCIONAME - US tariff fraction + + # Additional classification + sub_key = Column(String(5), nullable=True) # CLAVESUB + physical_review = Column(SmallInteger, nullable=True) # REVFISICA + iva_exempt_fraction = Column(String(4), nullable=True) # FRACCIONEXENTAIVA + + # Relationships + material_type: "MaterialType" = relationship("MaterialType", foreign_keys=[material_key]) + + # Inverse relationship with GParts that have this class + parts: List["GPart"] = relationship( + "GPart", + primaryjoin="and_(GClass.client_key == GPart.client_key, GClass.class_code == GPart.part_class)", + foreign_keys="[GPart.client_key, GPart.part_class]", + viewonly=True, + back_populates="part_class_info" + ) + + def __repr__(self): + return f"" + + diff --git a/backend/api/v1/modules/a76/GClass/routes.py b/backend/api/v1/modules/a76/GClass/routes.py new file mode 100644 index 00000000..f0b906ab --- /dev/null +++ b/backend/api/v1/modules/a76/GClass/routes.py @@ -0,0 +1,261 @@ +""" +Endpoints API para gestión de clases SCAII y SCAF +""" +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session +from typing import List, Optional + +from core.database import get_core_db +from core.security import get_current_user, has_role +from .service import ClassService +from .dto import ( + ClassCreateDTO, + ClassUpdateDTO, + ClassResponseDTO, + ClassBasicDTO, + ClassListDTO, + ClassSearchDTO +) + +router = APIRouter(prefix="/classes", tags=["Classes"]) + + +@router.post("/", response_model=ClassResponseDTO, status_code=status.HTTP_201_CREATED) +async def create_class( + class_data: ClassCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Create a new class in the system + """ + service = ClassService(db) + return service.create_class(class_data) + + +@router.get("/", response_model=ClassListDTO) +async def list_classes( + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + client_key: Optional[int] = Query(None, description="Filter by client key"), + class_code: Optional[str] = Query(None, description="Search by class code"), + description: Optional[str] = Query(None, description="Search in descriptions"), + material_key: Optional[str] = Query(None, description="Filter by material key"), + fraction: Optional[str] = Query(None, description="Filter by tariff fraction"), + physical_review: Optional[int] = Query(None, description="Filter by physical review indicator"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + List classes with optional filters and pagination + """ + service = ClassService(db) + search_params = ClassSearchDTO( + client_key=client_key, + class_code=class_code, + description=description, + material_key=material_key, + fraction=fraction, + physical_review=physical_review + ) + return service.list_classes(skip, limit, search_params) + + +@router.get("/client/{client_key}", response_model=List[ClassBasicDTO]) +async def get_classes_by_client( + client_key: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get all classes for a specific client + """ + service = ClassService(db) + return service.search_by_client(client_key, skip, limit) + + +@router.get("/search/fraction/{fraction}", response_model=List[ClassBasicDTO]) +async def search_by_fraction( + fraction: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Search classes by tariff fraction + """ + service = ClassService(db) + return service.search_by_fraction(fraction) + + +@router.get("/search/material/{material_key}", response_model=List[ClassBasicDTO]) +async def search_by_material( + material_key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Search classes by material key + """ + service = ClassService(db) + return service.search_by_material(material_key) + + +@router.get("/search/unit-measure/{unit_of_measure}", response_model=List[ClassBasicDTO]) +async def get_classes_by_unit_measure( + unit_of_measure: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get classes by unit of measure + """ + service = ClassService(db) + return service.get_classes_by_unit_measure(unit_of_measure) + + +@router.get("/search/physical-review/{physical_review}", response_model=List[ClassBasicDTO]) +async def get_classes_by_physical_review( + physical_review: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get classes by physical review indicator + """ + service = ClassService(db) + return service.get_classes_by_physical_review(physical_review) + + +@router.get("/statistics", response_model=dict) +async def get_classes_statistics( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic classes statistics + """ + service = ClassService(db) + return service.get_classes_statistics() + + +@router.get("/{client_key}/{class_code}", response_model=ClassResponseDTO) +async def get_class( + client_key: int, + class_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get class by composite key (client_key + class_code) + """ + service = ClassService(db) + class_obj = service.get_class(client_key, class_code) + if not class_obj: + raise HTTPException( + status_code=404, + detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + ) + return class_obj + + +@router.put("/{client_key}/{class_code}", response_model=ClassResponseDTO) +async def update_class( + client_key: int, + class_code: str, + class_data: ClassUpdateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Update class information + """ + service = ClassService(db) + class_obj = service.update_class(client_key, class_code, class_data) + if not class_obj: + raise HTTPException( + status_code=404, + detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + ) + return class_obj + + +@router.delete("/{client_key}/{class_code}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_class( + client_key: int, + class_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Delete class from the system + + Note: This will completely remove the class from the system. + """ + service = ClassService(db) + if not service.delete_class(client_key, class_code): + raise HTTPException( + status_code=404, + detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + ) + + +# Endpoints específicos para información detallada +@router.get("/{client_key}/{class_code}/basic", response_model=ClassBasicDTO) +async def get_class_basic_info( + client_key: int, + class_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic information for a class + """ + service = ClassService(db) + class_obj = service.get_class(client_key, class_code) + if not class_obj: + raise HTTPException( + status_code=404, + detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + ) + + return ClassBasicDTO( + client_key=class_obj.client_key, + class_code=class_obj.class_code, + description_spanish=class_obj.description_spanish, + description_english=class_obj.description_english, + material_key=class_obj.material_key, + fraction=class_obj.fraction + ) + + +@router.get("/{client_key}/{class_code}/tariff", response_model=dict) +async def get_class_tariff_info( + client_key: int, + class_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get tariff information for a class (fractions, IVA exempt, etc.) + """ + service = ClassService(db) + class_obj = service.get_class(client_key, class_code) + if not class_obj: + raise HTTPException( + status_code=404, + detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + ) + + return { + "client_key": class_obj.client_key, + "class_code": class_obj.class_code, + "fraction": class_obj.fraction, + "us_fraction": class_obj.us_fraction, + "iva_exempt_fraction": class_obj.iva_exempt_fraction, + "sub_key": class_obj.sub_key, + "physical_review": class_obj.physical_review + } + + diff --git a/backend/api/v1/modules/a76/GClass/service.py b/backend/api/v1/modules/a76/GClass/service.py new file mode 100644 index 00000000..ceed5c56 --- /dev/null +++ b/backend/api/v1/modules/a76/GClass/service.py @@ -0,0 +1,294 @@ +""" +Capa de servicio para lógica de negocio de clases SCAII y SCAF +""" +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from sqlalchemy import or_, and_, func +from fastapi import HTTPException +from typing import List, Optional +import logging + +from .models import GClass +from .dto import ( + ClassCreateDTO, + ClassUpdateDTO, + ClassResponseDTO, + ClassBasicDTO, + ClassListDTO, + ClassSearchDTO +) + +logger = logging.getLogger(__name__) + + +class ClassService: + """Servicio para gestión de clases SCAII y SCAF""" + + def __init__(self, db: Session): + self.db = db + + def create_class(self, class_data: ClassCreateDTO) -> ClassResponseDTO: + """ + Crea una nueva clase en el sistema + + Args: + class_data: Datos de la clase a crear + + Returns: + ClassResponseDTO con información de la clase creada + + Raises: + HTTPException: Si la clase ya existe o error en la creación + """ + try: + # Verificar que no exista la clase + existing = self.db.query(GClass).filter( + and_( + GClass.client_key == class_data.client_key, + GClass.class_code == class_data.class_code + ) + ).first() + + if existing: + raise HTTPException( + status_code=400, + detail=f"Class with client_key '{class_data.client_key}' and class_code '{class_data.class_code}' already exists" + ) + + # Crear clase + db_class = GClass( + client_key=class_data.client_key, + class_code=class_data.class_code, + description_spanish=class_data.description_spanish, + description_english=class_data.description_english, + material_key=class_data.material_key, + unit_of_measure=class_data.unit_of_measure, + fraction=class_data.fraction, + us_fraction=class_data.us_fraction, + sub_key=class_data.sub_key, + physical_review=class_data.physical_review, + iva_exempt_fraction=class_data.iva_exempt_fraction + ) + + self.db.add(db_class) + self.db.commit() + self.db.refresh(db_class) + + logger.info(f"Class created: {db_class.client_key}-{db_class.class_code}") + + return ClassResponseDTO.model_validate(db_class) + + except IntegrityError as e: + self.db.rollback() + logger.error(f"IntegrityError creating class: {str(e)}") + raise HTTPException(status_code=400, detail="Class with this client_key and class_code already exists") + except HTTPException: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error creating class: {str(e)}") + raise HTTPException(status_code=500, detail="Error creating class") + + def get_class(self, client_key: int, class_code: str) -> Optional[ClassResponseDTO]: + """ + Obtiene una clase por clave compuesta + + Args: + client_key: Clave del cliente + class_code: Código de clase + + Returns: + ClassResponseDTO o None si no existe + """ + class_obj = self.db.query(GClass).filter( + and_( + GClass.client_key == client_key, + GClass.class_code == class_code + ) + ).first() + + if not class_obj: + return None + return ClassResponseDTO.model_validate(class_obj) + + def list_classes( + self, + skip: int = 0, + limit: int = 100, + search_params: Optional[ClassSearchDTO] = None + ) -> ClassListDTO: + """ + Lista clases con filtros + + Args: + skip: Número de registros a omitir + limit: Número máximo de registros a retornar + search_params: Parámetros de búsqueda + + Returns: + ClassListDTO con la lista paginada + """ + query = self.db.query(GClass) + + # Aplicar filtros si se proporcionan + if search_params: + if search_params.client_key: + query = query.filter(GClass.client_key == search_params.client_key) + + if search_params.class_code: + query = query.filter(GClass.class_code.ilike(f"%{search_params.class_code}%")) + + if search_params.description: + description_pattern = f"%{search_params.description}%" + query = query.filter( + or_( + GClass.description_spanish.ilike(description_pattern), + GClass.description_english.ilike(description_pattern) + ) + ) + + if search_params.material_key: + query = query.filter(GClass.material_key.ilike(f"%{search_params.material_key}%")) + + if search_params.fraction: + query = query.filter(GClass.fraction.ilike(f"%{search_params.fraction}%")) + + if search_params.physical_review is not None: + query = query.filter(GClass.physical_review == search_params.physical_review) + + # Contar total + total = query.count() + + # Aplicar paginación + classes = query.offset(skip).limit(limit).all() + + # Convertir a DTOs básicos + class_dtos = [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + + return ClassListDTO( + classes=class_dtos, + total=total, + page=(skip // limit) + 1 if limit > 0 else 1, + size=len(class_dtos) + ) + + def update_class(self, client_key: int, class_code: str, class_data: ClassUpdateDTO) -> Optional[ClassResponseDTO]: + """ + Actualiza una clase + + Args: + client_key: Clave del cliente + class_code: Código de clase + class_data: Datos a actualizar + + Returns: + ClassResponseDTO actualizado o None si no existe + """ + class_obj = self.db.query(GClass).filter( + and_( + GClass.client_key == client_key, + GClass.class_code == class_code + ) + ).first() + + if not class_obj: + return None + + try: + # Actualizar solo campos proporcionados + update_data = class_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(class_obj, field, value) + + self.db.commit() + self.db.refresh(class_obj) + logger.info(f"Class updated: {client_key}-{class_code}") + + return ClassResponseDTO.model_validate(class_obj) + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating class {client_key}-{class_code}: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating class") + + def delete_class(self, client_key: int, class_code: str) -> bool: + """ + Elimina una clase + + Args: + client_key: Clave del cliente + class_code: Código de clase + + Returns: + True si se eliminó, False si no existe + """ + class_obj = self.db.query(GClass).filter( + and_( + GClass.client_key == client_key, + GClass.class_code == class_code + ) + ).first() + + if not class_obj: + return False + + try: + self.db.delete(class_obj) + self.db.commit() + logger.info(f"Class deleted: {client_key}-{class_code}") + return True + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting class {client_key}-{class_code}: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting class") + + def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: + """Busca clases por fracción arancelaria""" + classes = self.db.query(GClass).filter(GClass.fraction.ilike(f"%{fraction}%")).all() + return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + + def search_by_client(self, client_key: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: + """Obtiene todas las clases de un cliente específico""" + classes = self.db.query(GClass).filter(GClass.client_key == client_key).offset(skip).limit(limit).all() + return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + + def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: + """Busca clases por clave de material""" + classes = self.db.query(GClass).filter(GClass.material_key.ilike(f"%{material_key}%")).all() + return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + + def get_classes_by_physical_review(self, physical_review: int) -> List[ClassBasicDTO]: + """Obtiene clases por indicador de revisión física""" + classes = self.db.query(GClass).filter(GClass.physical_review == physical_review).all() + return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + + def get_classes_statistics(self) -> dict: + """Obtiene estadísticas básicas de clases""" + total_classes = self.db.query(GClass).count() + + # Contar por clientes + clients_count = self.db.query(GClass.client_key).distinct().count() + + # Contar por revisión física + physical_review_stats = {} + for i in range(3): # Asumiendo valores 0, 1, 2 + count = self.db.query(GClass).filter(GClass.physical_review == i).count() + physical_review_stats[f"physical_review_{i}"] = count + + # Contar clases con fracciones + with_fraction = self.db.query(GClass).filter(GClass.fraction.isnot(None)).count() + with_us_fraction = self.db.query(GClass).filter(GClass.us_fraction.isnot(None)).count() + + return { + "total_classes": total_classes, + "clients_with_classes": clients_count, + "classes_with_fraction": with_fraction, + "classes_with_us_fraction": with_us_fraction, + **physical_review_stats + } + + def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]: + """Obtiene clases por unidad de medida""" + classes = self.db.query(GClass).filter(GClass.unit_of_measure == unit_of_measure).all() + return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] + diff --git a/backend/api/v1/modules/a76/GParts/__init__.py b/backend/api/v1/modules/a76/GParts/__init__.py new file mode 100644 index 00000000..7c89dc70 --- /dev/null +++ b/backend/api/v1/modules/a76/GParts/__init__.py @@ -0,0 +1,6 @@ +""" +Módulo de Tenants +""" +from .routes import router + +__all__ = ["router"] diff --git a/backend/api/v1/modules/a76/GParts/dto.py b/backend/api/v1/modules/a76/GParts/dto.py new file mode 100644 index 00000000..13524abd --- /dev/null +++ b/backend/api/v1/modules/a76/GParts/dto.py @@ -0,0 +1,182 @@ +""" +DTOs (Data Transfer Objects) para módulo de partes/componentes +Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS +""" +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from decimal import Decimal + + +class PartCreateDTO(BaseModel): + """DTO para crear una parte""" + client_key: int = Field(..., description="Client key") + part_number: str = Field(..., max_length=49, description="Part number") + fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction") + description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") + description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + part_class: Optional[str] = Field(None, max_length=8, description="Part class") + unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure") + commercial_part_number: Optional[str] = Field(None, max_length=70, description="Commercial part number") + country_of_origin: Optional[str] = Field(None, max_length=3, description="Country of origin code") + + # Pricing and currency + unit_cost: Optional[Decimal] = Field(None, description="Unit cost") + currency_type: Optional[str] = Field(None, max_length=2, description="Currency type") + currency_key: Optional[str] = Field(None, max_length=3, description="Currency key") + + # Weight information + unit_weight: Optional[Decimal] = Field(None, description="Unit weight") + weight_type: Optional[str] = Field(None, max_length=6, description="Weight type") + + # Classification and regulatory + us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + fda_key: Optional[str] = Field(None, max_length=20, description="FDA key") + fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key") + license_code: Optional[str] = Field(None, max_length=3, description="License code") + eccn: Optional[str] = Field(None, max_length=20, description="Export Control Classification Number") + export_code: Optional[str] = Field(None, max_length=2, description="Export code") + exclusion_symbol: Optional[str] = Field(None, max_length=19, description="Exclusion symbol") + + # Additional information + supplier: Optional[str] = Field(None, max_length=14, description="Supplier") + alternate_unit_measure: Optional[str] = Field(None, max_length=14, description="Alternate unit of measure") + added_value: Optional[Decimal] = Field(None, description="Added value") + + # Status and media + enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") + creation_date: Optional[int] = Field(None, description="Creation date") + part_photo: Optional[str] = Field(None, max_length=255, description="Part photo URL") + + class Config: + from_attributes = True + + +class PartUpdateDTO(BaseModel): + """DTO para actualizar una parte""" + fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction") + description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") + description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + part_class: Optional[str] = Field(None, max_length=8, description="Part class") + unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure") + commercial_part_number: Optional[str] = Field(None, max_length=70, description="Commercial part number") + country_of_origin: Optional[str] = Field(None, max_length=3, description="Country of origin code") + + # Pricing and currency + unit_cost: Optional[Decimal] = Field(None, description="Unit cost") + currency_type: Optional[str] = Field(None, max_length=2, description="Currency type") + currency_key: Optional[str] = Field(None, max_length=3, description="Currency key") + + # Weight information + unit_weight: Optional[Decimal] = Field(None, description="Unit weight") + weight_type: Optional[str] = Field(None, max_length=6, description="Weight type") + + # Classification and regulatory + us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + fda_key: Optional[str] = Field(None, max_length=20, description="FDA key") + fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key") + license_code: Optional[str] = Field(None, max_length=3, description="License code") + eccn: Optional[str] = Field(None, max_length=20, description="Export Control Classification Number") + export_code: Optional[str] = Field(None, max_length=2, description="Export code") + exclusion_symbol: Optional[str] = Field(None, max_length=19, description="Exclusion symbol") + + # Additional information + supplier: Optional[str] = Field(None, max_length=14, description="Supplier") + alternate_unit_measure: Optional[str] = Field(None, max_length=14, description="Alternate unit of measure") + added_value: Optional[Decimal] = Field(None, description="Added value") + + # Status and media + enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") + part_photo: Optional[str] = Field(None, max_length=255, description="Part photo URL") + + class Config: + from_attributes = True + + +class PartResponseDTO(BaseModel): + """DTO para respuesta de parte""" + client_key: int + part_number: str + fraction: Optional[str] = None + description_spanish: Optional[str] = None + description_english: Optional[str] = None + part_class: Optional[str] = None + unit_of_measure: Optional[str] = None + commercial_part_number: Optional[str] = None + country_of_origin: Optional[str] = None + + # Pricing and currency + unit_cost: Optional[Decimal] = None + currency_type: Optional[str] = None + currency_key: Optional[str] = None + + # Weight information + unit_weight: Optional[Decimal] = None + weight_type: Optional[str] = None + + # Classification and regulatory + us_fraction: Optional[str] = None + fda_key: Optional[str] = None + fcc_key: Optional[str] = None + license_code: Optional[str] = None + eccn: Optional[str] = None + export_code: Optional[str] = None + exclusion_symbol: Optional[str] = None + + # Additional information + supplier: Optional[str] = None + alternate_unit_measure: Optional[str] = None + added_value: Optional[Decimal] = None + + # Status and dates + enabled_disabled: Optional[int] = None + creation_date: Optional[int] = None + modification_date: Optional[int] = None + modification_date_iso: Optional[datetime] = None + + # Media + part_photo: Optional[str] = None + + class Config: + from_attributes = True + + +class PartBasicDTO(BaseModel): + """DTO para información básica de parte""" + client_key: int + part_number: str + description_spanish: Optional[str] = None + description_english: Optional[str] = None + part_class: Optional[str] = None + unit_cost: Optional[Decimal] = None + currency_key: Optional[str] = None + enabled_disabled: Optional[int] = None + + class Config: + from_attributes = True + + +class PartListDTO(BaseModel): + """DTO para lista de partes""" + parts: list[PartBasicDTO] + total: int + page: int + size: int + + class Config: + from_attributes = True + + +class PartSearchDTO(BaseModel): + """DTO para búsqueda de partes""" + client_key: Optional[int] = Field(None, description="Filter by client key") + part_number: Optional[str] = Field(None, description="Search by part number") + description: Optional[str] = Field(None, description="Search in descriptions") + fraction: Optional[str] = Field(None, description="Filter by tariff fraction") + supplier: Optional[str] = Field(None, description="Filter by supplier") + enabled_only: bool = Field(False, description="Show only enabled parts") + + class Config: + from_attributes = True + + diff --git a/backend/api/v1/modules/a76/GParts/models.py b/backend/api/v1/modules/a76/GParts/models.py new file mode 100644 index 00000000..4c0b1d04 --- /dev/null +++ b/backend/api/v1/modules/a76/GParts/models.py @@ -0,0 +1,87 @@ +""" +Modelos ORM para gestión de partes/componentes +""" +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Numeric, SmallInteger, ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from core.database import Base +import enum + +# Importar modelos relacionados para type hints y relationships +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from api.v1.modules.public.reference_data.countries.models import Country + from api.v1.modules.public.reference_data.currency_types.models import CurrencyType + from api.v1.modules.a76.GClass.models import GClass + + +class GPart(Base): + """ + Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) + """ + __tablename__ = "gparts" + + # Primary key compuesta + client_key = Column(Integer, primary_key=True, nullable=False) + part_number = Column(String(49), primary_key=True, nullable=False) + + # Basic information + fraction = Column(String(10), nullable=True) + description_spanish = Column(String(500), nullable=True) + description_english = Column(String(500), nullable=True) + part_class = Column(String(8), nullable=True) + unit_of_measure = Column(String(5), nullable=True) + commercial_part_number = Column(String(70), nullable=True) + country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key'), nullable=True) + + # Pricing and currency + unit_cost = Column(Numeric(23, 8), nullable=True) + currency_type = Column(String(2), nullable=True) + currency_key = Column(String(3), ForeignKey('public.currency_types.code'), nullable=True) + + # Weight information + unit_weight = Column(Numeric(19, 8), nullable=True) + weight_type = Column(String(6), nullable=True) + + # Classification and regulatory + us_fraction = Column(String(16), nullable=True) # FRACCIONAME + fda_key = Column(String(20), nullable=True) + fcc_key = Column(String(30), nullable=True) + license_code = Column(String(3), nullable=True) + eccn = Column(String(20), nullable=True) # Export Control Classification Number + export_code = Column(String(2), nullable=True) + exclusion_symbol = Column(String(19), nullable=True) # SIMBOLOEXCLIC + + # Additional information + supplier = Column(String(14), nullable=True) + alternate_unit_measure = Column(String(14), nullable=True) + added_value = Column(Numeric(23, 8), nullable=True) + + # Status and dates + enabled_disabled = Column(SmallInteger, nullable=True) + creation_date = Column(Integer, nullable=True) # FECHACREACIONPARTE + modification_date = Column(Integer, nullable=True) # FECHAMODIFICA + modification_date_iso = Column(DateTime(timezone=True), nullable=True) # FECHAMODIFICA_ISO + + # Media + part_photo = Column(String(255), nullable=True) + + # Relationships + country: "Country" = relationship("Country", foreign_keys=[country_of_origin]) + currency: "CurrencyType" = relationship("CurrencyType", foreign_keys=[currency_key]) + + # Relationship with GClass through composite foreign key + # Note: This requires both client_key and part_class to match client_key and class_code in GClass + part_class_info: "GClass" = relationship( + "GClass", + primaryjoin="and_(GPart.client_key == GClass.client_key, GPart.part_class == GClass.class_code)", + foreign_keys="[GPart.client_key, GPart.part_class]", + viewonly=True, + back_populates="parts" + ) + + def __repr__(self): + return f"" + + diff --git a/backend/api/v1/modules/a76/GParts/routes.py b/backend/api/v1/modules/a76/GParts/routes.py new file mode 100644 index 00000000..36486b04 --- /dev/null +++ b/backend/api/v1/modules/a76/GParts/routes.py @@ -0,0 +1,273 @@ +""" +Endpoints API para gestión de partes/componentes +""" +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session +from typing import List, Optional + +from core.database import get_core_db +from core.security import get_current_user, has_role +from .service import PartService +from .dto import ( + PartCreateDTO, + PartUpdateDTO, + PartResponseDTO, + PartBasicDTO, + PartListDTO, + PartSearchDTO +) + +router = APIRouter(prefix="/parts", tags=["Parts"]) + + +@router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED) +async def create_part( + part_data: PartCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Create a new part in the system + """ + service = PartService(db) + return service.create_part(part_data) + + +@router.get("/", response_model=PartListDTO) +async def list_parts( + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + client_key: Optional[int] = Query(None, description="Filter by client key"), + part_number: Optional[str] = Query(None, description="Search by part number"), + description: Optional[str] = Query(None, description="Search in descriptions"), + fraction: Optional[str] = Query(None, description="Filter by tariff fraction"), + supplier: Optional[str] = Query(None, description="Filter by supplier"), + enabled_only: bool = Query(False, description="Show only enabled parts"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + List parts with optional filters and pagination + """ + service = PartService(db) + search_params = PartSearchDTO( + client_key=client_key, + part_number=part_number, + description=description, + fraction=fraction, + supplier=supplier, + enabled_only=enabled_only + ) + return service.list_parts(skip, limit, search_params) + + +@router.get("/client/{client_key}", response_model=List[PartBasicDTO]) +async def get_parts_by_client( + client_key: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get all parts for a specific client + """ + service = PartService(db) + return service.search_by_client(client_key, skip, limit) + + +@router.get("/search/fraction/{fraction}", response_model=List[PartBasicDTO]) +async def search_by_fraction( + fraction: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Search parts by tariff fraction + """ + service = PartService(db) + return service.search_by_fraction(fraction) + + +@router.get("/search/supplier/{supplier}", response_model=List[PartBasicDTO]) +async def search_by_supplier( + supplier: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Search parts by supplier + """ + service = PartService(db) + return service.search_by_supplier(supplier) + + +@router.get("/search/country/{country_code}", response_model=List[PartBasicDTO]) +async def get_parts_by_country( + country_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get parts by country of origin + """ + service = PartService(db) + return service.get_parts_by_country(country_code) + + +@router.get("/statistics", response_model=dict) +async def get_parts_statistics( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic parts statistics + """ + service = PartService(db) + return service.get_parts_statistics() + + +@router.get("/{client_key}/{part_number}", response_model=PartResponseDTO) +async def get_part( + client_key: int, + part_number: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get part by composite key (client_key + part_number) + """ + service = PartService(db) + part = service.get_part(client_key, part_number) + if not part: + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + return part + + +@router.put("/{client_key}/{part_number}", response_model=PartResponseDTO) +async def update_part( + client_key: int, + part_number: str, + part_data: PartUpdateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Update part information + """ + service = PartService(db) + part = service.update_part(client_key, part_number, part_data) + if not part: + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + return part + + +@router.delete("/{client_key}/{part_number}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_part( + client_key: int, + part_number: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Delete part from the system + + Note: This will completely remove the part from the system. + """ + service = PartService(db) + if not service.delete_part(client_key, part_number): + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + + +@router.patch("/{client_key}/{part_number}/toggle-status", response_model=PartResponseDTO) +async def toggle_part_status( + client_key: int, + part_number: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Toggle part enabled/disabled status + """ + service = PartService(db) + part = service.toggle_status(client_key, part_number) + if not part: + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + return part + + +# Endpoints específicos para información detallada +@router.get("/{client_key}/{part_number}/basic", response_model=PartBasicDTO) +async def get_part_basic_info( + client_key: int, + part_number: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic information for a part + """ + service = PartService(db) + part = service.get_part(client_key, part_number) + if not part: + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + + return PartBasicDTO( + client_key=part.client_key, + part_number=part.part_number, + description_spanish=part.description_spanish, + description_english=part.description_english, + part_class=part.part_class, + unit_cost=part.unit_cost, + currency_key=part.currency_key, + enabled_disabled=part.enabled_disabled + ) + + +@router.get("/{client_key}/{part_number}/regulatory", response_model=dict) +async def get_part_regulatory_info( + client_key: int, + part_number: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get regulatory information for a part (FDA, FCC, ECCN, etc.) + """ + service = PartService(db) + part = service.get_part(client_key, part_number) + if not part: + raise HTTPException( + status_code=404, + detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + ) + + return { + "client_key": part.client_key, + "part_number": part.part_number, + "fraction": part.fraction, + "us_fraction": part.us_fraction, + "fda_key": part.fda_key, + "fcc_key": part.fcc_key, + "license_code": part.license_code, + "eccn": part.eccn, + "export_code": part.export_code, + "exclusion_symbol": part.exclusion_symbol + } + + diff --git a/backend/api/v1/modules/a76/GParts/service.py b/backend/api/v1/modules/a76/GParts/service.py new file mode 100644 index 00000000..a5815300 --- /dev/null +++ b/backend/api/v1/modules/a76/GParts/service.py @@ -0,0 +1,15 @@ +""" +Capa de servicio para lógica de negocio de partes/componentes +""" +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from sqlalchemy import or_, and_, func +from fastapi import HTTPException +from typing import List, Optional +import logging +from datetime import datetime + + + +logger = logging.getLogger(__name__) + diff --git a/backend/api/v1/modules/a76/client_&_provider/__init__.py b/backend/api/v1/modules/a76/client_&_provider/__init__.py new file mode 100644 index 00000000..7c89dc70 --- /dev/null +++ b/backend/api/v1/modules/a76/client_&_provider/__init__.py @@ -0,0 +1,6 @@ +""" +Módulo de Tenants +""" +from .routes import router + +__all__ = ["router"] diff --git a/backend/api/v1/modules/a76/client_&_provider/dto.py b/backend/api/v1/modules/a76/client_&_provider/dto.py new file mode 100644 index 00000000..be248661 --- /dev/null +++ b/backend/api/v1/modules/a76/client_&_provider/dto.py @@ -0,0 +1,165 @@ +""" +DTOs (Data Transfer Objects) para módulo de clientes y proveedores +Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS +""" +from pydantic import BaseModel, Field, EmailStr +from typing import Optional +from datetime import datetime +from decimal import Decimal + + +# DTOs para dirección +class ClientProviderAddressDTO(BaseModel): + """DTO para dirección de cliente/proveedor""" + municipality: Optional[str] = Field(None, max_length=150, description="Municipality") + streets: Optional[str] = Field(None, max_length=100, description="Streets") + neighborhood: Optional[str] = Field(None, max_length=40, description="Neighborhood") + interior_number: Optional[str] = Field(None, max_length=20, description="Interior number") + exterior_number: Optional[str] = Field(None, max_length=20, description="Exterior number") + postal_code: Optional[str] = Field(None, max_length=15, description="Postal code") + city: Optional[str] = Field(None, max_length=30, description="City") + state: Optional[str] = Field(None, max_length=30, description="State") + country: Optional[str] = Field(None, max_length=3, description="Country code") + phone: Optional[str] = Field(None, max_length=30, description="Phone number") + fax_number: Optional[str] = Field(None, max_length=30, description="Fax number") + email: Optional[str] = Field(None, max_length=100, description="Email address") + contact: Optional[str] = Field(None, max_length=50, description="Contact person") + reference: Optional[str] = Field(None, max_length=250, description="Reference") + + class Config: + from_attributes = True + + +# DTOs para programas +class ClientProviderProgramsDTO(BaseModel): + """DTO para programas de cliente/proveedor""" + program: Optional[str] = Field(None, max_length=7, description="Program") + program_number: Optional[str] = Field(None, max_length=40, description="Program number") + prosec: Optional[int] = Field(None, description="PROSEC") + prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") + secon_auth_date: Optional[int] = Field(None, description="SECON authorization date") + manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") + tax_id: Optional[str] = Field(None, max_length=30, description="Tax ID") + broker: Optional[str] = Field(None, max_length=6, description="Broker") + import_broker: Optional[str] = Field(None, max_length=6, description="Import broker") + transfer_key: Optional[str] = Field(None, max_length=8, description="Transfer key") + secon_authorization: Optional[str] = Field(None, max_length=20, description="SECON authorization") + applied_proportion: Optional[Decimal] = Field(None, description="Applied proportion") + is_certified_company: Optional[str] = Field(None, max_length=1, description="Is certified company") + certified_company_registry: Optional[str] = Field(None, max_length=40, description="Certified company registry") + donation_auth_number: Optional[str] = Field(None, max_length=50, description="Donation authorization number") + ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") + tax_registry_number: Optional[str] = Field(None, max_length=40, description="Tax registry number") + subassembly_service: Optional[int] = Field(None, description="Subassembly service") + autse_dates: Optional[int] = Field(None, description="AUTSE dates") + autse_number: Optional[str] = Field(None, max_length=300, description="AUTSE number") + + class Config: + from_attributes = True + + +# DTOs principales +class ClientProviderCreateDTO(BaseModel): + """DTO para crear cliente/proveedor""" + client_id: str = Field(..., max_length=8, description="Client ID") + type_nat_foreign: Optional[str] = Field(None, max_length=1, description="Type national/foreign") + name: Optional[str] = Field(None, max_length=256, description="Name") + short_name: Optional[str] = Field(None, max_length=10, description="Short name") + rfc: Optional[str] = Field(None, max_length=30, description="RFC") + curp: Optional[str] = Field(None, max_length=19, description="CURP") + client_or_provider: Optional[str] = Field(None, max_length=1, description="Client or provider") + linking: Optional[str] = Field(None, max_length=1, description="Linking") + transform_subassembly: Optional[str] = Field(None, max_length=1, description="Transform subassembly") + extra_information: Optional[str] = Field(None, max_length=399, description="Extra information") + web_key: Optional[str] = Field(None, max_length=40, description="Web key") + responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + position: Optional[str] = Field(None, max_length=30, description="Position") + incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm") + is_national_provider: Optional[str] = Field(None, max_length=2, description="Is national provider") + enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") + + # Nested DTOs + address: Optional[ClientProviderAddressDTO] = Field(None, description="Address information") + programs: Optional[ClientProviderProgramsDTO] = Field(None, description="Programs information") + + class Config: + from_attributes = True + + +class ClientProviderUpdateDTO(BaseModel): + """DTO para actualizar cliente/proveedor""" + type_nat_foreign: Optional[str] = Field(None, max_length=1, description="Type national/foreign") + name: Optional[str] = Field(None, max_length=256, description="Name") + short_name: Optional[str] = Field(None, max_length=10, description="Short name") + rfc: Optional[str] = Field(None, max_length=30, description="RFC") + curp: Optional[str] = Field(None, max_length=19, description="CURP") + client_or_provider: Optional[str] = Field(None, max_length=1, description="Client or provider") + linking: Optional[str] = Field(None, max_length=1, description="Linking") + transform_subassembly: Optional[str] = Field(None, max_length=1, description="Transform subassembly") + extra_information: Optional[str] = Field(None, max_length=399, description="Extra information") + web_key: Optional[str] = Field(None, max_length=40, description="Web key") + responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + position: Optional[str] = Field(None, max_length=30, description="Position") + incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm") + is_national_provider: Optional[str] = Field(None, max_length=2, description="Is national provider") + enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") + + # Nested DTOs + address: Optional[ClientProviderAddressDTO] = Field(None, description="Address information") + programs: Optional[ClientProviderProgramsDTO] = Field(None, description="Programs information") + + class Config: + from_attributes = True + + +class ClientProviderResponseDTO(BaseModel): + """DTO para respuesta de cliente/proveedor""" + client_id: str + type_nat_foreign: Optional[str] = None + name: Optional[str] = None + short_name: Optional[str] = None + rfc: Optional[str] = None + curp: Optional[str] = None + client_or_provider: Optional[str] = None + linking: Optional[str] = None + transform_subassembly: Optional[str] = None + extra_information: Optional[str] = None + web_key: Optional[str] = None + responsible: Optional[str] = None + position: Optional[str] = None + incoterm: Optional[str] = None + is_national_provider: Optional[str] = None + enabled_disabled: Optional[int] = None + + # Nested DTOs + address: Optional[ClientProviderAddressDTO] = None + programs: Optional[ClientProviderProgramsDTO] = None + + class Config: + from_attributes = True + + +# DTOs para respuestas específicas +class ClientProviderBasicDTO(BaseModel): + """DTO para información básica de cliente/proveedor""" + client_id: str + name: Optional[str] = None + short_name: Optional[str] = None + rfc: Optional[str] = None + client_or_provider: Optional[str] = None + enabled_disabled: Optional[int] = None + + class Config: + from_attributes = True + + +class ClientProviderListDTO(BaseModel): + """DTO para lista de clientes/proveedores""" + clients: list[ClientProviderBasicDTO] + total: int + page: int + size: int + + class Config: + from_attributes = True + diff --git a/backend/api/v1/modules/a76/client_&_provider/models.py b/backend/api/v1/modules/a76/client_&_provider/models.py new file mode 100644 index 00000000..2c9c36cc --- /dev/null +++ b/backend/api/v1/modules/a76/client_&_provider/models.py @@ -0,0 +1,105 @@ +""" +Modelos ORM para gestión de clientes y proveedores +""" +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, SmallInteger, Numeric, ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from core.database import Base +import enum + + +class GClientProvider(Base): + """ + Modelo para la tabla GClientesPro - Información de clientes y proveedores + """ + __tablename__ = "gclient_provider" + + # Primary key + client_id = Column(String(8), primary_key=True, nullable=False) + + # Basic information + type_nat_foreign = Column(String(1), nullable=True) # TIPO NACIONAL/EXTRANJERO + name = Column(String(256), nullable=True) + short_name = Column(String(10), nullable=True) + rfc = Column(String(30), nullable=True) + curp = Column(String(19), nullable=True) + client_or_provider = Column(String(1), nullable=True) + linking = Column(String(1), nullable=True) + transform_subassembly = Column(String(1), nullable=True) + extra_information = Column(String(399), nullable=True) + web_key = Column(String(40), nullable=True) + responsible = Column(String(80), nullable=True) + position = Column(String(30), nullable=True) + incoterm = Column(String(19), nullable=True) + is_national_provider = Column(String(2), nullable=True) + enabled_disabled = Column(SmallInteger, nullable=True) + + # Relationships + address = relationship("GClientProviderAddress", back_populates="client_provider", uselist=False, cascade="all, delete-orphan") + programs = relationship("GClientProviderPrograms", back_populates="client_provider", uselist=False, cascade="all, delete-orphan") + + +class GClientProviderAddress(Base): + """ + Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores + """ + __tablename__ = "gclient_provider_address" + + # Primary key (foreign key) + client_id = Column(String(8), ForeignKey('gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + + # Address information + municipality = Column(String(150), nullable=True) + streets = Column(String(100), nullable=True) + neighborhood = Column(String(40), nullable=True) + interior_number = Column(String(20), nullable=True) + exterior_number = Column(String(20), nullable=True) + postal_code = Column(String(15), nullable=True) + city = Column(String(30), nullable=True) + state = Column(String(30), nullable=True) + country = Column(String(3), nullable=True) + phone = Column(String(30), nullable=True) + fax_number = Column(String(30), nullable=True) + email = Column(String(100), nullable=True) + contact = Column(String(50), nullable=True) + reference = Column(String(250), nullable=True) + + # Relationship + client_provider = relationship("GClientProvider", back_populates="address") + + +class GClientProviderPrograms(Base): + """ + Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores + """ + __tablename__ = "gclient_provider_programs" + + # Primary key (foreign key) + client_id = Column(String(8), ForeignKey('gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + + # Program information + program = Column(String(7), nullable=True) + program_number = Column(String(40), nullable=True) + prosec = Column(SmallInteger, nullable=True) + prosec_authorization = Column(String(20), nullable=True) + secon_auth_date = Column(Integer, nullable=True) + manufacturer_id = Column(String(25), nullable=True) + tax_id = Column(String(30), nullable=True) + broker = Column(String(6), nullable=True) + import_broker = Column(String(6), nullable=True) + transfer_key = Column(String(8), nullable=True) + secon_authorization = Column(String(20), nullable=True) + applied_proportion = Column(Numeric(7, 2), nullable=True) + is_certified_company = Column(String(1), nullable=True) + certified_company_registry = Column(String(40), nullable=True) + donation_auth_number = Column(String(50), nullable=True) + ctpat_svi = Column(String(100), nullable=True) + tax_registry_number = Column(String(40), nullable=True) + subassembly_service = Column(SmallInteger, nullable=True) + autse_dates = Column(Integer, nullable=True) + autse_number = Column(String(300), nullable=True) + + # Relationship + client_provider = relationship("GClientProvider", back_populates="programs") + + diff --git a/backend/api/v1/modules/a76/client_&_provider/routes.py b/backend/api/v1/modules/a76/client_&_provider/routes.py new file mode 100644 index 00000000..fb9edd50 --- /dev/null +++ b/backend/api/v1/modules/a76/client_&_provider/routes.py @@ -0,0 +1,221 @@ +""" +Endpoints API para gestión de clientes y proveedores +""" +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session +from typing import List, Optional + +from core.database import get_core_db +from core.security import get_current_user, has_role +from .service import ClientProviderService +from .dto import ( + ClientProviderCreateDTO, + ClientProviderUpdateDTO, + ClientProviderResponseDTO, + ClientProviderBasicDTO, + ClientProviderListDTO +) + +router = APIRouter(prefix="/clients-providers", tags=["Clients & Providers"]) + + +@router.post("/", response_model=ClientProviderResponseDTO, status_code=status.HTTP_201_CREATED) +async def create_client_provider( + client_data: ClientProviderCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Create a new client or provider in the system + """ + service = ClientProviderService(db) + return service.create_client_provider(client_data) + + +@router.get("/", response_model=ClientProviderListDTO) +async def list_clients_providers( + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + search: Optional[str] = Query(None, description="Search text for name, RFC, or ID"), + client_or_provider: Optional[str] = Query(None, regex="^[CP]$", description="Filter by type: C=Client, P=Provider"), + enabled_only: bool = Query(False, description="Show only enabled records"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + List clients and providers with optional filters and pagination + """ + service = ClientProviderService(db) + return service.list_clients_providers(skip, limit, search, client_or_provider, enabled_only) + + +@router.get("/clients", response_model=List[ClientProviderBasicDTO]) +async def get_clients_only( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get only clients (client_or_provider = 'C') + """ + service = ClientProviderService(db) + return service.get_clients_only(skip, limit) + + +@router.get("/providers", response_model=List[ClientProviderBasicDTO]) +async def get_providers_only( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get only providers (client_or_provider = 'P') + """ + service = ClientProviderService(db) + return service.get_providers_only(skip, limit) + + +@router.get("/search/rfc/{rfc}", response_model=List[ClientProviderBasicDTO]) +async def search_by_rfc( + rfc: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Search clients/providers by RFC + """ + service = ClientProviderService(db) + return service.search_by_rfc(rfc) + + +@router.get("/{client_id}", response_model=ClientProviderResponseDTO) +async def get_client_provider( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get client/provider by ID with all related information + """ + service = ClientProviderService(db) + client = service.get_client_provider(client_id) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + return client + + +@router.put("/{client_id}", response_model=ClientProviderResponseDTO) +async def update_client_provider( + client_id: str, + client_data: ClientProviderUpdateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Update client/provider information + """ + service = ClientProviderService(db) + client = service.update_client_provider(client_id, client_data) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + return client + + +@router.delete("/{client_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_client_provider( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Delete client/provider from the system + + Note: This will completely remove the client/provider and all related data. + """ + service = ClientProviderService(db) + if not service.delete_client_provider(client_id): + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + + +@router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO) +async def toggle_client_provider_status( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Toggle client/provider enabled/disabled status + """ + service = ClientProviderService(db) + client = service.toggle_status(client_id) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + return client + + +# Endpoints específicos para información detallada +@router.get("/{client_id}/address", response_model=dict) +async def get_client_provider_address( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get only address information for a client/provider + """ + service = ClientProviderService(db) + client = service.get_client_provider(client_id) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + + return { + "client_id": client.client_id, + "address": client.address + } + + +@router.get("/{client_id}/programs", response_model=dict) +async def get_client_provider_programs( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get only programs information for a client/provider + """ + service = ClientProviderService(db) + client = service.get_client_provider(client_id) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + + return { + "client_id": client.client_id, + "programs": client.programs + } + + +@router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO) +async def get_client_provider_basic_info( + client_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic information for a client/provider (without address and programs) + """ + service = ClientProviderService(db) + client = service.get_client_provider(client_id) + if not client: + raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + + return ClientProviderBasicDTO( + client_id=client.client_id, + name=client.name, + short_name=client.short_name, + rfc=client.rfc, + client_or_provider=client.client_or_provider, + enabled_disabled=client.enabled_disabled + ) + diff --git a/backend/api/v1/modules/a76/client_&_provider/service.py b/backend/api/v1/modules/a76/client_&_provider/service.py new file mode 100644 index 00000000..a9573c10 --- /dev/null +++ b/backend/api/v1/modules/a76/client_&_provider/service.py @@ -0,0 +1,309 @@ +""" +Capa de servicio para lógica de negocio de clientes y proveedores +""" +from sqlalchemy.orm import Session, joinedload +from sqlalchemy.exc import IntegrityError +from sqlalchemy import or_, and_ +from fastapi import HTTPException +from typing import List, Optional +import logging + +from .models import GClientProvider, GClientProviderAddress, GClientProviderPrograms +from .dto import ( + ClientProviderCreateDTO, + ClientProviderUpdateDTO, + ClientProviderResponseDTO, + ClientProviderBasicDTO, + ClientProviderListDTO, + ClientProviderAddressDTO, + ClientProviderProgramsDTO +) + +logger = logging.getLogger(__name__) + + +class ClientProviderService: + """Servicio para gestión de clientes y proveedores""" + + def __init__(self, db: Session): + self.db = db + + def create_client_provider(self, client_data: ClientProviderCreateDTO) -> ClientProviderResponseDTO: + """ + Crea un nuevo cliente/proveedor en el sistema + + Args: + client_data: Datos del cliente/proveedor a crear + + Returns: + ClientProviderResponseDTO con información del cliente/proveedor creado + + Raises: + HTTPException: Si el cliente ya existe o error en la creación + """ + try: + # Verificar que no exista el cliente + existing = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_data.client_id).first() + if existing: + raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists") + + # Crear cliente/proveedor principal + db_client = GClientProvider( + client_id=client_data.client_id, + type_nat_foreign=client_data.type_nat_foreign, + name=client_data.name, + short_name=client_data.short_name, + rfc=client_data.rfc, + curp=client_data.curp, + client_or_provider=client_data.client_or_provider, + linking=client_data.linking, + transform_subassembly=client_data.transform_subassembly, + extra_information=client_data.extra_information, + web_key=client_data.web_key, + responsible=client_data.responsible, + position=client_data.position, + incoterm=client_data.incoterm, + is_national_provider=client_data.is_national_provider, + enabled_disabled=client_data.enabled_disabled + ) + + self.db.add(db_client) + self.db.flush() # Para obtener el ID antes del commit + + # Crear dirección si se proporciona + if client_data.address: + db_address = GClientProviderAddress( + client_id=client_data.client_id, + **client_data.address.model_dump(exclude_unset=True) + ) + self.db.add(db_address) + + # Crear programas si se proporciona + if client_data.programs: + db_programs = GClientProviderPrograms( + client_id=client_data.client_id, + **client_data.programs.model_dump(exclude_unset=True) + ) + self.db.add(db_programs) + + self.db.commit() + self.db.refresh(db_client) + + logger.info(f"Client/Provider created: {db_client.client_id} - {db_client.name}") + + return self._get_client_with_relations(client_data.client_id) + + except IntegrityError as e: + self.db.rollback() + logger.error(f"IntegrityError creating client/provider: {str(e)}") + raise HTTPException(status_code=400, detail="Client/Provider with this ID already exists") + except HTTPException: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error creating client/provider: {str(e)}") + raise HTTPException(status_code=500, detail="Error creating client/provider") + + def get_client_provider(self, client_id: str) -> Optional[ClientProviderResponseDTO]: + """ + Obtiene un cliente/proveedor por ID + + Args: + client_id: ID del cliente/proveedor + + Returns: + ClientProviderResponseDTO o None si no existe + """ + return self._get_client_with_relations(client_id) + + def _get_client_with_relations(self, client_id: str) -> Optional[ClientProviderResponseDTO]: + """Método privado para obtener cliente con relaciones""" + client = self.db.query(GClientProvider).options( + joinedload(GClientProvider.address), + joinedload(GClientProvider.programs) + ).filter(GClientProvider.client_id == client_id).first() + + if not client: + return None + return ClientProviderResponseDTO.model_validate(client) + + def list_clients_providers( + self, + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + client_or_provider: Optional[str] = None, + enabled_only: bool = False + ) -> ClientProviderListDTO: + """ + Lista clientes/proveedores con filtros + + Args: + skip: Número de registros a omitir + limit: Número máximo de registros a retornar + search: Texto de búsqueda (nombre, RFC, ID) + client_or_provider: Filtrar por tipo (C=Cliente, P=Proveedor) + enabled_only: Si True, solo retorna activos + + Returns: + ClientProviderListDTO con la lista paginada + """ + query = self.db.query(GClientProvider) + + # Aplicar filtros + if search: + search_pattern = f"%{search}%" + query = query.filter( + or_( + GClientProvider.name.ilike(search_pattern), + GClientProvider.short_name.ilike(search_pattern), + GClientProvider.rfc.ilike(search_pattern), + GClientProvider.client_id.ilike(search_pattern) + ) + ) + + if client_or_provider: + query = query.filter(GClientProvider.client_or_provider == client_or_provider) + + if enabled_only: + query = query.filter(GClientProvider.enabled_disabled == 1) + + # Contar total + total = query.count() + + # Aplicar paginación + clients = query.offset(skip).limit(limit).all() + + # Convertir a DTOs básicos + client_dtos = [ClientProviderBasicDTO.model_validate(client) for client in clients] + + return ClientProviderListDTO( + clients=client_dtos, + total=total, + page=(skip // limit) + 1 if limit > 0 else 1, + size=len(client_dtos) + ) + + def update_client_provider(self, client_id: str, client_data: ClientProviderUpdateDTO) -> Optional[ClientProviderResponseDTO]: + """ + Actualiza un cliente/proveedor + + Args: + client_id: ID del cliente/proveedor a actualizar + client_data: Datos a actualizar + + Returns: + ClientProviderResponseDTO actualizado o None si no existe + """ + client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + if not client: + return None + + try: + # Actualizar campos del cliente principal + update_data = client_data.model_dump(exclude_unset=True, exclude={'address', 'programs'}) + for field, value in update_data.items(): + setattr(client, field, value) + + # Actualizar dirección + if client_data.address: + address = self.db.query(GClientProviderAddress).filter(GClientProviderAddress.client_id == client_id).first() + if address: + # Actualizar dirección existente + address_data = client_data.address.model_dump(exclude_unset=True) + for field, value in address_data.items(): + setattr(address, field, value) + else: + # Crear nueva dirección + address = GClientProviderAddress( + client_id=client_id, + **client_data.address.model_dump(exclude_unset=True) + ) + self.db.add(address) + + # Actualizar programas + if client_data.programs: + programs = self.db.query(GClientProviderPrograms).filter(GClientProviderPrograms.client_id == client_id).first() + if programs: + # Actualizar programas existentes + programs_data = client_data.programs.model_dump(exclude_unset=True) + for field, value in programs_data.items(): + setattr(programs, field, value) + else: + # Crear nuevos programas + programs = GClientProviderPrograms( + client_id=client_id, + **client_data.programs.model_dump(exclude_unset=True) + ) + self.db.add(programs) + + self.db.commit() + logger.info(f"Client/Provider updated: {client_id}") + + return self._get_client_with_relations(client_id) + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating client/provider {client_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating client/provider") + + def delete_client_provider(self, client_id: str) -> bool: + """ + Elimina un cliente/proveedor + + Args: + client_id: ID del cliente/proveedor a eliminar + + Returns: + True si se eliminó, False si no existe + """ + client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + if not client: + return False + + try: + self.db.delete(client) # Las relaciones se eliminan en cascada + self.db.commit() + logger.info(f"Client/Provider deleted: {client_id}") + return True + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting client/provider {client_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting client/provider") + + def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: + """Obtiene solo clientes (C)""" + query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'C') + clients = query.offset(skip).limit(limit).all() + return [ClientProviderBasicDTO.model_validate(client) for client in clients] + + def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: + """Obtiene solo proveedores (P)""" + query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'P') + providers = query.offset(skip).limit(limit).all() + return [ClientProviderBasicDTO.model_validate(provider) for provider in providers] + + def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]: + """Busca clientes/proveedores por RFC""" + clients = self.db.query(GClientProvider).filter(GClientProvider.rfc.ilike(f"%{rfc}%")).all() + return [ClientProviderBasicDTO.model_validate(client) for client in clients] + + def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]: + """Cambia el estado habilitado/deshabilitado""" + client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + if not client: + return None + + # Toggle status (1 = habilitado, 0 = deshabilitado) + client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0 + + try: + self.db.commit() + logger.info(f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}") + return self._get_client_with_relations(client_id) + except Exception as e: + self.db.rollback() + logger.error(f"Error toggling status for {client_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating status") + + diff --git a/backend/api/v1/modules/a76/company/__init__.py b/backend/api/v1/modules/a76/company/__init__.py new file mode 100644 index 00000000..7c89dc70 --- /dev/null +++ b/backend/api/v1/modules/a76/company/__init__.py @@ -0,0 +1,6 @@ +""" +Módulo de Tenants +""" +from .routes import router + +__all__ = ["router"] diff --git a/backend/api/v1/modules/a76/company/dto.py b/backend/api/v1/modules/a76/company/dto.py new file mode 100644 index 00000000..592dd07b --- /dev/null +++ b/backend/api/v1/modules/a76/company/dto.py @@ -0,0 +1,157 @@ +""" +DTOs (Data Transfer Objects) para módulo de empresa +Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS +""" +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + + +class CompanyCreateDTO(BaseModel): + """DTO para crear una empresa""" + id: str = Field(default='EMP', max_length=3, description="Company ID") + consecutive: bool = Field(default=True, description="Unique record control") + name: Optional[str] = Field(None, max_length=255, description="Company name") + rfc: Optional[str] = Field(None, max_length=30, description="Company RFC") + main_activity: Optional[str] = Field(None, max_length=255, description="Main activity") + + # Program information + program: Optional[str] = Field(None, max_length=10, description="Program") + program_number: Optional[str] = Field(None, max_length=40, description="Program number") + prosec: Optional[int] = Field(None, description="PROSEC") + prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") + + # Identifiers + manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") + broker_company: Optional[str] = Field(None, max_length=10, description="Broker company") + + # Responsible person + responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name") + responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name") + responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name") + responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC") + position: Optional[str] = Field(None, max_length=30, description="Responsible position") + + # Configuration + logo: Optional[str] = Field(None, max_length=255, description="Company logo") + has_express_line: Optional[bool] = Field(None, description="Has express line") + order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type") + previous_code: Optional[int] = Field(None, description="Previous code") + is_service_company: Optional[bool] = Field(None, description="Is service company") + + # Client and subassembly + client_name: Optional[str] = Field(None, max_length=300, description="Client name") + subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode") + + # Additional information + curp: Optional[str] = Field(None, max_length=19, description="CURP") + inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name") + ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") + trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number") + prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key") + seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment") + + class Config: + from_attributes = True + + +class CompanyUpdateDTO(BaseModel): + """DTO para actualizar una empresa""" + name: Optional[str] = Field(None, max_length=255, description="Company name") + rfc: Optional[str] = Field(None, max_length=30, description="Company RFC") + main_activity: Optional[str] = Field(None, max_length=255, description="Main activity") + + # Program information + program: Optional[str] = Field(None, max_length=10, description="Program") + program_number: Optional[str] = Field(None, max_length=40, description="Program number") + prosec: Optional[int] = Field(None, description="PROSEC") + prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") + + # Identifiers + manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") + broker_company: Optional[str] = Field(None, max_length=10, description="Broker company") + + # Responsible person + responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name") + responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name") + responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name") + responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC") + position: Optional[str] = Field(None, max_length=30, description="Responsible position") + + # Configuration + logo: Optional[str] = Field(None, max_length=255, description="Company logo") + has_express_line: Optional[bool] = Field(None, description="Has express line") + order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type") + previous_code: Optional[int] = Field(None, description="Previous code") + is_service_company: Optional[bool] = Field(None, description="Is service company") + + # Client and subassembly + client_name: Optional[str] = Field(None, max_length=300, description="Client name") + subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode") + + # Additional information + curp: Optional[str] = Field(None, max_length=19, description="CURP") + inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name") + ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") + trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number") + prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key") + seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment") + + class Config: + from_attributes = True + + +class CompanyResponseDTO(BaseModel): + """DTO para respuesta de empresa""" + id: str + consecutive: bool + name: Optional[str] = None + rfc: Optional[str] = None + main_activity: Optional[str] = None + + # Program information + program: Optional[str] = None + program_number: Optional[str] = None + prosec: Optional[int] = None + prosec_authorization: Optional[str] = None + + # Identifiers + manufacturer_id: Optional[str] = None + broker_company: Optional[str] = None + + # Responsible person + responsible: Optional[str] = None + responsible_name: Optional[str] = None + responsible_last_name: Optional[str] = None + responsible_mother_last_name: Optional[str] = None + responsible_rfc: Optional[str] = None + position: Optional[str] = None + + # Configuration + logo: Optional[str] = None + has_express_line: Optional[bool] = None + order_format_type: Optional[str] = None + previous_code: Optional[int] = None + is_service_company: Optional[bool] = None + + # Client and subassembly + client_name: Optional[str] = None + subassembly_mode: Optional[str] = None + + # Additional information + curp: Optional[str] = None + inter_db_name: Optional[str] = None + ctpat_svi: Optional[str] = None + trusted_exporter_number: Optional[str] = None + prevalidator_key: Optional[str] = None + seventh_amendment: Optional[bool] = None + + # Timestamps + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + diff --git a/backend/api/v1/modules/a76/company/models.py b/backend/api/v1/modules/a76/company/models.py new file mode 100644 index 00000000..bbba06f3 --- /dev/null +++ b/backend/api/v1/modules/a76/company/models.py @@ -0,0 +1,68 @@ +""" +Modelos ORM para gestión de empresa +""" +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, SmallInteger +from sqlalchemy.sql import func +from core.database import Base +import enum + + +class GCompany(Base): + """ + Modelo para la tabla GCompany - Información de la empresa + """ + __tablename__ = "gcompany" + + # Primary key + id = Column(String(3), primary_key=True, default='EMP', nullable=False) + + # Control de registro único + consecutive = Column(Boolean, unique=True, default=True, nullable=False) + + # Información básica de la empresa + name = Column(String(255), nullable=True) + rfc = Column(String(30), nullable=True) + main_activity = Column(String(255), nullable=True) + + # Información del programa + program = Column(String(10), nullable=True) + program_number = Column(String(40), nullable=True) + prosec = Column(SmallInteger, nullable=True) + prosec_authorization = Column(String(20), nullable=True) + + # Identificadores + manufacturer_id = Column(String(25), nullable=True) + broker_company = Column(String(10), nullable=True) + + # Responsable + responsible = Column(String(80), nullable=True) + responsible_name = Column(String(20), nullable=True) + responsible_last_name = Column(String(20), nullable=True) + responsible_mother_last_name = Column(String(20), nullable=True) + responsible_rfc = Column(String(30), nullable=True) + position = Column(String(30), nullable=True) + + # Configuración + logo = Column(String(255), nullable=True) + has_express_line = Column(Boolean, nullable=True) + order_format_type = Column(String(19), nullable=True) + previous_code = Column(SmallInteger, nullable=True) + is_service_company = Column(Boolean, nullable=True) + + # Cliente y submaquila + client_name = Column(String(300), nullable=True) + subassembly_mode = Column(String(7), nullable=True) + + # Información adicional + curp = Column(String(19), nullable=True) + inter_db_name = Column(String(100), nullable=True) + ctpat_svi = Column(String(100), nullable=True) + trusted_exporter_number = Column(String(50), nullable=True) + prevalidator_key = Column(String(20), nullable=True) + seventh_amendment = Column(Boolean, nullable=True) # FINALCONTADORAELECTRONICO renombrado + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + + diff --git a/backend/api/v1/modules/a76/company/routes.py b/backend/api/v1/modules/a76/company/routes.py new file mode 100644 index 00000000..4473591d --- /dev/null +++ b/backend/api/v1/modules/a76/company/routes.py @@ -0,0 +1,176 @@ +""" +Endpoints API para gestión de empresa +""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import Optional + +from core.database import get_core_db +from core.security import get_current_user, has_role +from .service import CompanyService +from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO + +router = APIRouter(prefix="/company", tags=["Company"]) + + +@router.post("/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED) +async def create_company( + company_data: CompanyCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Create a new company in the system + + Only one company can exist per system due to the unique consecutive field. + """ + service = CompanyService(db) + return service.create_company(company_data) + + +@router.get("/", response_model=Optional[CompanyResponseDTO]) +async def get_company( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get the registered company information + + Returns the unique company in the system or None if it doesn't exist. + """ + service = CompanyService(db) + company = service.get_company() + if not company: + raise HTTPException(status_code=404, detail="No company found") + return company + + +@router.get("/{company_id}", response_model=CompanyResponseDTO) +async def get_company_by_id( + company_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get company by specific ID + """ + service = CompanyService(db) + company = service.get_company_by_id(company_id) + if not company: + raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") + return company + + +@router.put("/{company_id}", response_model=CompanyResponseDTO) +async def update_company( + company_id: str, + company_data: CompanyUpdateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Update company information + """ + service = CompanyService(db) + company = service.update_company(company_id, company_data) + if not company: + raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") + return company + + +@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_company( + company_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Delete company from the system + + Note: This will completely remove the company from the system. + """ + service = CompanyService(db) + if not service.delete_company(company_id): + raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") + + +@router.get("/status/exists", response_model=dict) +async def check_company_exists( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Check if a company is registered in the system + """ + service = CompanyService(db) + exists = service.exists_company() + return {"exists": exists, "message": "Company found" if exists else "No company registered"} + + +# Specific endpoints for important fields +@router.get("/info/basic", response_model=dict) +async def get_company_basic_info( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get basic company information (name, RFC, main activity) + """ + service = CompanyService(db) + company = service.get_company() + if not company: + raise HTTPException(status_code=404, detail="No company found") + + return { + "name": company.name, + "rfc": company.rfc, + "main_activity": company.main_activity, + "logo": company.logo + } + + +@router.get("/info/responsible", response_model=dict) +async def get_company_responsible_info( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get company responsible person information + """ + service = CompanyService(db) + company = service.get_company() + if not company: + raise HTTPException(status_code=404, detail="No company found") + + return { + "responsible": company.responsible, + "responsible_name": company.responsible_name, + "responsible_last_name": company.responsible_last_name, + "responsible_mother_last_name": company.responsible_mother_last_name, + "responsible_rfc": company.responsible_rfc, + "position": company.position + } + + +@router.get("/info/program", response_model=dict) +async def get_company_program_info( + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Get company program information + """ + service = CompanyService(db) + company = service.get_company() + if not company: + raise HTTPException(status_code=404, detail="No company found") + + return { + "program": company.program, + "program_number": company.program_number, + "prosec": company.prosec, + "prosec_authorization": company.prosec_authorization, + "manufacturer_id": company.manufacturer_id + } + + diff --git a/backend/api/v1/modules/a76/company/service.py b/backend/api/v1/modules/a76/company/service.py new file mode 100644 index 00000000..405ad1a2 --- /dev/null +++ b/backend/api/v1/modules/a76/company/service.py @@ -0,0 +1,184 @@ +""" +Capa de servicio para lógica de negocio de empresa +""" +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from fastapi import HTTPException +from typing import List, Optional +import logging + +from .models import GCompany +from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO + +logger = logging.getLogger(__name__) + + +class CompanyService: + """Servicio para gestión de empresa""" + + def __init__(self, db: Session): + self.db = db + + def create_company(self, company_data: CompanyCreateDTO) -> CompanyResponseDTO: + """ + Crea una nueva empresa en el sistema + + Args: + company_data: Datos de la empresa a crear + + Returns: + CompanyResponseDTO con información de la empresa creada + + Raises: + HTTPException: Si ya existe una empresa o error en la creación + """ + try: + # Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único) + existing = self.db.query(GCompany).filter(GCompany.consecutive == True).first() + if existing: + raise HTTPException(status_code=400, detail="A company is already registered in the system") + + # Crear empresa + db_company = GCompany( + id=company_data.id, + consecutive=company_data.consecutive, + name=company_data.name, + rfc=company_data.rfc, + main_activity=company_data.main_activity, + program=company_data.program, + program_number=company_data.program_number, + prosec=company_data.prosec, + prosec_authorization=company_data.prosec_authorization, + manufacturer_id=company_data.manufacturer_id, + broker_company=company_data.broker_company, + responsible=company_data.responsible, + responsible_name=company_data.responsible_name, + responsible_last_name=company_data.responsible_last_name, + responsible_mother_last_name=company_data.responsible_mother_last_name, + responsible_rfc=company_data.responsible_rfc, + position=company_data.position, + logo=company_data.logo, + has_express_line=company_data.has_express_line, + order_format_type=company_data.order_format_type, + previous_code=company_data.previous_code, + is_service_company=company_data.is_service_company, + client_name=company_data.client_name, + subassembly_mode=company_data.subassembly_mode, + curp=company_data.curp, + inter_db_name=company_data.inter_db_name, + ctpat_svi=company_data.ctpat_svi, + trusted_exporter_number=company_data.trusted_exporter_number, + prevalidator_key=company_data.prevalidator_key, + seventh_amendment=company_data.seventh_amendment + ) + + self.db.add(db_company) + self.db.commit() + self.db.refresh(db_company) + + logger.info(f"Company created: {db_company.id} - {db_company.name}") + + return CompanyResponseDTO.model_validate(db_company) + + except IntegrityError as e: + self.db.rollback() + logger.error(f"IntegrityError creating company: {str(e)}") + raise HTTPException(status_code=400, detail="Integrity error: A company already exists in the system") + except HTTPException: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error creating company: {str(e)}") + raise HTTPException(status_code=500, detail="Error creating company") + + def get_company(self) -> Optional[CompanyResponseDTO]: + """ + Obtiene la empresa (solo puede haber una) + + Returns: + CompanyResponseDTO o None si no existe + """ + company = self.db.query(GCompany).filter(GCompany.consecutive == True).first() + if not company: + return None + return CompanyResponseDTO.model_validate(company) + + def get_company_by_id(self, company_id: str) -> Optional[CompanyResponseDTO]: + """ + Obtiene una empresa por ID + + Args: + company_id: ID de la empresa + + Returns: + CompanyResponseDTO o None si no existe + """ + company = self.db.query(GCompany).filter(GCompany.id == company_id).first() + if not company: + return None + return CompanyResponseDTO.model_validate(company) + + def update_company(self, company_id: str, company_data: CompanyUpdateDTO) -> Optional[CompanyResponseDTO]: + """ + Actualiza una empresa + + Args: + company_id: ID de la empresa a actualizar + company_data: Datos a actualizar + + Returns: + CompanyResponseDTO actualizada o None si no existe + """ + company = self.db.query(GCompany).filter(GCompany.id == company_id).first() + if not company: + return None + + # Actualizar solo campos proporcionados + update_data = company_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(company, field, value) + + try: + self.db.commit() + self.db.refresh(company) + logger.info(f"Company updated: {company_id}") + return CompanyResponseDTO.model_validate(company) + except Exception as e: + self.db.rollback() + logger.error(f"Error updating company {company_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating company") + + def delete_company(self, company_id: str) -> bool: + """ + Elimina una empresa + + Args: + company_id: ID de la empresa a eliminar + + Returns: + True si se eliminó, False si no existe + """ + company = self.db.query(GCompany).filter(GCompany.id == company_id).first() + if not company: + return False + + try: + self.db.delete(company) + self.db.commit() + logger.info(f"Company deleted: {company_id}") + return True + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting company {company_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting company") + + def exists_company(self) -> bool: + """ + Verifica si existe una empresa registrada + + Returns: + True si existe una empresa, False en caso contrario + """ + return self.db.query(GCompany).filter(GCompany.consecutive == True).first() is not None + + diff --git a/backend/api/v1/router.py b/backend/api/v1/router.py index ffb93465..88480cfe 100644 --- a/backend/api/v1/router.py +++ b/backend/api/v1/router.py @@ -26,6 +26,14 @@ from .modules.public.reference_data.code_pedimento_regimens.routes import router from .modules.public.reference_data.pedimento_regimens.routes import router as pedimento_regimens_router from .modules.public.reference_data.incoterms.routes import router as incoterms_router from .modules.a76.licenses import router as licenses_router +from .modules.a76.company.routes import router as company_router +from .modules.a76.GParts.routes import router as gparts_router +from .modules.a76.GClass.routes import router as gclass_router + +# Import module with special character using importlib +import importlib +client_provider_module = importlib.import_module('.modules.a76.client_&_provider.routes', package='api.v1') +client_provider_router = client_provider_module.router # Router principal router = APIRouter() @@ -34,6 +42,10 @@ router = APIRouter() router.include_router(auth_router) router.include_router(tenants_router) router.include_router(licenses_router) +router.include_router(company_router) +router.include_router(client_provider_router) +router.include_router(gparts_router) +router.include_router(gclass_router) router.include_router(pedimento_codes_router) router.include_router(payment_methods_router) router.include_router(containers_router) diff --git a/docs/RELATIONSHIPS.md b/docs/RELATIONSHIPS.md new file mode 100644 index 00000000..362ca6fe --- /dev/null +++ b/docs/RELATIONSHIPS.md @@ -0,0 +1,107 @@ +# Relaciones entre Modelos A76 + +## Resumen de Relaciones Establecidas + +### GPart (Tabla: gparts) +El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI. + +#### Relaciones: + +1. **Con Country (public.countries)** + - Campo: `country_of_origin` → `countries.m3_key` + - Relación: Many-to-One + - Propósito: País de origen de la parte + +2. **Con CurrencyType (public.currency_types)** + - Campo: `currency_key` → `currency_types.code` + - Relación: Many-to-One + - Propósito: Tipo de moneda para el costo unitario + +3. **Con GClass (gclasses)** + - Campos: `(client_key, part_class)` → `(client_key, class_code)` + - Relación: Many-to-One (usando primaryjoin complejo) + - Propósito: Clasificación de la parte + - Atributo: `part_class_info` + +### GClass (Tabla: gclasses) +El modelo `GClass` representa las clases de clasificación en sistemas SCAII y SCAF. + +#### Relaciones: + +1. **Con MaterialType (public.material_types)** + - Campo: `material_key` → `material_types.key` + - Relación: Many-to-One + - Propósito: Tipo de material de la clase + +2. **Con GPart (gparts)** + - Campos: `(client_key, class_code)` → `(client_key, part_class)` + - Relación: One-to-Many (inversa de la relación en GPart) + - Propósito: Partes que pertenecen a esta clase + - Atributo: `parts` + +## Esquema de Relaciones + +``` +GPart +├── country (Country) # País de origen +├── currency (CurrencyType) # Tipo de moneda +└── part_class_info (GClass) # Información de clasificación + └── material_type (MaterialType) # Tipo de material + +GClass +├── material_type (MaterialType) # Tipo de material +└── parts (List[GPart]) # Partes que usan esta clase +``` + +## Uso de las Relaciones + +### En consultas: +```python +# Obtener una parte con su información completa +part = session.query(GPart).options( + joinedload(GPart.country), + joinedload(GPart.currency), + joinedload(GPart.part_class_info).joinedload(GClass.material_type) +).filter( + GPart.client_key == 1, + GPart.part_number == "PART001" +).first() + +# Acceder a los datos relacionados +print(f"País: {part.country.description_es}") +print(f"Moneda: {part.currency.currency_name}") +print(f"Clase: {part.part_class_info.description_spanish}") +print(f"Material: {part.part_class_info.material_type.description}") +``` + +### En DTOs: +Los DTOs pueden incluir información relacionada: +```python +class PartDetailResponseDTO(BaseModel): + client_key: int + part_number: str + description_spanish: Optional[str] + country_name: Optional[str] = None + currency_name: Optional[str] = None + class_description: Optional[str] = None + material_type: Optional[str] = None +``` + +## Consideraciones Técnicas + +1. **Composite Foreign Keys**: La relación entre `GPart` y `GClass` usa claves foráneas compuestas que requieren `primaryjoin` personalizado. + +2. **Viewonly Relationships**: Algunas relaciones están marcadas como `viewonly=True` para evitar problemas de escritura accidental. + +3. **Lazy Loading**: Por defecto, las relaciones usan lazy loading. Para consultas que necesiten datos relacionados, usar `joinedload` o `selectinload`. + +4. **Type Hints**: Se usan `TYPE_CHECKING` imports para evitar import circulares mientras se mantienen los type hints. + +## Futuras Relaciones + +Potenciales relaciones adicionales que se pueden agregar: + +1. **Con Sectors** (public.sectors) - para clasificación sectorial +2. **Con Transport Types** (public.transport_types) - para modo de transporte +3. **Con Customs Sections** (public.customs_sections) - para sección aduanera +4. **Relaciones con tablas subsidiarias** como `SPartes`, `QPartes`, etc. \ No newline at end of file From d2ae76ef8171053b1c39e435e2587c57715f9a43 Mon Sep 17 00:00:00 2001 From: acazares Date: Tue, 4 Nov 2025 22:27:31 -0600 Subject: [PATCH 12/16] feat(theme): add theme toggle functionality and initialize theme state --- frontend/src/app.html | 11 +++- .../lib/components/sidebar/nav-user.svelte | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/src/app.html b/frontend/src/app.html index e52450df..608bad89 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,10 +1,19 @@ - + Anexo76 - Gestión de Comercio Exterior + %sveltekit.head% diff --git a/frontend/src/lib/components/sidebar/nav-user.svelte b/frontend/src/lib/components/sidebar/nav-user.svelte index 151259a8..f3f472e2 100644 --- a/frontend/src/lib/components/sidebar/nav-user.svelte +++ b/frontend/src/lib/components/sidebar/nav-user.svelte @@ -10,6 +10,8 @@ import LogOutIcon from "@lucide/svelte/icons/log-out"; import SparklesIcon from "@lucide/svelte/icons/sparkles"; import LanguagesIcon from "@lucide/svelte/icons/languages"; + import MoonIcon from "@lucide/svelte/icons/moon"; + import SunIcon from "@lucide/svelte/icons/sun"; import { logout } from "$lib/auth"; import { cookieName } from "$lib/paraglide/runtime"; import { page } from "$app/state"; @@ -21,6 +23,30 @@ // Estado reactivo del idioma actual let currentLocale = $derived(page.data.locale || 'en'); + // Estado reactivo del tema actual + let isDarkMode = $state(false); + + // Inicializar el estado del tema al montar el componente + $effect(() => { + if (browser) { + // Cargar la preferencia guardada o usar el valor actual del HTML + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + isDarkMode = savedTheme === 'dark'; + if (savedTheme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } else { + // Si no hay preferencia guardada, usar el valor actual + isDarkMode = document.documentElement.classList.contains('dark'); + // Guardar el estado actual + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); + } + } + }); + async function handleLogout() { await logout(); } @@ -42,6 +68,23 @@ // Recargar la página para que el servidor procese el nuevo idioma window.location.reload(); } + + function toggleTheme() { + if (!browser) return; + + const html = document.documentElement; + const newTheme = html.classList.contains('dark') ? 'light' : 'dark'; + + if (newTheme === 'dark') { + html.classList.add('dark'); + } else { + html.classList.remove('dark'); + } + + // Guardar la preferencia en localStorage + localStorage.setItem('theme', newTheme); + isDarkMode = newTheme === 'dark'; + } @@ -111,6 +154,15 @@ Language: {currentLocale.toUpperCase()} + + {#if isDarkMode} + + Light Mode + {:else} + + Dark Mode + {/if} + From c1dee97092391d4cfe3141f0243f9803b4884448 Mon Sep 17 00:00:00 2001 From: Kevin Rosales Date: Wed, 5 Nov 2025 22:38:58 -0600 Subject: [PATCH 13/16] # Reporte de Trabajo - 5 de Noviembre de 2025 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Cambios Realizados ### 1. **Modelos** - Se actualizaron los modelos para incluir el esquema en las tablas: - - - - - Ajustes en relaciones y claves foráneas para garantizar consistencia con el esquema . - Se añadieron anotaciones de tipo y mejoras en la documentación de los modelos. ### 2. **Servicios** - Implementación de lógica de negocio en para el módulo : - Creación, actualización, eliminación y búsqueda de partes. - Métodos para estadísticas y manejo de estados habilitado/deshabilitado. ### 3. **Migraciones** - Creación de nuevas migraciones de Alembic para las tablas: - - - - - Tablas relacionadas con direcciones y programas de clientes/proveedores. ### 4. **Documentación** - Actualización de : - Detalles de las nuevas funcionalidades implementadas. - Endpoints REST API agregados para los módulos. - Relaciones principales entre tablas. - Actualización de : - Cambios realizados en los modelos para usar el esquema . - Beneficios de la separación de esquemas. - Próximos pasos para completar la integración. ## Próximos Pasos 1. Verificar las migraciones generadas y aplicarlas en el entorno de desarrollo. 2. Implementar pruebas unitarias para los nuevos servicios y modelos. --- *Documento generado automáticamente el 5 de noviembre de 2025.* --- ...54f2046774d0_create_new_a76_tables_only.py | 195 ++++++++++ ...reate_a76_tables_company_clients_parts_.py | 355 ++++++++++++++++++ backend/api/v1/modules/a76/GClass/__init__.py | 2 +- backend/api/v1/modules/a76/GClass/models.py | 7 +- backend/api/v1/modules/a76/GParts/__init__.py | 2 +- backend/api/v1/modules/a76/GParts/models.py | 9 +- backend/api/v1/modules/a76/GParts/service.py | 262 ++++++++++++- .../modules/a76/client_&_provider/__init__.py | 2 +- .../modules/a76/client_&_provider/models.py | 7 +- .../api/v1/modules/a76/company/__init__.py | 2 +- backend/api/v1/modules/a76/company/models.py | 1 + docs/MODULOS_A76_IMPLEMENTADOS.md | 174 +++++++++ docs/SCHEMA_A76_UPDATE.md | 126 +++++++ 13 files changed, 1130 insertions(+), 14 deletions(-) create mode 100644 backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py create mode 100644 backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py create mode 100644 docs/MODULOS_A76_IMPLEMENTADOS.md create mode 100644 docs/SCHEMA_A76_UPDATE.md diff --git a/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py b/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py new file mode 100644 index 00000000..01ae5756 --- /dev/null +++ b/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py @@ -0,0 +1,195 @@ +"""Create new A76 tables only - company, clients, parts, classes + +Revision ID: 54f2046774d0 +Revises: 7937209f9718 +Create Date: 2025-11-06 03:38:27.848630 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '54f2046774d0' +down_revision: Union[str, Sequence[str], None] = '7937209f9718' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema - Create only new A76 tables.""" + + # Create new A76 tables only (skip existing tenants, licenses, license_usage) + op.create_table('gclient_provider', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('type_nat_foreign', sa.String(length=1), nullable=True), + sa.Column('name', sa.String(length=256), nullable=True), + sa.Column('short_name', sa.String(length=10), nullable=True), + sa.Column('rfc', sa.String(length=30), nullable=True), + sa.Column('curp', sa.String(length=19), nullable=True), + sa.Column('client_or_provider', sa.String(length=1), nullable=True), + sa.Column('linking', sa.String(length=1), nullable=True), + sa.Column('transform_subassembly', sa.String(length=1), nullable=True), + sa.Column('extra_information', sa.String(length=399), nullable=True), + sa.Column('web_key', sa.String(length=40), nullable=True), + sa.Column('responsible', sa.String(length=80), nullable=True), + sa.Column('position', sa.String(length=30), nullable=True), + sa.Column('incoterm', sa.String(length=19), nullable=True), + sa.Column('is_national_provider', sa.String(length=2), nullable=True), + sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + + op.create_table('gcompany', + sa.Column('id', sa.String(length=3), nullable=False), + sa.Column('consecutive', sa.Boolean(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('rfc', sa.String(length=30), nullable=True), + sa.Column('main_activity', sa.String(length=255), nullable=True), + sa.Column('program', sa.String(length=10), nullable=True), + sa.Column('program_number', sa.String(length=40), nullable=True), + sa.Column('prosec', sa.SmallInteger(), nullable=True), + sa.Column('prosec_authorization', sa.String(length=20), nullable=True), + sa.Column('manufacturer_id', sa.String(length=25), nullable=True), + sa.Column('broker_company', sa.String(length=10), nullable=True), + sa.Column('responsible', sa.String(length=80), nullable=True), + sa.Column('responsible_name', sa.String(length=20), nullable=True), + sa.Column('responsible_last_name', sa.String(length=20), nullable=True), + sa.Column('responsible_mother_last_name', sa.String(length=20), nullable=True), + sa.Column('responsible_rfc', sa.String(length=30), nullable=True), + sa.Column('position', sa.String(length=30), nullable=True), + sa.Column('logo', sa.String(length=255), nullable=True), + sa.Column('has_express_line', sa.Boolean(), nullable=True), + sa.Column('order_format_type', sa.String(length=19), nullable=True), + sa.Column('previous_code', sa.SmallInteger(), nullable=True), + sa.Column('is_service_company', sa.Boolean(), nullable=True), + sa.Column('client_name', sa.String(length=300), nullable=True), + sa.Column('subassembly_mode', sa.String(length=7), nullable=True), + sa.Column('curp', sa.String(length=19), nullable=True), + sa.Column('inter_db_name', sa.String(length=100), nullable=True), + sa.Column('ctpat_svi', sa.String(length=100), nullable=True), + sa.Column('trusted_exporter_number', sa.String(length=50), nullable=True), + sa.Column('prevalidator_key', sa.String(length=20), nullable=True), + sa.Column('seventh_amendment', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('consecutive'), + schema='a76' + ) + + op.create_table('gclasses', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('class_code', sa.String(length=8), nullable=False), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('material_key', sa.String(length=10), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('sub_key', sa.String(length=5), nullable=True), + sa.Column('physical_review', sa.SmallInteger(), nullable=True), + sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True), + sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ), + sa.PrimaryKeyConstraint('client_key', 'class_code'), + schema='a76' + ) + + op.create_table('gparts', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('part_number', sa.String(length=49), nullable=False), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('part_class', sa.String(length=8), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('commercial_part_number', sa.String(length=70), nullable=True), + sa.Column('country_of_origin', sa.String(length=3), nullable=True), + sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('currency_type', sa.String(length=2), nullable=True), + sa.Column('currency_key', sa.String(length=3), nullable=True), + sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True), + sa.Column('weight_type', sa.String(length=6), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('fda_key', sa.String(length=20), nullable=True), + sa.Column('fcc_key', sa.String(length=30), nullable=True), + sa.Column('license_code', sa.String(length=3), nullable=True), + sa.Column('eccn', sa.String(length=20), nullable=True), + sa.Column('export_code', sa.String(length=2), nullable=True), + sa.Column('exclusion_symbol', sa.String(length=19), nullable=True), + sa.Column('supplier', sa.String(length=14), nullable=True), + sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True), + sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), + sa.Column('creation_date', sa.Integer(), nullable=True), + sa.Column('modification_date', sa.Integer(), nullable=True), + sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True), + sa.Column('part_photo', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ), + sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ), + sa.PrimaryKeyConstraint('client_key', 'part_number'), + schema='a76' + ) + + # Create dependent tables after main tables + op.create_table('gclient_provider_address', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('municipality', sa.String(length=150), nullable=True), + sa.Column('streets', sa.String(length=100), nullable=True), + sa.Column('neighborhood', sa.String(length=40), nullable=True), + sa.Column('interior_number', sa.String(length=20), nullable=True), + sa.Column('exterior_number', sa.String(length=20), nullable=True), + sa.Column('postal_code', sa.String(length=15), nullable=True), + sa.Column('city', sa.String(length=30), nullable=True), + sa.Column('state', sa.String(length=30), nullable=True), + sa.Column('country', sa.String(length=3), nullable=True), + sa.Column('phone', sa.String(length=30), nullable=True), + sa.Column('fax_number', sa.String(length=30), nullable=True), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('contact', sa.String(length=50), nullable=True), + sa.Column('reference', sa.String(length=250), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + + op.create_table('gclient_provider_programs', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('program', sa.String(length=7), nullable=True), + sa.Column('program_number', sa.String(length=40), nullable=True), + sa.Column('prosec', sa.SmallInteger(), nullable=True), + sa.Column('prosec_authorization', sa.String(length=20), nullable=True), + sa.Column('secon_auth_date', sa.Integer(), nullable=True), + sa.Column('manufacturer_id', sa.String(length=25), nullable=True), + sa.Column('tax_id', sa.String(length=30), nullable=True), + sa.Column('broker', sa.String(length=6), nullable=True), + sa.Column('import_broker', sa.String(length=6), nullable=True), + sa.Column('transfer_key', sa.String(length=8), nullable=True), + sa.Column('secon_authorization', sa.String(length=20), nullable=True), + sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True), + sa.Column('is_certified_company', sa.String(length=1), nullable=True), + sa.Column('certified_company_registry', sa.String(length=40), nullable=True), + sa.Column('donation_auth_number', sa.String(length=50), nullable=True), + sa.Column('ctpat_svi', sa.String(length=100), nullable=True), + sa.Column('tax_registry_number', sa.String(length=40), nullable=True), + sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), + sa.Column('autse_dates', sa.Integer(), nullable=True), + sa.Column('autse_number', sa.String(length=300), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + + +def downgrade() -> None: + """Downgrade schema - Drop only new A76 tables.""" + # Drop tables in reverse dependency order + op.drop_table('gclient_provider_programs', schema='a76') + op.drop_table('gclient_provider_address', schema='a76') + op.drop_table('gparts', schema='a76') + op.drop_table('gclasses', schema='a76') + op.drop_table('gcompany', schema='a76') + op.drop_table('gclient_provider', schema='a76') \ No newline at end of file diff --git a/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py b/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py new file mode 100644 index 00000000..07992a28 --- /dev/null +++ b/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py @@ -0,0 +1,355 @@ +"""create_a76_tables_company_clients_parts_classes + +Revision ID: eb8a17e5fbde +Revises: 7937209f9718 +Create Date: 2025-11-06 03:15:17.248159 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eb8a17e5fbde' +down_revision: Union[str, Sequence[str], None] = '7937209f9718' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema - Create new A76 tables only.""" + + # Crear tabla gcompany + op.create_table('gcompany', + sa.Column('id', sa.String(length=3), nullable=False), + sa.Column('consecutive', sa.Boolean(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('rfc', sa.String(length=30), nullable=True), + sa.Column('main_activity', sa.String(length=255), nullable=True), + sa.Column('program', sa.String(length=10), nullable=True), + sa.Column('program_number', sa.String(length=40), nullable=True), + sa.Column('prosec', sa.SmallInteger(), nullable=True), + sa.Column('prosec_authorization', sa.String(length=20), nullable=True), + sa.Column('manufacturer_id', sa.String(length=25), nullable=True), + sa.Column('broker_company', sa.String(length=10), nullable=True), + sa.Column('responsible', sa.String(length=80), nullable=True), + sa.Column('responsible_name', sa.String(length=20), nullable=True), + sa.Column('responsible_last_name', sa.String(length=20), nullable=True), + sa.Column('responsible_mother_last_name', sa.String(length=20), nullable=True), + sa.Column('responsible_rfc', sa.String(length=30), nullable=True), + sa.Column('position', sa.String(length=30), nullable=True), + sa.Column('logo', sa.String(length=255), nullable=True), + sa.Column('has_express_line', sa.Boolean(), nullable=True), + sa.Column('order_format_type', sa.String(length=19), nullable=True), + sa.Column('previous_code', sa.SmallInteger(), nullable=True), + sa.Column('is_service_company', sa.Boolean(), nullable=True), + sa.Column('client_name', sa.String(length=300), nullable=True), + sa.Column('subassembly_mode', sa.String(length=7), nullable=True), + sa.Column('curp', sa.String(length=19), nullable=True), + sa.Column('inter_db_name', sa.String(length=100), nullable=True), + sa.Column('ctpat_svi', sa.String(length=100), nullable=True), + sa.Column('trusted_exporter_number', sa.String(length=50), nullable=True), + sa.Column('prevalidator_key', sa.String(length=20), nullable=True), + sa.Column('seventh_amendment', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('consecutive'), + schema='a76' + ) + + # Crear tabla gclient_provider + op.create_table('gclient_provider', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('type_nat_foreign', sa.String(length=1), nullable=True), + sa.Column('name', sa.String(length=256), nullable=True), + sa.Column('short_name', sa.String(length=10), nullable=True), + sa.Column('rfc', sa.String(length=30), nullable=True), + sa.Column('curp', sa.String(length=19), nullable=True), + sa.Column('client_or_provider', sa.String(length=1), nullable=True), + sa.Column('linking', sa.String(length=1), nullable=True), + sa.Column('transform_subassembly', sa.String(length=1), nullable=True), + sa.Column('extra_information', sa.String(length=399), nullable=True), + sa.Column('web_key', sa.String(length=40), nullable=True), + sa.Column('responsible', sa.String(length=80), nullable=True), + sa.Column('position', sa.String(length=30), nullable=True), + sa.Column('incoterm', sa.String(length=19), nullable=True), + sa.Column('is_national_provider', sa.String(length=2), nullable=True), + sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + + # Crear tabla gclasses + op.create_table('gclasses', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('class_code', sa.String(length=8), nullable=False), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('material_key', sa.String(length=10), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('sub_key', sa.String(length=5), nullable=True), + sa.Column('physical_review', sa.SmallInteger(), nullable=True), + sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True), + sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ), + sa.PrimaryKeyConstraint('client_key', 'class_code'), + schema='a76' + ) + + # Crear tabla gparts + op.create_table('gparts', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('part_number', sa.String(length=49), nullable=False), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('part_class', sa.String(length=8), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('commercial_part_number', sa.String(length=70), nullable=True), + sa.Column('country_of_origin', sa.String(length=3), nullable=True), + sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('currency_type', sa.String(length=2), nullable=True), + sa.Column('currency_key', sa.String(length=3), nullable=True), + sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True), + sa.Column('weight_type', sa.String(length=6), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('fda_key', sa.String(length=20), nullable=True), + sa.Column('fcc_key', sa.String(length=30), nullable=True), + sa.Column('license_code', sa.String(length=3), nullable=True), + sa.Column('eccn', sa.String(length=20), nullable=True), + sa.Column('export_code', sa.String(length=2), nullable=True), + sa.Column('exclusion_symbol', sa.String(length=19), nullable=True), + sa.Column('supplier', sa.String(length=14), nullable=True), + sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True), + sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), + sa.Column('creation_date', sa.Integer(), nullable=True), + sa.Column('modification_date', sa.Integer(), nullable=True), + sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True), + sa.Column('part_photo', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ), + sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ), + sa.PrimaryKeyConstraint('client_key', 'part_number'), + schema='a76' + ) + + # Crear tabla gclient_provider_address + op.create_table('gclient_provider_address', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('municipality', sa.String(length=150), nullable=True), + sa.Column('streets', sa.String(length=100), nullable=True), + sa.Column('neighborhood', sa.String(length=40), nullable=True), + sa.Column('interior_number', sa.String(length=20), nullable=True), + sa.Column('exterior_number', sa.String(length=20), nullable=True), + sa.Column('postal_code', sa.String(length=15), nullable=True), + sa.Column('city', sa.String(length=30), nullable=True), + sa.Column('state', sa.String(length=30), nullable=True), + sa.Column('country', sa.String(length=3), nullable=True), + sa.Column('phone', sa.String(length=30), nullable=True), + sa.Column('fax_number', sa.String(length=30), nullable=True), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('contact', sa.String(length=50), nullable=True), + sa.Column('reference', sa.String(length=250), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + + # Crear tabla gclient_provider_programs + op.create_table('gclient_provider_programs', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('program', sa.String(length=7), nullable=True), + sa.Column('program_number', sa.String(length=40), nullable=True), + sa.Column('prosec', sa.SmallInteger(), nullable=True), + sa.Column('prosec_authorization', sa.String(length=20), nullable=True), + sa.Column('secon_auth_date', sa.Integer(), nullable=True), + sa.Column('manufacturer_id', sa.String(length=25), nullable=True), + sa.Column('tax_id', sa.String(length=30), nullable=True), + sa.Column('broker', sa.String(length=6), nullable=True), + sa.Column('import_broker', sa.String(length=6), nullable=True), + sa.Column('transfer_key', sa.String(length=8), nullable=True), + sa.Column('secon_authorization', sa.String(length=20), nullable=True), + sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True), + sa.Column('is_certified_company', sa.String(length=1), nullable=True), + sa.Column('certified_company_registry', sa.String(length=40), nullable=True), + sa.Column('donation_auth_number', sa.String(length=50), nullable=True), + sa.Column('ctpat_svi', sa.String(length=100), nullable=True), + sa.Column('tax_registry_number', sa.String(length=40), nullable=True), + sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), + sa.Column('autse_dates', sa.Integer(), nullable=True), + sa.Column('autse_number', sa.String(length=300), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + op.create_table('gclasses', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('class_code', sa.String(length=8), nullable=False), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('material_key', sa.String(length=10), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('sub_key', sa.String(length=5), nullable=True), + sa.Column('physical_review', sa.SmallInteger(), nullable=True), + sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True), + sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ), + sa.PrimaryKeyConstraint('client_key', 'class_code'), + schema='a76' + ) + op.create_table('gclient_provider_address', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('municipality', sa.String(length=150), nullable=True), + sa.Column('streets', sa.String(length=100), nullable=True), + sa.Column('neighborhood', sa.String(length=40), nullable=True), + sa.Column('interior_number', sa.String(length=20), nullable=True), + sa.Column('exterior_number', sa.String(length=20), nullable=True), + sa.Column('postal_code', sa.String(length=15), nullable=True), + sa.Column('city', sa.String(length=30), nullable=True), + sa.Column('state', sa.String(length=30), nullable=True), + sa.Column('country', sa.String(length=3), nullable=True), + sa.Column('phone', sa.String(length=30), nullable=True), + sa.Column('fax_number', sa.String(length=30), nullable=True), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('contact', sa.String(length=50), nullable=True), + sa.Column('reference', sa.String(length=250), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + op.create_table('gclient_provider_programs', + sa.Column('client_id', sa.String(length=8), nullable=False), + sa.Column('program', sa.String(length=7), nullable=True), + sa.Column('program_number', sa.String(length=40), nullable=True), + sa.Column('prosec', sa.SmallInteger(), nullable=True), + sa.Column('prosec_authorization', sa.String(length=20), nullable=True), + sa.Column('secon_auth_date', sa.Integer(), nullable=True), + sa.Column('manufacturer_id', sa.String(length=25), nullable=True), + sa.Column('tax_id', sa.String(length=30), nullable=True), + sa.Column('broker', sa.String(length=6), nullable=True), + sa.Column('import_broker', sa.String(length=6), nullable=True), + sa.Column('transfer_key', sa.String(length=8), nullable=True), + sa.Column('secon_authorization', sa.String(length=20), nullable=True), + sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True), + sa.Column('is_certified_company', sa.String(length=1), nullable=True), + sa.Column('certified_company_registry', sa.String(length=40), nullable=True), + sa.Column('donation_auth_number', sa.String(length=50), nullable=True), + sa.Column('ctpat_svi', sa.String(length=100), nullable=True), + sa.Column('tax_registry_number', sa.String(length=40), nullable=True), + sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), + sa.Column('autse_dates', sa.Integer(), nullable=True), + sa.Column('autse_number', sa.String(length=300), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('client_id'), + schema='a76' + ) + op.create_table('gparts', + sa.Column('client_key', sa.Integer(), nullable=False), + sa.Column('part_number', sa.String(length=49), nullable=False), + sa.Column('fraction', sa.String(length=10), nullable=True), + sa.Column('description_spanish', sa.String(length=500), nullable=True), + sa.Column('description_english', sa.String(length=500), nullable=True), + sa.Column('part_class', sa.String(length=8), nullable=True), + sa.Column('unit_of_measure', sa.String(length=5), nullable=True), + sa.Column('commercial_part_number', sa.String(length=70), nullable=True), + sa.Column('country_of_origin', sa.String(length=3), nullable=True), + sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('currency_type', sa.String(length=2), nullable=True), + sa.Column('currency_key', sa.String(length=3), nullable=True), + sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True), + sa.Column('weight_type', sa.String(length=6), nullable=True), + sa.Column('us_fraction', sa.String(length=16), nullable=True), + sa.Column('fda_key', sa.String(length=20), nullable=True), + sa.Column('fcc_key', sa.String(length=30), nullable=True), + sa.Column('license_code', sa.String(length=3), nullable=True), + sa.Column('eccn', sa.String(length=20), nullable=True), + sa.Column('export_code', sa.String(length=2), nullable=True), + sa.Column('exclusion_symbol', sa.String(length=19), nullable=True), + sa.Column('supplier', sa.String(length=14), nullable=True), + sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True), + sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True), + sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), + sa.Column('creation_date', sa.Integer(), nullable=True), + sa.Column('modification_date', sa.Integer(), nullable=True), + sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True), + sa.Column('part_photo', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ), + sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ), + sa.PrimaryKeyConstraint('client_key', 'part_number'), + schema='a76' + ) + op.create_table('license_usage', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('period_start', sa.DateTime(timezone=True), nullable=False), + sa.Column('period_end', sa.DateTime(timezone=True), nullable=False), + sa.Column('active_users', sa.Integer(), nullable=True), + sa.Column('storage_used_gb', sa.Integer(), nullable=True), + sa.Column('operations_count', sa.Integer(), nullable=True), + sa.Column('api_calls_count', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='a76' + ) + op.create_index(op.f('ix_a76_license_usage_id'), 'license_usage', ['id'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_license_usage_tenant_id'), 'license_usage', ['tenant_id'], unique=False, schema='a76') + op.create_table('licenses', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('plan', sa.Enum('FREE', 'BASIC', 'PROFESSIONAL', 'ENTERPRISE', name='licenseplan'), nullable=False), + sa.Column('status', sa.Enum('ACTIVE', 'EXPIRED', 'SUSPENDED', 'PENDING', 'CANCELLED', name='licensestatus'), nullable=False), + sa.Column('max_users', sa.Integer(), nullable=False), + sa.Column('max_storage_gb', sa.Integer(), nullable=False), + sa.Column('max_monthly_operations', sa.Integer(), nullable=False), + sa.Column('feature_api_access', sa.Boolean(), nullable=True), + sa.Column('feature_advanced_reports', sa.Boolean(), nullable=True), + sa.Column('feature_integrations', sa.Boolean(), nullable=True), + sa.Column('feature_dedicated_support', sa.Boolean(), nullable=True), + sa.Column('starts_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='a76' + ) + op.create_index(op.f('ix_a76_licenses_id'), 'licenses', ['id'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_licenses_tenant_id'), 'licenses', ['tenant_id'], unique=True, schema='a76') + op.drop_constraint(op.f('fk_regimenped'), 'code_pedimento_regimens', type_='foreignkey') + op.drop_constraint(op.f('fk_codeped'), 'code_pedimento_regimens', type_='foreignkey') + op.create_foreign_key('fk_regimenped', 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'], source_schema='public', referent_schema='public') + op.create_foreign_key('fk_codeped', 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'], source_schema='public', referent_schema='public') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_codeped', 'code_pedimento_regimens', schema='public', type_='foreignkey') + op.drop_constraint('fk_regimenped', 'code_pedimento_regimens', schema='public', type_='foreignkey') + op.create_foreign_key(op.f('fk_codeped'), 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code']) + op.create_foreign_key(op.f('fk_regimenped'), 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code']) + op.drop_index(op.f('ix_a76_licenses_tenant_id'), table_name='licenses', schema='a76') + op.drop_index(op.f('ix_a76_licenses_id'), table_name='licenses', schema='a76') + op.drop_table('licenses', schema='a76') + op.drop_index(op.f('ix_a76_license_usage_tenant_id'), table_name='license_usage', schema='a76') + op.drop_index(op.f('ix_a76_license_usage_id'), table_name='license_usage', schema='a76') + op.drop_table('license_usage', schema='a76') + op.drop_table('gparts', schema='a76') + op.drop_table('gclient_provider_programs', schema='a76') + op.drop_table('gclient_provider_address', schema='a76') + op.drop_table('gclasses', schema='a76') + op.drop_index(op.f('ix_a76_tenants_slug'), table_name='tenants', schema='a76') + op.drop_index(op.f('ix_a76_tenants_name'), table_name='tenants', schema='a76') + op.drop_index(op.f('ix_a76_tenants_id'), table_name='tenants', schema='a76') + op.drop_table('tenants', schema='a76') + op.drop_table('gcompany', schema='a76') + op.drop_table('gclient_provider', schema='a76') + # ### end Alembic commands ### diff --git a/backend/api/v1/modules/a76/GClass/__init__.py b/backend/api/v1/modules/a76/GClass/__init__.py index 7c89dc70..09dc90d1 100644 --- a/backend/api/v1/modules/a76/GClass/__init__.py +++ b/backend/api/v1/modules/a76/GClass/__init__.py @@ -1,5 +1,5 @@ """ -Módulo de Tenants +Módulo de GClass """ from .routes import router diff --git a/backend/api/v1/modules/a76/GClass/models.py b/backend/api/v1/modules/a76/GClass/models.py index 157b3bee..77a6765a 100644 --- a/backend/api/v1/modules/a76/GClass/models.py +++ b/backend/api/v1/modules/a76/GClass/models.py @@ -8,7 +8,7 @@ from core.database import Base import enum # Importar modelos relacionados para type hints y relationships -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional if TYPE_CHECKING: from api.v1.modules.a76.GParts.models import GPart @@ -20,6 +20,7 @@ class GClass(Base): Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF """ __tablename__ = "gclasses" + __table_args__ = {"schema": "a76"} # Primary key compuesta client_key = Column(Integer, primary_key=True, nullable=False) @@ -43,10 +44,10 @@ class GClass(Base): iva_exempt_fraction = Column(String(4), nullable=True) # FRACCIONEXENTAIVA # Relationships - material_type: "MaterialType" = relationship("MaterialType", foreign_keys=[material_key]) + material_type = relationship("MaterialType", foreign_keys=[material_key]) # Inverse relationship with GParts that have this class - parts: List["GPart"] = relationship( + parts = relationship( "GPart", primaryjoin="and_(GClass.client_key == GPart.client_key, GClass.class_code == GPart.part_class)", foreign_keys="[GPart.client_key, GPart.part_class]", diff --git a/backend/api/v1/modules/a76/GParts/__init__.py b/backend/api/v1/modules/a76/GParts/__init__.py index 7c89dc70..11584490 100644 --- a/backend/api/v1/modules/a76/GParts/__init__.py +++ b/backend/api/v1/modules/a76/GParts/__init__.py @@ -1,5 +1,5 @@ """ -Módulo de Tenants +Módulo de GParts """ from .routes import router diff --git a/backend/api/v1/modules/a76/GParts/models.py b/backend/api/v1/modules/a76/GParts/models.py index 4c0b1d04..df58cb04 100644 --- a/backend/api/v1/modules/a76/GParts/models.py +++ b/backend/api/v1/modules/a76/GParts/models.py @@ -8,7 +8,7 @@ from core.database import Base import enum # Importar modelos relacionados para type hints y relationships -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from api.v1.modules.public.reference_data.countries.models import Country @@ -21,6 +21,7 @@ class GPart(Base): Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) """ __tablename__ = "gparts" + __table_args__ = {"schema": "a76"} # Primary key compuesta client_key = Column(Integer, primary_key=True, nullable=False) @@ -68,12 +69,12 @@ class GPart(Base): part_photo = Column(String(255), nullable=True) # Relationships - country: "Country" = relationship("Country", foreign_keys=[country_of_origin]) - currency: "CurrencyType" = relationship("CurrencyType", foreign_keys=[currency_key]) + country = relationship("Country", foreign_keys=[country_of_origin]) + currency = relationship("CurrencyType", foreign_keys=[currency_key]) # Relationship with GClass through composite foreign key # Note: This requires both client_key and part_class to match client_key and class_code in GClass - part_class_info: "GClass" = relationship( + part_class_info = relationship( "GClass", primaryjoin="and_(GPart.client_key == GClass.client_key, GPart.part_class == GClass.class_code)", foreign_keys="[GPart.client_key, GPart.part_class]", diff --git a/backend/api/v1/modules/a76/GParts/service.py b/backend/api/v1/modules/a76/GParts/service.py index a5815300..cdc08dc4 100644 --- a/backend/api/v1/modules/a76/GParts/service.py +++ b/backend/api/v1/modules/a76/GParts/service.py @@ -9,7 +9,267 @@ from typing import List, Optional import logging from datetime import datetime - +from .models import GPart +from .dto import PartCreateDTO, PartUpdateDTO logger = logging.getLogger(__name__) + +class PartService: + """ + Servicio para gestión de partes/componentes + """ + + @staticmethod + def create_part(db: Session, part_data: PartCreateDTO) -> GPart: + """ + Crear una nueva parte + """ + try: + db_part = GPart(**part_data.model_dump()) + db.add(db_part) + db.commit() + db.refresh(db_part) + return db_part + except IntegrityError as e: + db.rollback() + logger.error(f"Error creating part: {e}") + raise HTTPException(status_code=400, detail="Part with this client_key and part_number already exists") + except Exception as e: + db.rollback() + logger.error(f"Unexpected error creating part: {e}") + raise HTTPException(status_code=500, detail="Error creating part") + + @staticmethod + def get_part(db: Session, client_key: int, part_number: str) -> Optional[GPart]: + """ + Obtener una parte por clave de cliente y número de parte + """ + try: + return db.query(GPart).filter( + and_( + GPart.client_key == client_key, + GPart.part_number == part_number + ) + ).first() + except Exception as e: + logger.error(f"Error getting part: {e}") + raise HTTPException(status_code=500, detail="Error retrieving part") + + @staticmethod + def get_parts_paginated( + db: Session, + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + client_key: Optional[int] = None, + fraction: Optional[str] = None, + country_of_origin: Optional[str] = None + ) -> tuple[List[GPart], int]: + """ + Obtener partes con paginación y filtros + """ + try: + query = db.query(GPart) + + # Aplicar filtros + if search: + query = query.filter(or_( + GPart.description_spanish.ilike(f"%{search}%"), + GPart.description_english.ilike(f"%{search}%"), + GPart.part_number.ilike(f"%{search}%") + )) + + if client_key is not None: + query = query.filter(GPart.client_key == client_key) + + if fraction: + query = query.filter(GPart.fraction == fraction) + + if country_of_origin: + query = query.filter(GPart.country_of_origin == country_of_origin) + + # Contar total + total = query.count() + + # Aplicar paginación + parts = query.offset(skip).limit(limit).all() + + return parts, total + except Exception as e: + logger.error(f"Error getting paginated parts: {e}") + raise HTTPException(status_code=500, detail="Error retrieving parts") + + @staticmethod + def get_parts_by_client(db: Session, client_key: int) -> List[GPart]: + """ + Obtener todas las partes de un cliente específico + """ + try: + return db.query(GPart).filter(GPart.client_key == client_key).all() + except Exception as e: + logger.error(f"Error getting parts by client: {e}") + raise HTTPException(status_code=500, detail="Error retrieving client parts") + + @staticmethod + def search_parts_by_fraction(db: Session, fraction: str) -> List[GPart]: + """ + Buscar partes por fracción arancelaria + """ + try: + return db.query(GPart).filter( + or_( + GPart.fraction.ilike(f"%{fraction}%"), + GPart.us_fraction.ilike(f"%{fraction}%") + ) + ).all() + except Exception as e: + logger.error(f"Error searching parts by fraction: {e}") + raise HTTPException(status_code=500, detail="Error searching parts by fraction") + + @staticmethod + def search_parts_by_supplier(db: Session, supplier: str) -> List[GPart]: + """ + Buscar partes por proveedor + """ + try: + return db.query(GPart).filter(GPart.supplier.ilike(f"%{supplier}%")).all() + except Exception as e: + logger.error(f"Error searching parts by supplier: {e}") + raise HTTPException(status_code=500, detail="Error searching parts by supplier") + + @staticmethod + def search_parts_by_country(db: Session, country_code: str) -> List[GPart]: + """ + Buscar partes por país de origen + """ + try: + return db.query(GPart).filter(GPart.country_of_origin == country_code).all() + except Exception as e: + logger.error(f"Error searching parts by country: {e}") + raise HTTPException(status_code=500, detail="Error searching parts by country") + + @staticmethod + def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[GPart]: + """ + Actualizar una parte existente + """ + try: + db_part = PartService.get_part(db, client_key, part_number) + if not db_part: + return None + + # Actualizar campos + for field, value in part_data.model_dump(exclude_unset=True).items(): + setattr(db_part, field, value) + + db.commit() + db.refresh(db_part) + return db_part + except Exception as e: + db.rollback() + logger.error(f"Error updating part: {e}") + raise HTTPException(status_code=500, detail="Error updating part") + + @staticmethod + def delete_part(db: Session, client_key: int, part_number: str) -> bool: + """ + Eliminar una parte + """ + try: + db_part = PartService.get_part(db, client_key, part_number) + if not db_part: + return False + + db.delete(db_part) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error(f"Error deleting part: {e}") + raise HTTPException(status_code=500, detail="Error deleting part") + + @staticmethod + def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[GPart]: + """ + Cambiar el estado habilitado/deshabilitado de una parte + """ + try: + db_part = PartService.get_part(db, client_key, part_number) + if not db_part: + return None + + # Toggle status (assuming 1 = enabled, 0 = disabled) + db_part.enabled_disabled = 1 if db_part.enabled_disabled == 0 else 0 + + db.commit() + db.refresh(db_part) + return db_part + except Exception as e: + db.rollback() + logger.error(f"Error toggling part status: {e}") + raise HTTPException(status_code=500, detail="Error toggling part status") + + @staticmethod + def get_parts_statistics(db: Session) -> dict: + """ + Obtener estadísticas de partes + """ + try: + total_parts = db.query(GPart).count() + + # Partes por cliente + parts_by_client = db.query( + GPart.client_key, + func.count(GPart.part_number).label('count') + ).group_by(GPart.client_key).all() + + # Partes por país de origen + parts_by_country = db.query( + GPart.country_of_origin, + func.count(GPart.part_number).label('count') + ).filter(GPart.country_of_origin.isnot(None))\ + .group_by(GPart.country_of_origin).all() + + # Partes habilitadas vs deshabilitadas + enabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 1).count() + disabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 0).count() + + return { + "total_parts": total_parts, + "enabled_parts": enabled_parts, + "disabled_parts": disabled_parts, + "parts_by_client": [{"client_key": item[0], "count": item[1]} for item in parts_by_client], + "parts_by_country": [{"country": item[0], "count": item[1]} for item in parts_by_country] + } + except Exception as e: + logger.error(f"Error getting parts statistics: {e}") + raise HTTPException(status_code=500, detail="Error retrieving parts statistics") + + @staticmethod + def get_part_regulatory_info(db: Session, client_key: int, part_number: str) -> Optional[dict]: + """ + Obtener información regulatoria específica de una parte + """ + try: + db_part = PartService.get_part(db, client_key, part_number) + if not db_part: + return None + + return { + "client_key": db_part.client_key, + "part_number": db_part.part_number, + "fraction": db_part.fraction, + "us_fraction": db_part.us_fraction, + "fda_key": db_part.fda_key, + "fcc_key": db_part.fcc_key, + "license_code": db_part.license_code, + "eccn": db_part.eccn, + "export_code": db_part.export_code, + "exclusion_symbol": db_part.exclusion_symbol, + "country_of_origin": db_part.country_of_origin + } + except Exception as e: + logger.error(f"Error getting part regulatory info: {e}") + raise HTTPException(status_code=500, detail="Error retrieving part regulatory information") + diff --git a/backend/api/v1/modules/a76/client_&_provider/__init__.py b/backend/api/v1/modules/a76/client_&_provider/__init__.py index 7c89dc70..9621b9c4 100644 --- a/backend/api/v1/modules/a76/client_&_provider/__init__.py +++ b/backend/api/v1/modules/a76/client_&_provider/__init__.py @@ -1,5 +1,5 @@ """ -Módulo de Tenants +Módulo de Client & Provider """ from .routes import router diff --git a/backend/api/v1/modules/a76/client_&_provider/models.py b/backend/api/v1/modules/a76/client_&_provider/models.py index 2c9c36cc..14834953 100644 --- a/backend/api/v1/modules/a76/client_&_provider/models.py +++ b/backend/api/v1/modules/a76/client_&_provider/models.py @@ -13,6 +13,7 @@ class GClientProvider(Base): Modelo para la tabla GClientesPro - Información de clientes y proveedores """ __tablename__ = "gclient_provider" + __table_args__ = {"schema": "a76"} # Primary key client_id = Column(String(8), primary_key=True, nullable=False) @@ -44,9 +45,10 @@ class GClientProviderAddress(Base): Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores """ __tablename__ = "gclient_provider_address" + __table_args__ = {"schema": "a76"} # Primary key (foreign key) - client_id = Column(String(8), ForeignKey('gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) # Address information municipality = Column(String(150), nullable=True) @@ -73,9 +75,10 @@ class GClientProviderPrograms(Base): Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores """ __tablename__ = "gclient_provider_programs" + __table_args__ = {"schema": "a76"} # Primary key (foreign key) - client_id = Column(String(8), ForeignKey('gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) # Program information program = Column(String(7), nullable=True) diff --git a/backend/api/v1/modules/a76/company/__init__.py b/backend/api/v1/modules/a76/company/__init__.py index 7c89dc70..1930a6ac 100644 --- a/backend/api/v1/modules/a76/company/__init__.py +++ b/backend/api/v1/modules/a76/company/__init__.py @@ -1,5 +1,5 @@ """ -Módulo de Tenants +Módulo de Company """ from .routes import router diff --git a/backend/api/v1/modules/a76/company/models.py b/backend/api/v1/modules/a76/company/models.py index bbba06f3..3d8c6587 100644 --- a/backend/api/v1/modules/a76/company/models.py +++ b/backend/api/v1/modules/a76/company/models.py @@ -12,6 +12,7 @@ class GCompany(Base): Modelo para la tabla GCompany - Información de la empresa """ __tablename__ = "gcompany" + __table_args__ = {"schema": "a76"} # Primary key id = Column(String(3), primary_key=True, default='EMP', nullable=False) diff --git a/docs/MODULOS_A76_IMPLEMENTADOS.md b/docs/MODULOS_A76_IMPLEMENTADOS.md new file mode 100644 index 00000000..90dd563c --- /dev/null +++ b/docs/MODULOS_A76_IMPLEMENTADOS.md @@ -0,0 +1,174 @@ +# Módulos A76 Implementados - Anexo 76 + +**Fecha de implementación:** 4 de noviembre de 2025 + +--- + +## ✨ Nuevas Funcionalidades + +### Módulo de Empresa (Company) +- Gestión de empresa única con información comercial completa +- Manejo de datos fiscales y operativos centralizados + +### Módulo de Clientes y Proveedores (Client & Provider) +- Gestión integral de clientes y proveedores +- Relaciones con direcciones y programas asociados +- Capacidad de diferenciar entre clientes y proveedores + +### Módulo de Partes (GParts) +- Gestión de partes/componentes para los sistemas SCAII, SCAF y WINSAAI +- Control de inventario y clasificación arancelaria +- Información regulatoria y de cumplimiento + +### Módulo de Clases (GClass) +- Clasificaciones para sistemas SCAII y SCAF +- Información arancelaria detallada +- Gestión de fracciones arancelarias y materiales + +--- + +## 🔗 Relaciones de Base de Datos + +### Relaciones Principales +- **GPart ↔ GClass**: Relación de clave compuesta (client_key, part_class ↔ class_code) +- **GPart → Country**: Clave foránea a public.countries (country_of_origin) +- **GPart → CurrencyType**: Clave foránea a public.currency_types (currency_key) +- **GClass → MaterialType**: Clave foránea a public.material_types (material_key) + +### Esquema de Relaciones +``` +GPart (Partes) +├── País de origen → Country +├── Tipo de moneda → CurrencyType +└── Información de clase → GClass + └── Tipo de material → MaterialType +``` + +--- + +## 📊 Endpoints de API Agregados + +### Módulo Empresa (`/company`) +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| POST | `/` | Crear empresa | +| GET | `/` | Obtener información de la empresa | + +### Módulo Clientes y Proveedores (`/clients-providers`) +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| POST | `/` | Crear cliente/proveedor | +| GET | `/` | Listar todos con paginación | +| GET | `/clients` | Listar solo clientes | +| GET | `/providers` | Listar solo proveedores | +| GET | `/search/rfc/{rfc}` | Buscar por RFC | +| GET | `/{client_id}` | Obtener por ID | +| PUT | `/{client_id}` | Actualizar cliente/proveedor | +| DELETE | `/{client_id}` | Eliminar cliente/proveedor | +| PATCH | `/{client_id}/toggle-status` | Cambiar estatus | +| GET | `/{client_id}/address` | Obtener información de dirección | +| GET | `/{client_id}/programs` | Obtener información de programas | +| GET | `/{client_id}/basic` | Obtener información básica | + +### Módulo Partes (`/parts`) +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| POST | `/` | Crear parte | +| GET | `/` | Listar todas con paginación y filtros | +| GET | `/client/{client_key}` | Obtener partes por cliente | +| GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | +| GET | `/search/supplier/{supplier}` | Buscar por proveedor | +| GET | `/search/country/{country_code}` | Buscar por país | +| GET | `/statistics` | Obtener estadísticas de partes | +| GET | `/{client_key}/{part_number}` | Obtener parte específica | +| PUT | `/{client_key}/{part_number}` | Actualizar parte | +| DELETE | `/{client_key}/{part_number}` | Eliminar parte | +| PATCH | `/{client_key}/{part_number}/toggle-status` | Cambiar estatus | +| GET | `/{client_key}/{part_number}/basic` | Obtener información básica | +| GET | `/{client_key}/{part_number}/regulatory` | Obtener información regulatoria | + +### Módulo Clases (`/classes`) +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| POST | `/` | Crear clase | +| GET | `/` | Listar todas con paginación y filtros | +| GET | `/client/{client_key}` | Obtener clases por cliente | +| GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | +| GET | `/search/material/{material_key}` | Buscar por material | +| GET | `/search/unit-measure/{unit_of_measure}` | Buscar por unidad de medida | +| GET | `/search/physical-review/{physical_review}` | Buscar por revisión física | +| GET | `/statistics` | Obtener estadísticas de clases | +| GET | `/{client_key}/{class_code}` | Obtener clase específica | +| PUT | `/{client_key}/{class_code}` | Actualizar clase | +| DELETE | `/{client_key}/{class_code}` | Eliminar clase | +| GET | `/{client_key}/{class_code}/basic` | Obtener información básica | +| GET | `/{client_key}/{class_code}/tariff` | Obtener información arancelaria | + +--- + +## 🏗️ Arquitectura Implementada + +### Diseño Modular +- **Modelos**: Definición de entidades ORM con SQLAlchemy +- **DTOs**: Objetos de transferencia de datos con validación Pydantic +- **Servicios**: Lógica de negocio y operaciones de base de datos +- **Rutas**: Endpoints REST API con documentación automática + +### Características Técnicas +- **Nombres de campos en inglés** para consistencia internacional +- **Claves primarias compuestas** donde es aplicable +- **Operaciones CRUD completas** con endpoints de búsqueda especializados +- **Relaciones SQLAlchemy** con restricciones de clave foránea apropiadas +- **DTOs type-safe** con validación Pydantic + +### Patrones de Desarrollo +- Estructura consistente en todos los módulos para facilitar mantenimiento +- Separación clara de responsabilidades (models, DTOs, services, routes) +- Validación de datos en múltiples capas +- Manejo de errores estandarizado +- Documentación automática con FastAPI/OpenAPI + +--- + +## 📝 Documentación + +### Archivos de Documentación +- **RELATIONSHIPS.md**: Documentación completa de relaciones de base de datos +- **Type hints detallados** en todos los métodos de servicio +- **Comentarios explicativos** en modelos y funciones complejas + +### Estándares de Código +- Consistencia en patrones de desarrollo entre módulos +- Nomenclatura estandarizada para endpoints y funciones +- Validación robusta de datos de entrada y salida +- Manejo de excepciones centralizado + +--- + +## 📈 Resumen de Implementación + +### Números Totales +- **4 módulos completos** implementados +- **42+ endpoints** REST API disponibles +- **23 archivos nuevos** agregados al proyecto +- **2,798+ líneas de código** implementadas + +### Estado del Proyecto +- ✅ Modelos de base de datos implementados +- ✅ Relaciones entre entidades establecidas +- ✅ DTOs con validación completa +- ✅ Servicios con lógica de negocio +- ✅ Endpoints REST API funcionales +- ✅ Integración en router principal +- ⏳ Migraciones de base de datos (pendiente) + +### Próximos Pasos +1. Crear migraciones de Alembic para las nuevas tablas +2. Implementar tests unitarios para cada módulo +3. Agregar documentación de API con ejemplos +4. Implementar autenticación y autorización +5. Optimizar consultas de base de datos + +--- + +*Documento generado automáticamente el 4 de noviembre de 2025* \ No newline at end of file diff --git a/docs/SCHEMA_A76_UPDATE.md b/docs/SCHEMA_A76_UPDATE.md new file mode 100644 index 00000000..5a3fadba --- /dev/null +++ b/docs/SCHEMA_A76_UPDATE.md @@ -0,0 +1,126 @@ +# Actualización de Schemas A76 + +**Fecha de actualización:** 5 de noviembre de 2025 + +--- + +## ✅ Modelos Actualizados al Schema A76 + +Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schema `a76` en PostgreSQL. + +### 📋 Tablas Configuradas + +| Módulo | Tabla | Schema | Estado | +|--------|-------|---------|---------| +| **Company** | `gcompany` | `a76` | ✅ Actualizada | +| **Client & Provider** | `gclient_provider` | `a76` | ✅ Actualizada | +| **Client & Provider** | `gclient_provider_address` | `a76` | ✅ Actualizada | +| **Client & Provider** | `gclient_provider_programs` | `a76` | ✅ Actualizada | +| **GParts** | `gparts` | `a76` | ✅ Actualizada | +| **GClass** | `gclasses` | `a76` | ✅ Actualizada | +| **Licenses** | `licenses` | `a76` | ✅ Ya estaba | +| **Licenses** | `license_usage` | `a76` | ✅ Ya estaba | +| **Tenants** | `tenants` | `a76` | ✅ Ya estaba | + +### 🔄 Cambios Realizados + +#### 1. Configuración de Schema +```python +# ANTES +class GCompany(Base): + __tablename__ = "gcompany" + +# DESPUÉS +class GCompany(Base): + __tablename__ = "gcompany" + __table_args__ = {"schema": "a76"} +``` + +#### 2. Foreign Keys Actualizadas +```python +# ANTES +client_id = Column(String(8), ForeignKey('gclient_provider.client_id'), ...) + +# DESPUÉS +client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id'), ...) +``` + +### 🏗️ Estructura de Schemas + +``` +PostgreSQL Database +├── Schema: public +│ ├── countries +│ ├── currency_types +│ ├── material_types +│ └── ... (reference data) +│ +└── Schema: a76 + ├── tenants + ├── licenses + ├── license_usage + ├── gcompany + ├── gclient_provider + ├── gclient_provider_address + ├── gclient_provider_programs + ├── gparts + └── gclasses +``` + +### 🔗 Relaciones Mantenidas + +Las relaciones entre schemas funcionan correctamente: + +- **A76 → Public**: Los modelos A76 pueden referenciar datos de referencia en `public` +- **A76 → A76**: Las relaciones internas del schema A76 están actualizadas +- **Composite Keys**: Las relaciones con claves compuestas funcionan correctamente + +#### Ejemplos de Relaciones Cross-Schema: +```python +# GPart (a76) → Country (public) +country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key')) + +# GPart (a76) → CurrencyType (public) +currency_key = Column(String(3), ForeignKey('public.currency_types.code')) + +# GClass (a76) → MaterialType (public) +material_key = Column(String(10), ForeignKey('public.material_types.key')) +``` + +### 🎯 Beneficios de la Separación + +1. **Organización**: Datos de negocio separados de datos de referencia +2. **Seguridad**: Permisos granulares por schema +3. **Mantenimiento**: Facilita respaldos y migraciones selectivas +4. **Escalabilidad**: Permite distribuir schemas en el futuro +5. **Claridad**: Separación lógica de responsabilidades + +### ⚠️ Consideraciones Importantes + +1. **Migraciones**: Las nuevas migraciones deben especificar el schema `a76` +2. **Permisos DB**: El usuario de base de datos necesita permisos en ambos schemas +3. **Testing**: Los tests deben considerar la estructura de schemas +4. **Backup**: Configurar respaldos para incluir ambos schemas + +### 📝 Próximos Pasos + +1. **Crear migraciones de Alembic** con el schema correcto +2. **Verificar permisos** de base de datos para el usuario de aplicación +3. **Actualizar tests** para considerar la estructura de schemas +4. **Documentar convenciones** de naming para futuros modelos + +--- + +### 🔧 Comando de Verificación + +Para verificar que todos los modelos tienen el schema correcto: + +```bash +grep -r "__table_args__ = {\"schema\": \"a76\"}" backend/api/v1/modules/a76/*/models.py +``` + +**Resultado esperado:** 8 coincidencias (una por cada modelo A76) + +--- + +*Actualización completada el 5 de noviembre de 2025* \ No newline at end of file From 07dfe1edb1398b9ea2270a82fb9b6371a3639bf1 Mon Sep 17 00:00:00 2001 From: acazares Date: Thu, 6 Nov 2025 17:16:29 -0600 Subject: [PATCH 14/16] Add service layers and models for Pedimento CRUD operations - Implemented service classes for PedimentoConfigParameters, PedimentoConfigSurcharges, PedimentoConfigUpdateRectification, PedimentoConfigUpdates, PedimentoCustomsOffices, PedimentoDates, PedimentoDecrementables, PedimentoIncrementables, PedimentoIndexes, PedimentoPayments, PedimentoRectificationDestination, PedimentoRectificationOrigin, PedimentoTransportMeans, and PedimentoValidation. - Each service class includes methods for CRUD operations: create, read, update, and delete. - Added a main router for the API v1, integrating various modules including authentication, tenants, licenses, and pedimentos. - Created models for PedimentoValidation with appropriate constraints and relationships. --- backend/alembic.ini | 3 +- backend/alembic/env.py | 17 +- .../versions/03b786378f94_pedimentos.py | 424 ++++++++++++++++++ backend/api/v1/modules/a76/licenses/routes.py | 2 +- .../dtos/pedimento_config_additional.py | 38 ++ .../dtos/pedimento_config_calculations.py | 46 ++ .../dtos/pedimento_config_parameters.py | 49 ++ .../dtos/pedimento_config_surcharges.py | 38 ++ .../pedimento_config_update_rectification.py | 36 ++ .../dtos/pedimento_config_updates.py | 34 ++ .../dtos/pedimento_customs_offices.py | 30 ++ .../a76/pedmientos/dtos/pedimento_dates.py | 50 +++ .../dtos/pedimento_decrementables.py | 45 ++ .../dtos/pedimento_incrementables.py | 47 ++ .../a76/pedmientos/dtos/pedimento_indexes.py | 33 ++ .../a76/pedmientos/dtos/pedimento_payments.py | 50 +++ .../pedimento_rectification_destination.py | 32 ++ .../dtos/pedimento_rectification_origin.py | 51 +++ .../dtos/pedimento_transport_means.py | 34 ++ .../pedmientos/dtos/pedimento_validation.py | 42 ++ .../modules/a76/pedmientos/dtos/pedimentos.py | 54 +++ .../models/pedimento_config_additional.py | 27 ++ .../models/pedimento_config_calculations.py | 31 ++ .../models/pedimento_config_parameters.py | 33 ++ .../models/pedimento_config_surcharges.py | 27 ++ .../pedimento_config_update_rectification.py | 26 ++ .../models/pedimento_config_updates.py | 25 ++ .../models/pedimento_customs_offices.py | 23 + .../a76/pedmientos/models/pedimento_dates.py | 34 ++ .../models/pedimento_decrementables.py | 30 ++ .../models/pedimento_incrementables.py | 31 ++ .../pedmientos/models/pedimento_indexes.py | 24 + .../pedmientos/models/pedimento_payments.py | 34 ++ .../pedimento_rectification_destination.py | 24 + .../models/pedimento_rectification_origin.py | 33 ++ .../models/pedimento_transport_means.py | 25 ++ .../pedmientos/models/pedimento_validation.py | 29 ++ .../a76/pedmientos/models/pedimentos.py | 52 +++ .../api/v1/modules/a76/pedmientos/router.py | 39 ++ .../routes/pedimento_config_additional.py | 94 ++++ .../routes/pedimento_config_calculations.py | 92 ++++ .../routes/pedimento_config_parameters.py | 92 ++++ .../routes/pedimento_config_surcharges.py | 92 ++++ .../pedimento_config_update_rectification.py | 92 ++++ .../routes/pedimento_config_updates.py | 92 ++++ .../routes/pedimento_customs_offices.py | 111 +++++ .../a76/pedmientos/routes/pedimento_dates.py | 92 ++++ .../routes/pedimento_decrementables.py | 111 +++++ .../routes/pedimento_incrementables.py | 111 +++++ .../pedmientos/routes/pedimento_indexes.py | 92 ++++ .../pedmientos/routes/pedimento_payments.py | 111 +++++ .../pedimento_rectification_destination.py | 92 ++++ .../routes/pedimento_rectification_origin.py | 92 ++++ .../routes/pedimento_transport_means.py | 111 +++++ .../pedmientos/routes/pedimento_validation.py | 92 ++++ .../a76/pedmientos/routes/pedimentos.py | 118 +++++ .../services/pedimento_config_additional.py | 60 +++ .../services/pedimento_config_calculations.py | 60 +++ .../services/pedimento_config_parameters.py | 60 +++ .../services/pedimento_config_surcharges.py | 60 +++ .../pedimento_config_update_rectification.py | 60 +++ .../services/pedimento_config_updates.py | 60 +++ .../services/pedimento_customs_offices.py | 60 +++ .../pedmientos/services/pedimento_dates.py | 60 +++ .../services/pedimento_decrementables.py | 60 +++ .../services/pedimento_incrementables.py | 60 +++ .../pedmientos/services/pedimento_indexes.py | 60 +++ .../pedmientos/services/pedimento_payments.py | 60 +++ .../pedimento_rectification_destination.py | 60 +++ .../pedimento_rectification_origin.py | 60 +++ .../services/pedimento_transport_means.py | 60 +++ .../services/pedimento_validation.py | 60 +++ .../a76/pedmientos/services/pedimentos.py | 140 ++++++ backend/api/v1/modules/a76/router.py | 20 + backend/api/v1/modules/a76/tenants/routes.py | 2 +- .../code_pedimento_regimens/routes.py | 2 +- .../reference_data/containers/routes.py | 2 +- .../public/reference_data/countries/routes.py | 2 +- .../reference_data/currency_types/routes.py | 2 +- .../reference_data/customs_sections/routes.py | 2 +- .../customs_warehouses/routes.py | 2 +- .../public/reference_data/incoterms/routes.py | 2 +- .../reference_data/invoice_types/routes.py | 2 +- .../reference_data/material_types/routes.py | 2 +- .../reference_data/payment_methods/routes.py | 2 +- .../reference_data/pedimento_codes/routes.py | 2 +- .../pedimento_regimens/routes.py | 2 +- .../modules/public/reference_data/router.py | 45 ++ .../public/reference_data/sectors/routes.py | 2 +- .../public/reference_data/states/routes.py | 2 +- .../reference_data/transport_modes/routes.py | 2 +- .../reference_data/transport_types/routes.py | 2 +- .../valuation_methods/routes.py | 2 +- backend/api/v1/modules/public/router.py | 13 + backend/api/v1/router.py | 47 +- backend/main.py | 3 - models.py | 135 ------ models_ped.py | 27 ++ 98 files changed, 4613 insertions(+), 202 deletions(-) create mode 100644 backend/alembic/versions/03b786378f94_pedimentos.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py create mode 100644 backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py create mode 100644 backend/api/v1/modules/a76/pedmientos/models/pedimentos.py create mode 100644 backend/api/v1/modules/a76/pedmientos/router.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py create mode 100644 backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py create mode 100644 backend/api/v1/modules/a76/pedmientos/services/pedimentos.py create mode 100644 backend/api/v1/modules/a76/router.py create mode 100644 backend/api/v1/modules/public/reference_data/router.py create mode 100644 backend/api/v1/modules/public/router.py create mode 100644 models_ped.py diff --git a/backend/alembic.ini b/backend/alembic.ini index 47b33935..dc170438 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -84,7 +84,8 @@ path_separator = os # database URL. This is consumed by the user-maintained env.py script only. # other means of configuring database URLs may be customized within the env.py # file. -sqlalchemy.url = ${DATABASE_URL} + +sqlalchemy.url = postgresql://${CORE_DB_USER}:${CORE_DB_PASSWORD}@${CORE_DB_HOST}:${CORE_DB_PORT}/${CORE_DB_NAME} [post_write_hooks] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 3b2a3673..819d6e50 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -78,8 +78,9 @@ fileConfig(config.config_file_name) target_metadata = Base.metadata def import_models_from_dir(dir_path: str): - """Importa recursivamente cualquier archivo models.py desde dir_path""" + """Importa recursivamente cualquier archivo models.py desde dir_path y archivos en directorios models/""" for root, dirs, files in os.walk(dir_path): + # Importar archivos models.py directos if "models.py" in files: module_path = os.path.join(root, "models.py") # Convertir path en nombre de módulo compatible @@ -89,6 +90,20 @@ def import_models_from_dir(dir_path: str): spec = importlib.util.spec_from_file_location(module_name, module_path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) + + # Importar todos los archivos .py en directorios llamados "models" + if os.path.basename(root) == "models": + for file in files: + if file.endswith(".py") and not file.startswith("__"): + module_path = os.path.join(root, file) + rel_path = os.path.relpath(module_path, BASE_DIR) + module_name = rel_path.replace(os.sep, ".").replace(".py", "") + try: + spec = importlib.util.spec_from_file_location(module_name, module_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + except Exception as e: + logger.warning(f"No se pudo importar {module_path}: {e}") # Importar todos los models dentro de api/v1/modules y api/v1/modules/uploads modules_dir = os.path.join(BASE_DIR, "api", "v1", "modules") diff --git a/backend/alembic/versions/03b786378f94_pedimentos.py b/backend/alembic/versions/03b786378f94_pedimentos.py new file mode 100644 index 00000000..d760d4b8 --- /dev/null +++ b/backend/alembic/versions/03b786378f94_pedimentos.py @@ -0,0 +1,424 @@ +"""Pedimentos + +Revision ID: 03b786378f94 +Revises: 7937209f9718 +Create Date: 2025-11-06 17:07:16.536298 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '03b786378f94' +down_revision: Union[str, Sequence[str], None] = '7937209f9718' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pedimentos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('year', sa.String(length=2), nullable=True), + sa.Column('customs_office', sa.String(length=2), nullable=True), + sa.Column('license', sa.String(length=4), nullable=True), + sa.Column('pedimento_number', sa.String(length=7), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('operation_type', sa.Integer(), nullable=True), + sa.Column('pedimento_type', sa.Integer(), nullable=True), + sa.Column('pedimento_key', sa.String(length=2), nullable=True), + sa.Column('regime', sa.String(length=3), nullable=True), + sa.Column('status', sa.String(length=30), nullable=True), + sa.Column('usd_value', sa.Numeric(precision=17, scale=6), nullable=True), + sa.Column('paid_price', sa.Numeric(precision=17, scale=6), nullable=True), + sa.Column('gross_weight', sa.Numeric(precision=19, scale=3), nullable=True), + sa.Column('exchange_rate', sa.Numeric(precision=9, scale=5), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimentos_pkey'), + schema='a76' + ) + op.create_index('idx_pedimentos_client_id', 'pedimentos', ['client_id'], unique=False, schema='a76') + op.create_index('idx_pedimentos_created_at', 'pedimentos', ['created_at'], unique=False, schema='a76') + op.create_index('idx_pedimentos_status', 'pedimentos', ['status'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_pedimentos_tenant_id'), 'pedimentos', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_additional', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('add_po_identifier', sa.SmallInteger(), nullable=True), + sa.Column('do_not_exempt_norms_complement_x', sa.SmallInteger(), nullable=True), + sa.Column('manual_pedimento_year', sa.String(length=2), nullable=True), + sa.Column('enable_import_invoice_recipient', sa.SmallInteger(), nullable=True), + sa.Column('send_502_validation_file_for_consolidated', sa.SmallInteger(), nullable=True), + sa.Column('add_remove_norms', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_additional', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_additional_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_additional_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_additional_tenant_id'), 'pedimento_config_additional', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_calculations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('dta_type', sa.String(length=1), nullable=True), + sa.Column('dta_operation', sa.SmallInteger(), nullable=True), + sa.Column('dta_vehicle_count', sa.SmallInteger(), nullable=True), + sa.Column('dta_mixed_rate_8permil', sa.SmallInteger(), nullable=True), + sa.Column('pays_vat', sa.SmallInteger(), nullable=True), + sa.Column('pays_prevalidation', sa.SmallInteger(), nullable=True), + sa.Column('include_sagar_certificate_fee', sa.SmallInteger(), nullable=True), + sa.Column('fixed_vehicle_dta_fee', sa.SmallInteger(), nullable=True), + sa.Column('additional_fixed_fee', sa.SmallInteger(), nullable=True), + sa.Column('additional_fixed_fee_payment_method', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_calculations', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_calculations_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_calculations_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_calculations_tenant_id'), 'pedimento_config_calculations', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_parameters', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('is_embassy', sa.SmallInteger(), nullable=True), + sa.Column('embassy_dta', sa.Numeric(precision=11, scale=2), nullable=True), + sa.Column('rule_3121_section_ii', sa.SmallInteger(), nullable=True), + sa.Column('use_previous_tariff', sa.SmallInteger(), nullable=True), + sa.Column('use_payment_date_fi', sa.SmallInteger(), nullable=True), + sa.Column('add_state_supplier_record_505', sa.SmallInteger(), nullable=True), + sa.Column('customs_value_calculation', sa.SmallInteger(), nullable=True), + sa.Column('two_decimals_unit_value', sa.SmallInteger(), nullable=True), + sa.Column('customs_value_per_item', sa.SmallInteger(), nullable=True), + sa.Column('is_national_supplier', sa.SmallInteger(), nullable=True), + sa.Column('is_consolidated', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_parameters', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_parameters_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_parameters_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_parameters_tenant_id'), 'pedimento_config_parameters', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_surcharges', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('surcharge_igi', sa.SmallInteger(), nullable=True), + sa.Column('surcharge_dta', sa.SmallInteger(), nullable=True), + sa.Column('surcharge_vat', sa.SmallInteger(), nullable=True), + sa.Column('surcharge_isan', sa.SmallInteger(), nullable=True), + sa.Column('surcharge_ieps', sa.SmallInteger(), nullable=True), + sa.Column('surcharge_cc', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_surcharges', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_surcharges_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_surcharges_tenant_id'), 'pedimento_config_surcharges', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_update_rectification', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('update_vat', sa.SmallInteger(), nullable=True), + sa.Column('update_advalorem', sa.SmallInteger(), nullable=True), + sa.Column('update_cc', sa.SmallInteger(), nullable=True), + sa.Column('update_ieps', sa.SmallInteger(), nullable=True), + sa.Column('calculate_surcharge', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_update_rectification', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_update_rectification_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_update_rectification_tenant_id'), 'pedimento_config_update_rectification', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_config_updates', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('update_vat', sa.SmallInteger(), nullable=True), + sa.Column('update_advalorem', sa.SmallInteger(), nullable=True), + sa.Column('update_cc', sa.SmallInteger(), nullable=True), + sa.Column('update_ieps', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_updates', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_config_updates_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_config_updates_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_config_updates_tenant_id'), 'pedimento_config_updates', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_customs_offices', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('dispatch_customs', sa.String(length=3), nullable=True), + sa.Column('entry_exit_customs', sa.String(length=3), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_customs_offices', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_customs_offices_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_customs_offices_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_customs_offices_tenant_id'), 'pedimento_customs_offices', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_dates', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('entry_date', sa.DateTime(), nullable=True), + sa.Column('pedimento_date', sa.DateTime(), nullable=True), + sa.Column('payment_date', sa.DateTime(), nullable=True), + sa.Column('rectification_payment_date', sa.DateTime(), nullable=True), + sa.Column('extraction_date', sa.DateTime(), nullable=True), + sa.Column('submission_date', sa.DateTime(), nullable=True), + sa.Column('eucan_date', sa.DateTime(), nullable=True), + sa.Column('original_date', sa.DateTime(), nullable=True), + sa.Column('start_date', sa.DateTime(), nullable=True), + sa.Column('end_date', sa.DateTime(), nullable=True), + sa.Column('capture_date', sa.DateTime(), nullable=True), + sa.Column('capture_time', sa.Time(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_dates', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_dates_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_dates_pedimento_id_key'), + schema='a76' + ) + op.create_index('idx_pedimento_dates_pedimento_id', 'pedimento_dates', ['pedimento_id'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_pedimento_dates_tenant_id'), 'pedimento_dates', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_decrementables', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('freight', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('insurance', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('loading', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('unloading', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('others', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('currency', sa.String(length=3), nullable=True), + sa.Column('currency_factor', sa.Numeric(precision=15, scale=8), nullable=True), + sa.Column('not_affect_usd_value', sa.SmallInteger(), nullable=True), + sa.Column('not_affect_customs_value', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_decrementables', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_decrementables_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_decrementables_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_decrementables_tenant_id'), 'pedimento_decrementables', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_incrementables', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('insured_value', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('freight', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('insurance', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('packaging', sa.Numeric(precision=13, scale=2), nullable=True), + sa.Column('others', sa.Numeric(precision=13, scale=3), nullable=True), + sa.Column('deductibles', sa.Numeric(precision=13, scale=3), nullable=True), + sa.Column('currency', sa.String(length=3), nullable=True), + sa.Column('currency_factor', sa.Numeric(precision=15, scale=8), nullable=True), + sa.Column('not_affect_usd_value', sa.SmallInteger(), nullable=True), + sa.Column('not_affect_customs_value', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_incrementables', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_incrementables_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_incrementables_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_incrementables_tenant_id'), 'pedimento_incrementables', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_indexes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('update_factor_type', sa.SmallInteger(), nullable=True), + sa.Column('update_factor', sa.Numeric(precision=7, scale=4), nullable=True), + sa.Column('manual_update_factor', sa.SmallInteger(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_indexes', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_indexes_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_indexes_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_indexes_tenant_id'), 'pedimento_indexes', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_payments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('acknowledgment', sa.String(length=20), nullable=True), + sa.Column('operation_number', sa.String(length=14), nullable=True), + sa.Column('bank_code', sa.Integer(), nullable=True), + sa.Column('cashier', sa.String(length=2), nullable=True), + sa.Column('date', sa.Date(), nullable=True), + sa.Column('time', sa.Time(), nullable=True), + sa.Column('shift', sa.String(length=1), nullable=True), + sa.Column('total_cash_paid', sa.Integer(), nullable=True), + sa.Column('total_contributions', sa.Integer(), nullable=True), + sa.Column('counter_payment', sa.SmallInteger(), nullable=True), + sa.Column('pece_code', sa.String(length=5), nullable=True), + sa.Column('payment_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_payments', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_payments_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_payments_pedimento_id_key'), + schema='a76' + ) + op.create_index('idx_pedimento_payments_pedimento_id', 'pedimento_payments', ['pedimento_id'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_pedimento_payments_tenant_id'), 'pedimento_payments', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_rectification_destination', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('destination_pedimento_year', sa.String(length=2), nullable=True), + sa.Column('destination_customs_office', sa.String(length=3), nullable=True), + sa.Column('destination_license', sa.String(length=4), nullable=True), + sa.Column('destination_pedimento_number', sa.String(length=7), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_rectification_destination', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_rectification_destination_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_rectification_destination_tenant_id'), 'pedimento_rectification_destination', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_rectification_origin', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('original_pedimento_year', sa.String(length=2), nullable=True), + sa.Column('original_customs_office', sa.String(length=3), nullable=True), + sa.Column('original_license', sa.String(length=4), nullable=True), + sa.Column('original_pedimento_number', sa.String(length=7), nullable=True), + sa.Column('original_pedimento_key', sa.String(length=2), nullable=True), + sa.Column('original_payment_date', sa.DateTime(), nullable=True), + sa.Column('total_cash', sa.Integer(), nullable=True), + sa.Column('total_others', sa.Integer(), nullable=True), + sa.Column('reason', sa.String(length=255), nullable=True), + sa.Column('charge_to_client', sa.SmallInteger(), nullable=True), + sa.Column('use_original_payment_date_for_interest_calc', sa.SmallInteger(), nullable=True), + sa.Column('manual_calculation', sa.SmallInteger(), nullable=True), + sa.Column('original_pedimento_norms', sa.SmallInteger(), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_rectification_origin', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_rectification_origin_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_rectification_origin_tenant_id'), 'pedimento_rectification_origin', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_transport_means', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('destination', sa.SmallInteger(), nullable=True), + sa.Column('entry_exit', sa.String(length=2), nullable=True), + sa.Column('arrival', sa.String(length=2), nullable=True), + sa.Column('departure', sa.String(length=2), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_transport_means', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_transport_means_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_transport_means_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_transport_means_tenant_id'), 'pedimento_transport_means', ['tenant_id'], unique=False, schema='a76') + op.create_table('pedimento_validation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pedimento_id', sa.Integer(), nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=False), + sa.Column('validator', sa.String(length=3), nullable=True), + sa.Column('validation_ack', sa.String(length=8), nullable=True), + sa.Column('pre_ack', sa.String(length=8), nullable=True), + sa.Column('line_signature', sa.String(length=50), nullable=True), + sa.Column('electronic_signature', sa.String(length=999), nullable=True), + sa.Column('certificate_number', sa.String(length=99), nullable=True), + sa.Column('validator_id', sa.Integer(), nullable=True), + sa.Column('responsible_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), + sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_validation', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), + sa.PrimaryKeyConstraint('id', name='pedimento_validation_pkey'), + sa.UniqueConstraint('pedimento_id', name='pedimento_validation_pedimento_id_key'), + schema='a76' + ) + op.create_index(op.f('ix_a76_pedimento_validation_tenant_id'), 'pedimento_validation', ['tenant_id'], unique=False, schema='a76') + op.drop_constraint(op.f('fk_regimenped'), 'code_pedimento_regimens', type_='foreignkey') + op.drop_constraint(op.f('fk_codeped'), 'code_pedimento_regimens', type_='foreignkey') + op.create_foreign_key('fk_codeped', 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'], source_schema='public', referent_schema='public') + op.create_foreign_key('fk_regimenped', 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'], source_schema='public', referent_schema='public') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_regimenped', 'code_pedimento_regimens', schema='public', type_='foreignkey') + op.drop_constraint('fk_codeped', 'code_pedimento_regimens', schema='public', type_='foreignkey') + op.create_foreign_key(op.f('fk_codeped'), 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code']) + op.create_foreign_key(op.f('fk_regimenped'), 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code']) + op.drop_index(op.f('ix_a76_pedimento_validation_tenant_id'), table_name='pedimento_validation', schema='a76') + op.drop_table('pedimento_validation', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_transport_means_tenant_id'), table_name='pedimento_transport_means', schema='a76') + op.drop_table('pedimento_transport_means', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_rectification_origin_tenant_id'), table_name='pedimento_rectification_origin', schema='a76') + op.drop_table('pedimento_rectification_origin', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_rectification_destination_tenant_id'), table_name='pedimento_rectification_destination', schema='a76') + op.drop_table('pedimento_rectification_destination', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_payments_tenant_id'), table_name='pedimento_payments', schema='a76') + op.drop_index('idx_pedimento_payments_pedimento_id', table_name='pedimento_payments', schema='a76') + op.drop_table('pedimento_payments', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_indexes_tenant_id'), table_name='pedimento_indexes', schema='a76') + op.drop_table('pedimento_indexes', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_incrementables_tenant_id'), table_name='pedimento_incrementables', schema='a76') + op.drop_table('pedimento_incrementables', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_decrementables_tenant_id'), table_name='pedimento_decrementables', schema='a76') + op.drop_table('pedimento_decrementables', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_dates_tenant_id'), table_name='pedimento_dates', schema='a76') + op.drop_index('idx_pedimento_dates_pedimento_id', table_name='pedimento_dates', schema='a76') + op.drop_table('pedimento_dates', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_customs_offices_tenant_id'), table_name='pedimento_customs_offices', schema='a76') + op.drop_table('pedimento_customs_offices', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_updates_tenant_id'), table_name='pedimento_config_updates', schema='a76') + op.drop_table('pedimento_config_updates', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_update_rectification_tenant_id'), table_name='pedimento_config_update_rectification', schema='a76') + op.drop_table('pedimento_config_update_rectification', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_surcharges_tenant_id'), table_name='pedimento_config_surcharges', schema='a76') + op.drop_table('pedimento_config_surcharges', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_parameters_tenant_id'), table_name='pedimento_config_parameters', schema='a76') + op.drop_table('pedimento_config_parameters', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_calculations_tenant_id'), table_name='pedimento_config_calculations', schema='a76') + op.drop_table('pedimento_config_calculations', schema='a76') + op.drop_index(op.f('ix_a76_pedimento_config_additional_tenant_id'), table_name='pedimento_config_additional', schema='a76') + op.drop_table('pedimento_config_additional', schema='a76') + op.drop_index(op.f('ix_a76_pedimentos_tenant_id'), table_name='pedimentos', schema='a76') + op.drop_index('idx_pedimentos_status', table_name='pedimentos', schema='a76') + op.drop_index('idx_pedimentos_created_at', table_name='pedimentos', schema='a76') + op.drop_index('idx_pedimentos_client_id', table_name='pedimentos', schema='a76') + op.drop_table('pedimentos', schema='a76') + op.drop_index(op.f('ix_a76_licenses_tenant_id'), table_name='licenses', schema='a76') + op.drop_index(op.f('ix_a76_licenses_id'), table_name='licenses', schema='a76') + op.drop_table('licenses', schema='a76') + op.drop_index(op.f('ix_a76_license_usage_tenant_id'), table_name='license_usage', schema='a76') + op.drop_index(op.f('ix_a76_license_usage_id'), table_name='license_usage', schema='a76') + op.drop_table('license_usage', schema='a76') + op.drop_index(op.f('ix_a76_tenants_slug'), table_name='tenants', schema='a76') + op.drop_index(op.f('ix_a76_tenants_name'), table_name='tenants', schema='a76') + op.drop_index(op.f('ix_a76_tenants_id'), table_name='tenants', schema='a76') + op.drop_table('tenants', schema='a76') + # ### end Alembic commands ### diff --git a/backend/api/v1/modules/a76/licenses/routes.py b/backend/api/v1/modules/a76/licenses/routes.py index 2703840b..730cbade 100644 --- a/backend/api/v1/modules/a76/licenses/routes.py +++ b/backend/api/v1/modules/a76/licenses/routes.py @@ -15,7 +15,7 @@ from .dto import ( ) from .service import LicenseService -router = APIRouter(prefix="/licenses", tags=["Licenses"]) +router = APIRouter(prefix="/licenses") @router.post("/", response_model=LicenseResponseDTO, status_code=201) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py new file mode 100644 index 00000000..5e372c0f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoConfigAdditionalBase(BaseModel): + """Base schema for Pedimento Config Additional""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + add_po_identifier: Optional[int] = Field(None, description="Add PO identifier") + do_not_exempt_norms_complement_x: Optional[int] = Field(None, description="Do not exempt norms complement X") + manual_pedimento_year: Optional[str] = Field(None, max_length=2, description="Manual pedimento year") + enable_import_invoice_recipient: Optional[int] = Field(None, description="Enable import invoice recipient") + send_502_validation_file_for_consolidated: Optional[int] = Field(None, description="Send 502 validation file for consolidated") + add_remove_norms: Optional[int] = Field(None, description="Add/remove norms") + + +class PedimentoConfigAdditionalCreate(PedimentoConfigAdditionalBase): + """Schema for creating a new Pedimento Config Additional""" + pass + + +class PedimentoConfigAdditionalUpdate(BaseModel): + """Schema for updating a Pedimento Config Additional""" + add_po_identifier: Optional[int] = None + do_not_exempt_norms_complement_x: Optional[int] = None + manual_pedimento_year: Optional[str] = Field(None, max_length=2) + enable_import_invoice_recipient: Optional[int] = None + send_502_validation_file_for_consolidated: Optional[int] = None + add_remove_norms: Optional[int] = None + + +class PedimentoConfigAdditionalResponse(PedimentoConfigAdditionalBase): + """Schema for Pedimento Config Additional response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py new file mode 100644 index 00000000..a9289d75 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoConfigCalculationsBase(BaseModel): + """Base schema for Pedimento Config Calculations""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + dta_type: Optional[str] = Field(None, max_length=1, description="DTA type") + dta_operation: Optional[int] = Field(None, description="DTA operation") + dta_vehicle_count: Optional[int] = Field(None, description="DTA vehicle count") + dta_mixed_rate_8permil: Optional[int] = Field(None, description="DTA mixed rate 8 per mil") + pays_vat: Optional[int] = Field(None, description="Pays VAT") + pays_prevalidation: Optional[int] = Field(None, description="Pays prevalidation") + include_sagar_certificate_fee: Optional[int] = Field(None, description="Include SAGAR certificate fee") + fixed_vehicle_dta_fee: Optional[int] = Field(None, description="Fixed vehicle DTA fee") + additional_fixed_fee: Optional[int] = Field(None, description="Additional fixed fee") + additional_fixed_fee_payment_method: Optional[int] = Field(None, description="Additional fixed fee payment method") + + +class PedimentoConfigCalculationsCreate(PedimentoConfigCalculationsBase): + """Schema for creating a new Pedimento Config Calculations""" + pass + + +class PedimentoConfigCalculationsUpdate(BaseModel): + """Schema for updating a Pedimento Config Calculations""" + dta_type: Optional[str] = Field(None, max_length=1) + dta_operation: Optional[int] = None + dta_vehicle_count: Optional[int] = None + dta_mixed_rate_8permil: Optional[int] = None + pays_vat: Optional[int] = None + pays_prevalidation: Optional[int] = None + include_sagar_certificate_fee: Optional[int] = None + fixed_vehicle_dta_fee: Optional[int] = None + additional_fixed_fee: Optional[int] = None + additional_fixed_fee_payment_method: Optional[int] = None + + +class PedimentoConfigCalculationsResponse(PedimentoConfigCalculationsBase): + """Schema for Pedimento Config Calculations response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py new file mode 100644 index 00000000..3590a24b --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from decimal import Decimal +from datetime import datetime + + +class PedimentoConfigParametersBase(BaseModel): + """Base schema for Pedimento Config Parameters""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + is_embassy: Optional[int] = Field(None, description="Is embassy") + embassy_dta: Optional[Decimal] = Field(None, description="Embassy DTA") + rule_3121_section_ii: Optional[int] = Field(None, description="Rule 3.1.21 Section II") + use_previous_tariff: Optional[int] = Field(None, description="Use previous tariff") + use_payment_date_fi: Optional[int] = Field(None, description="Use payment date FI") + add_state_supplier_record_505: Optional[int] = Field(None, description="Add state supplier record 505") + customs_value_calculation: Optional[int] = Field(None, description="Customs value calculation") + two_decimals_unit_value: Optional[int] = Field(None, description="Two decimals unit value") + customs_value_per_item: Optional[int] = Field(None, description="Customs value per item") + is_national_supplier: Optional[int] = Field(None, description="Is national supplier") + is_consolidated: Optional[int] = Field(None, description="Is consolidated") + + +class PedimentoConfigParametersCreate(PedimentoConfigParametersBase): + """Schema for creating a new Pedimento Config Parameters""" + pass + + +class PedimentoConfigParametersUpdate(BaseModel): + """Schema for updating a Pedimento Config Parameters""" + is_embassy: Optional[int] = None + embassy_dta: Optional[Decimal] = None + rule_3121_section_ii: Optional[int] = None + use_previous_tariff: Optional[int] = None + use_payment_date_fi: Optional[int] = None + add_state_supplier_record_505: Optional[int] = None + customs_value_calculation: Optional[int] = None + two_decimals_unit_value: Optional[int] = None + customs_value_per_item: Optional[int] = None + is_national_supplier: Optional[int] = None + is_consolidated: Optional[int] = None + + +class PedimentoConfigParametersResponse(PedimentoConfigParametersBase): + """Schema for Pedimento Config Parameters response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py new file mode 100644 index 00000000..36b4e5f4 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoConfigSurchargesBase(BaseModel): + """Base schema for Pedimento Config Surcharges""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + surcharge_igi: Optional[int] = Field(None, description="Surcharge IGI") + surcharge_dta: Optional[int] = Field(None, description="Surcharge DTA") + surcharge_vat: Optional[int] = Field(None, description="Surcharge VAT") + surcharge_isan: Optional[int] = Field(None, description="Surcharge ISAN") + surcharge_ieps: Optional[int] = Field(None, description="Surcharge IEPS") + surcharge_cc: Optional[int] = Field(None, description="Surcharge CC") + + +class PedimentoConfigSurchargesCreate(PedimentoConfigSurchargesBase): + """Schema for creating a new Pedimento Config Surcharges""" + pass + + +class PedimentoConfigSurchargesUpdate(BaseModel): + """Schema for updating a Pedimento Config Surcharges""" + surcharge_igi: Optional[int] = None + surcharge_dta: Optional[int] = None + surcharge_vat: Optional[int] = None + surcharge_isan: Optional[int] = None + surcharge_ieps: Optional[int] = None + surcharge_cc: Optional[int] = None + + +class PedimentoConfigSurchargesResponse(PedimentoConfigSurchargesBase): + """Schema for Pedimento Config Surcharges response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py new file mode 100644 index 00000000..08af0bb9 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoConfigUpdateRectificationBase(BaseModel): + """Base schema for Pedimento Config Update Rectification""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + update_vat: Optional[int] = Field(None, description="Update VAT") + update_advalorem: Optional[int] = Field(None, description="Update advalorem") + update_cc: Optional[int] = Field(None, description="Update CC") + update_ieps: Optional[int] = Field(None, description="Update IEPS") + calculate_surcharge: Optional[int] = Field(None, description="Calculate surcharge") + + +class PedimentoConfigUpdateRectificationCreate(PedimentoConfigUpdateRectificationBase): + """Schema for creating a new Pedimento Config Update Rectification""" + pass + + +class PedimentoConfigUpdateRectificationUpdate(BaseModel): + """Schema for updating a Pedimento Config Update Rectification""" + update_vat: Optional[int] = None + update_advalorem: Optional[int] = None + update_cc: Optional[int] = None + update_ieps: Optional[int] = None + calculate_surcharge: Optional[int] = None + + +class PedimentoConfigUpdateRectificationResponse(PedimentoConfigUpdateRectificationBase): + """Schema for Pedimento Config Update Rectification response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py new file mode 100644 index 00000000..a1fda392 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoConfigUpdatesBase(BaseModel): + """Base schema for Pedimento Config Updates""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + update_vat: Optional[int] = Field(None, description="Update VAT") + update_advalorem: Optional[int] = Field(None, description="Update advalorem") + update_cc: Optional[int] = Field(None, description="Update CC") + update_ieps: Optional[int] = Field(None, description="Update IEPS") + + +class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase): + """Schema for creating a new Pedimento Config Updates""" + pass + + +class PedimentoConfigUpdatesUpdate(BaseModel): + """Schema for updating a Pedimento Config Updates""" + update_vat: Optional[int] = None + update_advalorem: Optional[int] = None + update_cc: Optional[int] = None + update_ieps: Optional[int] = None + + +class PedimentoConfigUpdatesResponse(PedimentoConfigUpdatesBase): + """Schema for Pedimento Config Updates response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py new file mode 100644 index 00000000..108cf979 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoCustomsOfficesBase(BaseModel): + """Base schema for Pedimento Customs Offices""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + dispatch_customs: Optional[str] = Field(None, max_length=3, description="Dispatch customs") + entry_exit_customs: Optional[str] = Field(None, max_length=3, description="Entry/exit customs") + + +class PedimentoCustomsOfficesCreate(PedimentoCustomsOfficesBase): + """Schema for creating a new Pedimento Customs Offices""" + pass + + +class PedimentoCustomsOfficesUpdate(BaseModel): + """Schema for updating a Pedimento Customs Offices""" + dispatch_customs: Optional[str] = Field(None, max_length=3) + entry_exit_customs: Optional[str] = Field(None, max_length=3) + + +class PedimentoCustomsOfficesResponse(PedimentoCustomsOfficesBase): + """Schema for Pedimento Customs Offices response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py new file mode 100644 index 00000000..7cf2f9dc --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime, time + + +class PedimentoDatesBase(BaseModel): + """Base schema for Pedimento Dates""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + entry_date: Optional[datetime] = Field(None, description="Entry date") + pedimento_date: Optional[datetime] = Field(None, description="Pedimento date") + payment_date: Optional[datetime] = Field(None, description="Payment date") + rectification_payment_date: Optional[datetime] = Field(None, description="Rectification payment date") + extraction_date: Optional[datetime] = Field(None, description="Extraction date") + submission_date: Optional[datetime] = Field(None, description="Submission date") + eucan_date: Optional[datetime] = Field(None, description="EUCAN date") + original_date: Optional[datetime] = Field(None, description="Original date") + start_date: Optional[datetime] = Field(None, description="Start date") + end_date: Optional[datetime] = Field(None, description="End date") + capture_date: Optional[datetime] = Field(None, description="Capture date") + capture_time: Optional[time] = Field(None, description="Capture time") + + +class PedimentoDatesCreate(PedimentoDatesBase): + """Schema for creating a new Pedimento Dates""" + pass + + +class PedimentoDatesUpdate(BaseModel): + """Schema for updating a Pedimento Dates""" + entry_date: Optional[datetime] = None + pedimento_date: Optional[datetime] = None + payment_date: Optional[datetime] = None + rectification_payment_date: Optional[datetime] = None + extraction_date: Optional[datetime] = None + submission_date: Optional[datetime] = None + eucan_date: Optional[datetime] = None + original_date: Optional[datetime] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + capture_date: Optional[datetime] = None + capture_time: Optional[time] = None + + +class PedimentoDatesResponse(PedimentoDatesBase): + """Schema for Pedimento Dates response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py new file mode 100644 index 00000000..ec76a40e --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from decimal import Decimal +from datetime import datetime + + +class PedimentoDecrementablesBase(BaseModel): + """Base schema for Pedimento Decrementables""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + freight: Optional[Decimal] = Field(None, description="Freight") + insurance: Optional[Decimal] = Field(None, description="Insurance") + loading: Optional[Decimal] = Field(None, description="Loading") + unloading: Optional[Decimal] = Field(None, description="Unloading") + others: Optional[Decimal] = Field(None, description="Others") + currency: Optional[str] = Field(None, max_length=3, description="Currency") + currency_factor: Optional[Decimal] = Field(None, description="Currency factor") + not_affect_usd_value: Optional[int] = Field(None, description="Not affect USD value") + not_affect_customs_value: Optional[int] = Field(None, description="Not affect customs value") + + +class PedimentoDecrementablesCreate(PedimentoDecrementablesBase): + """Schema for creating a new Pedimento Decrementables""" + pass + + +class PedimentoDecrementablesUpdate(BaseModel): + """Schema for updating a Pedimento Decrementables""" + freight: Optional[Decimal] = None + insurance: Optional[Decimal] = None + loading: Optional[Decimal] = None + unloading: Optional[Decimal] = None + others: Optional[Decimal] = None + currency: Optional[str] = Field(None, max_length=3) + currency_factor: Optional[Decimal] = None + not_affect_usd_value: Optional[int] = None + not_affect_customs_value: Optional[int] = None + + +class PedimentoDecrementablesResponse(PedimentoDecrementablesBase): + """Schema for Pedimento Decrementables response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py new file mode 100644 index 00000000..fd22d50b --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py @@ -0,0 +1,47 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from decimal import Decimal +from datetime import datetime + + +class PedimentoIncrementablesBase(BaseModel): + """Base schema for Pedimento Incrementables""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + insured_value: Optional[Decimal] = Field(None, description="Insured value") + freight: Optional[Decimal] = Field(None, description="Freight") + insurance: Optional[Decimal] = Field(None, description="Insurance") + packaging: Optional[Decimal] = Field(None, description="Packaging") + others: Optional[Decimal] = Field(None, description="Others") + deductibles: Optional[Decimal] = Field(None, description="Deductibles") + currency: Optional[str] = Field(None, max_length=3, description="Currency") + currency_factor: Optional[Decimal] = Field(None, description="Currency factor") + not_affect_usd_value: Optional[int] = Field(None, description="Not affect USD value") + not_affect_customs_value: Optional[int] = Field(None, description="Not affect customs value") + + +class PedimentoIncrementablesCreate(PedimentoIncrementablesBase): + """Schema for creating a new Pedimento Incrementables""" + pass + + +class PedimentoIncrementablesUpdate(BaseModel): + """Schema for updating a Pedimento Incrementables""" + insured_value: Optional[Decimal] = None + freight: Optional[Decimal] = None + insurance: Optional[Decimal] = None + packaging: Optional[Decimal] = None + others: Optional[Decimal] = None + deductibles: Optional[Decimal] = None + currency: Optional[str] = Field(None, max_length=3) + currency_factor: Optional[Decimal] = None + not_affect_usd_value: Optional[int] = None + not_affect_customs_value: Optional[int] = None + + +class PedimentoIncrementablesResponse(PedimentoIncrementablesBase): + """Schema for Pedimento Incrementables response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py new file mode 100644 index 00000000..6dd993c9 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from decimal import Decimal +from datetime import datetime + + +class PedimentoIndexesBase(BaseModel): + """Base schema for Pedimento Indexes""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + update_factor_type: Optional[int] = Field(None, description="Update factor type") + update_factor: Optional[Decimal] = Field(None, description="Update factor") + manual_update_factor: Optional[int] = Field(None, description="Manual update factor") + + +class PedimentoIndexesCreate(PedimentoIndexesBase): + """Schema for creating a new Pedimento Indexes""" + pass + + +class PedimentoIndexesUpdate(BaseModel): + """Schema for updating a Pedimento Indexes""" + update_factor_type: Optional[int] = None + update_factor: Optional[Decimal] = None + manual_update_factor: Optional[int] = None + + +class PedimentoIndexesResponse(PedimentoIndexesBase): + """Schema for Pedimento Indexes response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py new file mode 100644 index 00000000..4421637c --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime, date as Date, time as Time + + +class PedimentoPaymentsBase(BaseModel): + """Base schema for Pedimento Payments""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + acknowledgment: Optional[str] = Field(None, max_length=20, description="Acknowledgment") + operation_number: Optional[str] = Field(None, max_length=14, description="Operation number") + bank_code: Optional[int] = Field(None, description="Bank code") + cashier: Optional[str] = Field(None, max_length=2, description="Cashier") + date: Optional[Date] = Field(None, description="Date") + time: Optional[Time] = Field(None, description="Time") + shift: Optional[str] = Field(None, max_length=1, description="Shift") + total_cash_paid: Optional[int] = Field(None, description="Total cash paid") + total_contributions: Optional[int] = Field(None, description="Total contributions") + counter_payment: Optional[int] = Field(None, description="Counter payment") + pece_code: Optional[str] = Field(None, max_length=5, description="PECE code") + payment_id: Optional[int] = Field(None, description="Payment ID") + + +class PedimentoPaymentsCreate(PedimentoPaymentsBase): + """Schema for creating a new Pedimento Payments""" + pass + + +class PedimentoPaymentsUpdate(BaseModel): + """Schema for updating a Pedimento Payments""" + acknowledgment: Optional[str] = Field(None, max_length=20) + operation_number: Optional[str] = Field(None, max_length=14) + bank_code: Optional[int] = None + cashier: Optional[str] = Field(None, max_length=2) + date: Optional[Date] = None + time: Optional[Time] = None + shift: Optional[str] = Field(None, max_length=1) + total_cash_paid: Optional[int] = None + total_contributions: Optional[int] = None + counter_payment: Optional[int] = None + pece_code: Optional[str] = Field(None, max_length=5) + payment_id: Optional[int] = None + + +class PedimentoPaymentsResponse(PedimentoPaymentsBase): + """Schema for Pedimento Payments response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py new file mode 100644 index 00000000..1f35d1af --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional + + +class PedimentoRectificationDestinationBase(BaseModel): + """Base schema for Pedimento Rectification Destination""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + destination_pedimento_year: Optional[str] = Field(None, max_length=2, description="Destination pedimento year") + destination_customs_office: Optional[str] = Field(None, max_length=3, description="Destination customs office") + destination_license: Optional[str] = Field(None, max_length=4, description="Destination license") + destination_pedimento_number: Optional[str] = Field(None, max_length=7, description="Destination pedimento number") + + +class PedimentoRectificationDestinationCreate(PedimentoRectificationDestinationBase): + """Schema for creating a new Pedimento Rectification Destination""" + pass + + +class PedimentoRectificationDestinationUpdate(BaseModel): + """Schema for updating a Pedimento Rectification Destination""" + destination_pedimento_year: Optional[str] = Field(None, max_length=2) + destination_customs_office: Optional[str] = Field(None, max_length=3) + destination_license: Optional[str] = Field(None, max_length=4) + destination_pedimento_number: Optional[str] = Field(None, max_length=7) + + +class PedimentoRectificationDestinationResponse(PedimentoRectificationDestinationBase): + """Schema for Pedimento Rectification Destination response""" + id: int + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py new file mode 100644 index 00000000..ee8dfeda --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py @@ -0,0 +1,51 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoRectificationOriginBase(BaseModel): + """Base schema for Pedimento Rectification Origin""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + original_pedimento_year: Optional[str] = Field(None, max_length=2, description="Original pedimento year") + original_customs_office: Optional[str] = Field(None, max_length=3, description="Original customs office") + original_license: Optional[str] = Field(None, max_length=4, description="Original license") + original_pedimento_number: Optional[str] = Field(None, max_length=7, description="Original pedimento number") + original_pedimento_key: Optional[str] = Field(None, max_length=2, description="Original pedimento key") + original_payment_date: Optional[datetime] = Field(None, description="Original payment date") + total_cash: Optional[int] = Field(None, description="Total cash") + total_others: Optional[int] = Field(None, description="Total others") + reason: Optional[str] = Field(None, max_length=255, description="Reason") + charge_to_client: Optional[int] = Field(None, description="Charge to client") + use_original_payment_date_for_interest_calc: Optional[int] = Field(None, description="Use original payment date for interest calculation") + manual_calculation: Optional[int] = Field(None, description="Manual calculation") + original_pedimento_norms: Optional[int] = Field(None, description="Original pedimento norms") + + +class PedimentoRectificationOriginCreate(PedimentoRectificationOriginBase): + """Schema for creating a new Pedimento Rectification Origin""" + pass + + +class PedimentoRectificationOriginUpdate(BaseModel): + """Schema for updating a Pedimento Rectification Origin""" + original_pedimento_year: Optional[str] = Field(None, max_length=2) + original_customs_office: Optional[str] = Field(None, max_length=3) + original_license: Optional[str] = Field(None, max_length=4) + original_pedimento_number: Optional[str] = Field(None, max_length=7) + original_pedimento_key: Optional[str] = Field(None, max_length=2) + original_payment_date: Optional[datetime] = None + total_cash: Optional[int] = None + total_others: Optional[int] = None + reason: Optional[str] = Field(None, max_length=255) + charge_to_client: Optional[int] = None + use_original_payment_date_for_interest_calc: Optional[int] = None + manual_calculation: Optional[int] = None + original_pedimento_norms: Optional[int] = None + + +class PedimentoRectificationOriginResponse(PedimentoRectificationOriginBase): + """Schema for Pedimento Rectification Origin response""" + id: int + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py new file mode 100644 index 00000000..3bec5e34 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoTransportMeansBase(BaseModel): + """Base schema for Pedimento Transport Means""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + destination: Optional[int] = Field(None, description="Destination") + entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") + arrival: Optional[str] = Field(None, max_length=2, description="Arrival") + departure: Optional[str] = Field(None, max_length=2, description="Departure") + + +class PedimentoTransportMeansCreate(PedimentoTransportMeansBase): + """Schema for creating a new Pedimento Transport Means""" + pass + + +class PedimentoTransportMeansUpdate(BaseModel): + """Schema for updating a Pedimento Transport Means""" + destination: Optional[int] = None + entry_exit: Optional[str] = Field(None, max_length=2) + arrival: Optional[str] = Field(None, max_length=2) + departure: Optional[str] = Field(None, max_length=2) + + +class PedimentoTransportMeansResponse(PedimentoTransportMeansBase): + """Schema for Pedimento Transport Means response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py new file mode 100644 index 00000000..817394e5 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py @@ -0,0 +1,42 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class PedimentoValidationBase(BaseModel): + """Base schema for Pedimento Validation""" + pedimento_id: int = Field(..., description="Pedimento ID") + tenant_id: int = Field(..., description="Tenant ID") + validator: Optional[str] = Field(None, max_length=3, description="Validator") + validation_ack: Optional[str] = Field(None, max_length=8, description="Validation acknowledgment") + pre_ack: Optional[str] = Field(None, max_length=8, description="Pre-acknowledgment") + line_signature: Optional[str] = Field(None, max_length=50, description="Line signature") + electronic_signature: Optional[str] = Field(None, max_length=999, description="Electronic signature") + certificate_number: Optional[str] = Field(None, max_length=99, description="Certificate number") + validator_id: Optional[int] = Field(None, description="Validator ID") + responsible_id: Optional[int] = Field(None, description="Responsible ID") + + +class PedimentoValidationCreate(PedimentoValidationBase): + """Schema for creating a new Pedimento Validation""" + pass + + +class PedimentoValidationUpdate(BaseModel): + """Schema for updating a Pedimento Validation""" + validator: Optional[str] = Field(None, max_length=3) + validation_ack: Optional[str] = Field(None, max_length=8) + pre_ack: Optional[str] = Field(None, max_length=8) + line_signature: Optional[str] = Field(None, max_length=50) + electronic_signature: Optional[str] = Field(None, max_length=999) + certificate_number: Optional[str] = Field(None, max_length=99) + validator_id: Optional[int] = None + responsible_id: Optional[int] = None + + +class PedimentoValidationResponse(PedimentoValidationBase): + """Schema for Pedimento Validation response""" + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py new file mode 100644 index 00000000..6ac4c4cf --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from decimal import Decimal +from datetime import datetime + + +class PedimentosBase(BaseModel): + """Base schema for Pedimentos""" + year: Optional[str] = Field(None, max_length=2, description="Year") + customs_office: Optional[str] = Field(None, max_length=2, description="Customs office") + license: Optional[str] = Field(None, max_length=4, description="License") + pedimento_number: Optional[str] = Field(None, max_length=7, description="Pedimento number") + client_id: Optional[int] = Field(None, description="Client ID") + operation_type: Optional[int] = Field(None, description="Operation type") + pedimento_type: Optional[int] = Field(None, description="Pedimento type") + pedimento_key: Optional[str] = Field(None, max_length=2, description="Pedimento key") + regime: Optional[str] = Field(None, max_length=3, description="Regime") + status: Optional[str] = Field(None, max_length=30, description="Status") + usd_value: Optional[Decimal] = Field(None, description="USD value") + paid_price: Optional[Decimal] = Field(None, description="Paid price") + gross_weight: Optional[Decimal] = Field(None, description="Gross weight") + exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate") + + +class PedimentosCreate(PedimentosBase): + """Schema for creating a new Pedimento""" + pass + + +class PedimentosUpdate(BaseModel): + """Schema for updating a Pedimento""" + year: Optional[str] = Field(None, max_length=2) + customs_office: Optional[str] = Field(None, max_length=2) + license: Optional[str] = Field(None, max_length=4) + pedimento_number: Optional[str] = Field(None, max_length=7) + client_id: Optional[int] = None + operation_type: Optional[int] = None + pedimento_type: Optional[int] = None + pedimento_key: Optional[str] = Field(None, max_length=2) + regime: Optional[str] = Field(None, max_length=3) + status: Optional[str] = Field(None, max_length=30) + usd_value: Optional[Decimal] = None + paid_price: Optional[Decimal] = None + gross_weight: Optional[Decimal] = None + exchange_rate: Optional[Decimal] = None + + +class PedimentosResponse(PedimentosBase): + """Schema for Pedimento response""" + id: int + tenant_id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py new file mode 100644 index 00000000..78ee3907 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py @@ -0,0 +1,27 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoConfigAdditional(Base): + __tablename__ = 'pedimento_config_additional' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_additional'), + PrimaryKeyConstraint('id', name='pedimento_config_additional_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_additional_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + add_po_identifier = mapped_column(SmallInteger) + do_not_exempt_norms_complement_x = mapped_column(SmallInteger) + manual_pedimento_year = mapped_column(String(2)) + enable_import_invoice_recipient = mapped_column(SmallInteger) + send_502_validation_file_for_consolidated = mapped_column(SmallInteger) + add_remove_norms = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_additional') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py new file mode 100644 index 00000000..da711625 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py @@ -0,0 +1,31 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoConfigCalculations(Base): + __tablename__ = 'pedimento_config_calculations' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_calculations'), + PrimaryKeyConstraint('id', name='pedimento_config_calculations_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_calculations_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + dta_type = mapped_column(String(1)) + dta_operation = mapped_column(SmallInteger) + dta_vehicle_count = mapped_column(SmallInteger) + dta_mixed_rate_8permil = mapped_column(SmallInteger) + pays_vat = mapped_column(SmallInteger) + pays_prevalidation = mapped_column(SmallInteger) + include_sagar_certificate_fee = mapped_column(SmallInteger) + fixed_vehicle_dta_fee = mapped_column(SmallInteger) + additional_fixed_fee = mapped_column(SmallInteger) + additional_fixed_fee_payment_method = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_calculations') diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py new file mode 100644 index 00000000..bce09458 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py @@ -0,0 +1,33 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + + +class PedimentoConfigParameters(Base): + __tablename__ = 'pedimento_config_parameters' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_parameters'), + PrimaryKeyConstraint('id', name='pedimento_config_parameters_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_parameters_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + is_embassy = mapped_column(SmallInteger) + embassy_dta = mapped_column(Numeric(11, 2)) + rule_3121_section_ii = mapped_column(SmallInteger) + use_previous_tariff = mapped_column(SmallInteger) + use_payment_date_fi = mapped_column(SmallInteger) + add_state_supplier_record_505 = mapped_column(SmallInteger) + customs_value_calculation = mapped_column(SmallInteger) + two_decimals_unit_value = mapped_column(SmallInteger) + customs_value_per_item = mapped_column(SmallInteger) + is_national_supplier = mapped_column(SmallInteger) + is_consolidated = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_parameters') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py new file mode 100644 index 00000000..1ba6e0d6 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py @@ -0,0 +1,27 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoConfigSurcharges(Base): + __tablename__ = 'pedimento_config_surcharges' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_surcharges'), + PrimaryKeyConstraint('id', name='pedimento_config_surcharges_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + surcharge_igi = mapped_column(SmallInteger) + surcharge_dta = mapped_column(SmallInteger) + surcharge_vat = mapped_column(SmallInteger) + surcharge_isan = mapped_column(SmallInteger) + surcharge_ieps = mapped_column(SmallInteger) + surcharge_cc = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_surcharges') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py new file mode 100644 index 00000000..f7e1ea42 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py @@ -0,0 +1,26 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoConfigUpdateRectification(Base): + __tablename__ = 'pedimento_config_update_rectification' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_update_rectification'), + PrimaryKeyConstraint('id', name='pedimento_config_update_rectification_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + update_vat = mapped_column(SmallInteger) + update_advalorem = mapped_column(SmallInteger) + update_cc = mapped_column(SmallInteger) + update_ieps = mapped_column(SmallInteger) + calculate_surcharge = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_update_rectification') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py new file mode 100644 index 00000000..1fcedb8c --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py @@ -0,0 +1,25 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoConfigUpdates(Base): + __tablename__ = 'pedimento_config_updates' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_updates'), + PrimaryKeyConstraint('id', name='pedimento_config_updates_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_config_updates_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + update_vat = mapped_column(SmallInteger) + update_advalorem = mapped_column(SmallInteger) + update_cc = mapped_column(SmallInteger) + update_ieps = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_updates') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py new file mode 100644 index 00000000..f6af5419 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py @@ -0,0 +1,23 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoCustomsOffices(Base): + __tablename__ = 'pedimento_customs_offices' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_customs_offices'), + PrimaryKeyConstraint('id', name='pedimento_customs_offices_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_customs_offices_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + dispatch_customs = mapped_column(String(3)) + entry_exit_customs = mapped_column(String(3)) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_customs_offices') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py new file mode 100644 index 00000000..406974c2 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py @@ -0,0 +1,34 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, Time, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoDates(Base): + __tablename__ = 'pedimento_dates' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_dates'), + PrimaryKeyConstraint('id', name='pedimento_dates_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_dates_pedimento_id_key'), + Index('idx_pedimento_dates_pedimento_id', 'pedimento_id'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + entry_date = mapped_column(DateTime) + pedimento_date = mapped_column(DateTime) + payment_date = mapped_column(DateTime) + rectification_payment_date = mapped_column(DateTime) + extraction_date = mapped_column(DateTime) + submission_date = mapped_column(DateTime) + eucan_date = mapped_column(DateTime) + original_date = mapped_column(DateTime) + start_date = mapped_column(DateTime) + end_date = mapped_column(DateTime) + capture_date = mapped_column(DateTime) + capture_time = mapped_column(Time) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_dates') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py new file mode 100644 index 00000000..04376b85 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py @@ -0,0 +1,30 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoDecrementables(Base): + __tablename__ = 'pedimento_decrementables' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_decrementables'), + PrimaryKeyConstraint('id', name='pedimento_decrementables_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_decrementables_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + freight = mapped_column(Numeric(13, 2)) + insurance = mapped_column(Numeric(13, 2)) + loading = mapped_column(Numeric(13, 2)) + unloading = mapped_column(Numeric(13, 2)) + others = mapped_column(Numeric(13, 2)) + currency = mapped_column(String(3)) + currency_factor = mapped_column(Numeric(15, 8)) + not_affect_usd_value = mapped_column(SmallInteger) + not_affect_customs_value = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_decrementables') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py new file mode 100644 index 00000000..44c3f987 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py @@ -0,0 +1,31 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoIncrementables(Base): + __tablename__ = 'pedimento_incrementables' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_incrementables'), + PrimaryKeyConstraint('id', name='pedimento_incrementables_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_incrementables_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + insured_value = mapped_column(Numeric(13, 2)) + freight = mapped_column(Numeric(13, 2)) + insurance = mapped_column(Numeric(13, 2)) + packaging = mapped_column(Numeric(13, 2)) + others = mapped_column(Numeric(13, 3)) + deductibles = mapped_column(Numeric(13, 3)) + currency = mapped_column(String(3)) + currency_factor = mapped_column(Numeric(15, 8)) + not_affect_usd_value = mapped_column(SmallInteger) + not_affect_customs_value = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_incrementables') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py new file mode 100644 index 00000000..3704e981 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py @@ -0,0 +1,24 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoIndexes(Base): + __tablename__ = 'pedimento_indexes' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_indexes'), + PrimaryKeyConstraint('id', name='pedimento_indexes_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_indexes_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + update_factor_type = mapped_column(SmallInteger) + update_factor = mapped_column(Numeric(7, 4)) + manual_update_factor = mapped_column(SmallInteger) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_indexes') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py new file mode 100644 index 00000000..5360af8b --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py @@ -0,0 +1,34 @@ +from sqlalchemy import Date, DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, SmallInteger, String, Time, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoPayments(Base): + __tablename__ = 'pedimento_payments' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_payments'), + PrimaryKeyConstraint('id', name='pedimento_payments_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_payments_pedimento_id_key'), + Index('idx_pedimento_payments_pedimento_id', 'pedimento_id'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + acknowledgment = mapped_column(String(20)) + operation_number = mapped_column(String(14)) + bank_code = mapped_column(Integer) + cashier = mapped_column(String(2)) + date = mapped_column(Date) + time = mapped_column(Time) + shift = mapped_column(String(1)) + total_cash_paid = mapped_column(Integer) + total_contributions = mapped_column(Integer) + counter_payment = mapped_column(SmallInteger) + pece_code = mapped_column(String(5)) + payment_id = mapped_column(Integer) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_payments') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py new file mode 100644 index 00000000..6b4a18fa --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py @@ -0,0 +1,24 @@ +from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoRectificationDestination(Base): + __tablename__ = 'pedimento_rectification_destination' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_destination'), + PrimaryKeyConstraint('id', name='pedimento_rectification_destination_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + destination_pedimento_year = mapped_column(String(2)) + destination_customs_office = mapped_column(String(3)) + destination_license = mapped_column(String(4)) + destination_pedimento_number = mapped_column(String(7)) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_destination') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py new file mode 100644 index 00000000..9e92068f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py @@ -0,0 +1,33 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoRectificationOrigin(Base): + __tablename__ = 'pedimento_rectification_origin' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_origin'), + PrimaryKeyConstraint('id', name='pedimento_rectification_origin_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + original_pedimento_year = mapped_column(String(2)) + original_customs_office = mapped_column(String(3)) + original_license = mapped_column(String(4)) + original_pedimento_number = mapped_column(String(7)) + original_pedimento_key = mapped_column(String(2)) + original_payment_date = mapped_column(DateTime) + total_cash = mapped_column(Integer) + total_others = mapped_column(Integer) + reason = mapped_column(String(255)) + charge_to_client = mapped_column(SmallInteger) + use_original_payment_date_for_interest_calc = mapped_column(SmallInteger) + manual_calculation = mapped_column(SmallInteger) + original_pedimento_norms = mapped_column(SmallInteger) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_origin') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py new file mode 100644 index 00000000..fa3d5508 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py @@ -0,0 +1,25 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoTransportMeans(Base): + __tablename__ = 'pedimento_transport_means' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_transport_means'), + PrimaryKeyConstraint('id', name='pedimento_transport_means_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_transport_means_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + destination = mapped_column(SmallInteger) + entry_exit = mapped_column(String(2)) + arrival = mapped_column(String(2)) + departure = mapped_column(String(2)) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_transport_means') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py new file mode 100644 index 00000000..9965c08a --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py @@ -0,0 +1,29 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoValidation(Base): + __tablename__ = 'pedimento_validation' #PedimentoValidacion + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_validation'), + PrimaryKeyConstraint('id', name='pedimento_validation_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_validation_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + tenant_id = mapped_column(Integer, nullable=False, index=True) + validator = mapped_column(String(3)) #validador + validation_ack = mapped_column(String(8)) #acuse_validacion + pre_ack = mapped_column(String(8)) #acuse_previo + line_signature = mapped_column(String(50)) #firma_linea_captura + electronic_signature = mapped_column(String(999)) #firma_electronica + certificate_number = mapped_column(String(99)) #numero_certificado + validator_id = mapped_column(Integer) + responsible_id = mapped_column(Integer) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_validation') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py new file mode 100644 index 00000000..977003d9 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -0,0 +1,52 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, Numeric, PrimaryKeyConstraint, String, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + + +class Pedimentos(Base): + __tablename__ = 'pedimentos' + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + PrimaryKeyConstraint('id', name='pedimentos_pkey'), + Index('idx_pedimentos_client_id', 'client_id'), + Index('idx_pedimentos_created_at', 'created_at'), + Index('idx_pedimentos_status', 'status'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + tenant_id = mapped_column(Integer, nullable=False, index=True) + year = mapped_column(String(2)) + customs_office = mapped_column(String(2)) + license = mapped_column(String(4)) + pedimento_number = mapped_column(String(7)) + client_id = mapped_column(Integer) + operation_type = mapped_column(Integer) + pedimento_type = mapped_column(Integer) + pedimento_key = mapped_column(String(2)) + regime = mapped_column(String(3)) + status = mapped_column(String(30)) + usd_value = mapped_column(Numeric(17, 6)) + paid_price = mapped_column(Numeric(17, 6)) + gross_weight = mapped_column(Numeric(19, 3)) + exchange_rate = mapped_column(Numeric(9, 5)) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento_config_additional: Mapped['PedimentoConfigAdditional'] = relationship('PedimentoConfigAdditional', uselist=False, back_populates='pedimento') + pedimento_config_calculations: Mapped['PedimentoConfigCalculations'] = relationship('PedimentoConfigCalculations', uselist=False, back_populates='pedimento') + pedimento_config_parameters: Mapped['PedimentoConfigParameters'] = relationship('PedimentoConfigParameters', uselist=False, back_populates='pedimento') + pedimento_config_surcharges: Mapped['PedimentoConfigSurcharges'] = relationship('PedimentoConfigSurcharges', uselist=False, back_populates='pedimento') + pedimento_config_update_rectification: Mapped['PedimentoConfigUpdateRectification'] = relationship('PedimentoConfigUpdateRectification', uselist=False, back_populates='pedimento') + pedimento_config_updates: Mapped['PedimentoConfigUpdates'] = relationship('PedimentoConfigUpdates', uselist=False, back_populates='pedimento') + pedimento_customs_offices: Mapped['PedimentoCustomsOffices'] = relationship('PedimentoCustomsOffices', uselist=False, back_populates='pedimento') + pedimento_dates: Mapped['PedimentoDates'] = relationship('PedimentoDates', uselist=False, back_populates='pedimento') + pedimento_decrementables: Mapped['PedimentoDecrementables'] = relationship('PedimentoDecrementables', uselist=False, back_populates='pedimento') + pedimento_incrementables: Mapped['PedimentoIncrementables'] = relationship('PedimentoIncrementables', uselist=False, back_populates='pedimento') + pedimento_indexes: Mapped['PedimentoIndexes'] = relationship('PedimentoIndexes', uselist=False, back_populates='pedimento') + pedimento_payments: Mapped['PedimentoPayments'] = relationship('PedimentoPayments', uselist=False, back_populates='pedimento') + pedimento_rectification_destination: Mapped['PedimentoRectificationDestination'] = relationship('PedimentoRectificationDestination', uselist=False, back_populates='pedimento') + pedimento_rectification_origin: Mapped['PedimentoRectificationOrigin'] = relationship('PedimentoRectificationOrigin', uselist=False, back_populates='pedimento') + pedimento_transport_means: Mapped['PedimentoTransportMeans'] = relationship('PedimentoTransportMeans', uselist=False, back_populates='pedimento') + pedimento_validation: Mapped['PedimentoValidation'] = relationship('PedimentoValidation', uselist=False, back_populates='pedimento') + diff --git a/backend/api/v1/modules/a76/pedmientos/router.py b/backend/api/v1/modules/a76/pedmientos/router.py new file mode 100644 index 00000000..07084d4f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/router.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter + +from .routes.pedimento_config_additional import router as pedimento_config_additional_router +from .routes.pedimento_config_calculations import router as pedimento_config_calculations_router +from .routes.pedimento_config_parameters import router as pedimento_config_parameters_router +from .routes.pedimento_config_surcharges import router as pedimento_config_surcharges_router +from .routes.pedimento_config_update_rectification import router as pedimento_config_update_rectification_router +from .routes.pedimento_config_updates import router as pedimento_config_updates_router +from .routes.pedimento_customs_offices import router as pedimento_customs_offices_router +from .routes.pedimento_dates import router as pedimento_dates_router +from .routes.pedimento_decrementables import router as pedimento_decrementables_router +from .routes.pedimento_incrementables import router as pedimento_incrementables_router +from .routes.pedimento_indexes import router as pedimento_indexes_router +from .routes.pedimento_payments import router as pedimento_payments_router +from .routes.pedimento_rectification_destination import router as pedimento_rectification_destination_router +from .routes.pedimento_rectification_origin import router as pedimento_rectification_origin_router +from .routes.pedimento_transport_means import router as pedimento_transport_means_router +from .routes.pedimento_validation import router as pedimento_validation_router +from .routes.pedimentos import router as pedimentos_router + +router = APIRouter() + +router.include_router(pedimento_config_additional_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_additional"]) +router.include_router(pedimento_config_calculations_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_calculations"]) +router.include_router(pedimento_config_parameters_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_parameters"]) +router.include_router(pedimento_config_surcharges_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_surcharges"]) +router.include_router(pedimento_config_update_rectification_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_update_rectification"]) +router.include_router(pedimento_config_updates_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_updates"]) +router.include_router(pedimento_customs_offices_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_customs_offices"]) +router.include_router(pedimento_dates_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_dates"]) +router.include_router(pedimento_decrementables_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_decrementables"]) +router.include_router(pedimento_incrementables_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_incrementables"]) +router.include_router(pedimento_indexes_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_indexes"]) +router.include_router(pedimento_payments_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_payments"]) +router.include_router(pedimento_rectification_destination_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_rectification_destination"]) +router.include_router(pedimento_rectification_origin_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_rectification_origin"]) +router.include_router(pedimento_transport_means_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_transport_means"]) +router.include_router(pedimento_validation_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_validation"]) +router.include_router(pedimentos_router, prefix="/pedimentos", tags=["a76 / pedimentos"]) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py new file mode 100644 index 00000000..206227c2 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py @@ -0,0 +1,94 @@ +""" +Routes for PedimentoConfigAdditional CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_additional import PedimentoConfigAdditionalService +from ..dtos.pedimento_config_additional import ( + PedimentoConfigAdditionalCreate, + PedimentoConfigAdditionalUpdate, + PedimentoConfigAdditionalResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-additional") + + +@router.get("/", response_model=PedimentoConfigAdditionalResponse) +async def get_config_additional( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config additional by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config additional not found") + + return config + + +@router.post("/", response_model=PedimentoConfigAdditionalResponse, status_code=201) +async def create_config_additional( + pedimento_id: int, + data: PedimentoConfigAdditionalCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config additional""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id and tenant_id match + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + if data.tenant_id != tenant_id: + raise HTTPException(status_code=403, detail="Tenant ID mismatch") + + config = PedimentoConfigAdditionalService.create(db, data) + return config + + +@router.put("/", response_model=PedimentoConfigAdditionalResponse) +async def update_config_additional( + pedimento_id: int, + data: PedimentoConfigAdditionalUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config additional""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigAdditionalService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config additional not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_additional( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config additional""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigAdditionalService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config additional not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py new file mode 100644 index 00000000..7b65b272 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoConfigCalculations CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService +from ..dtos.pedimento_config_calculations import ( + PedimentoConfigCalculationsCreate, + PedimentoConfigCalculationsUpdate, + PedimentoConfigCalculationsResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-calculations") + + +@router.get("/", response_model=PedimentoConfigCalculationsResponse) +async def get_config_calculations( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config calculations by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config calculations not found") + + return config + + +@router.post("/", response_model=PedimentoConfigCalculationsResponse, status_code=201) +async def create_config_calculations( + pedimento_id: int, + data: PedimentoConfigCalculationsCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config calculations""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + config = PedimentoConfigCalculationsService.create(db, data, tenant_id) + return config + + +@router.put("/", response_model=PedimentoConfigCalculationsResponse) +async def update_config_calculations( + pedimento_id: int, + data: PedimentoConfigCalculationsUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config calculations""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigCalculationsService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config calculations not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_calculations( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config calculations""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigCalculationsService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config calculations not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py new file mode 100644 index 00000000..92b9b023 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoConfigParameters CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_parameters import PedimentoConfigParametersService +from ..dtos.pedimento_config_parameters import ( + PedimentoConfigParametersCreate, + PedimentoConfigParametersUpdate, + PedimentoConfigParametersResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-parameters") + + +@router.get("/", response_model=PedimentoConfigParametersResponse) +async def get_config_parameters( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config parameters by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config parameters not found") + + return config + + +@router.post("/", response_model=PedimentoConfigParametersResponse, status_code=201) +async def create_config_parameters( + pedimento_id: int, + data: PedimentoConfigParametersCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config parameters""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + config = PedimentoConfigParametersService.create(db, data, tenant_id) + return config + + +@router.put("/", response_model=PedimentoConfigParametersResponse) +async def update_config_parameters( + pedimento_id: int, + data: PedimentoConfigParametersUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config parameters""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigParametersService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config parameters not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_parameters( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config parameters""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigParametersService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config parameters not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py new file mode 100644 index 00000000..cd5d8417 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoConfigSurcharges CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_surcharges import PedimentoConfigSurchargesService +from ..dtos.pedimento_config_surcharges import ( + PedimentoConfigSurchargesCreate, + PedimentoConfigSurchargesUpdate, + PedimentoConfigSurchargesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-surcharges") + + +@router.get("/", response_model=PedimentoConfigSurchargesResponse) +async def get_config_surcharges( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config surcharges by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config surcharges not found") + + return config + + +@router.post("/", response_model=PedimentoConfigSurchargesResponse, status_code=201) +async def create_config_surcharges( + pedimento_id: int, + data: PedimentoConfigSurchargesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config surcharges""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + config = PedimentoConfigSurchargesService.create(db, data, tenant_id) + return config + + +@router.put("/", response_model=PedimentoConfigSurchargesResponse) +async def update_config_surcharges( + pedimento_id: int, + data: PedimentoConfigSurchargesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config surcharges""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigSurchargesService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config surcharges not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_surcharges( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config surcharges""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigSurchargesService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config surcharges not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py new file mode 100644 index 00000000..b537a8a7 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoConfigUpdateRectification CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_update_rectification import PedimentoConfigUpdateRectificationService +from ..dtos.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectificationCreate, + PedimentoConfigUpdateRectificationUpdate, + PedimentoConfigUpdateRectificationResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-update-rectification") + + +@router.get("/", response_model=PedimentoConfigUpdateRectificationResponse) +async def get_config_update_rectification( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config update rectification by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config update rectification not found") + + return config + + +@router.post("/", response_model=PedimentoConfigUpdateRectificationResponse, status_code=201) +async def create_config_update_rectification( + pedimento_id: int, + data: PedimentoConfigUpdateRectificationCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config update rectification""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + config = PedimentoConfigUpdateRectificationService.create(db, data, tenant_id) + return config + + +@router.put("/", response_model=PedimentoConfigUpdateRectificationResponse) +async def update_config_update_rectification( + pedimento_id: int, + data: PedimentoConfigUpdateRectificationUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config update rectification""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigUpdateRectificationService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config update rectification not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_update_rectification( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config update rectification""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigUpdateRectificationService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config update rectification not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py new file mode 100644 index 00000000..5ae2c9e7 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoConfigUpdates CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_config_updates import PedimentoConfigUpdatesService +from ..dtos.pedimento_config_updates import ( + PedimentoConfigUpdatesCreate, + PedimentoConfigUpdatesUpdate, + PedimentoConfigUpdatesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/config-updates") + + +@router.get("/", response_model=PedimentoConfigUpdatesResponse) +async def get_config_updates( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get config updates by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + raise HTTPException(status_code=404, detail="Config updates not found") + + return config + + +@router.post("/", response_model=PedimentoConfigUpdatesResponse, status_code=201) +async def create_config_updates( + pedimento_id: int, + data: PedimentoConfigUpdatesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create config updates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + config = PedimentoConfigUpdatesService.create(db, data, tenant_id) + return config + + +@router.put("/", response_model=PedimentoConfigUpdatesResponse) +async def update_config_updates( + pedimento_id: int, + data: PedimentoConfigUpdatesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update config updates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + config = PedimentoConfigUpdatesService.update(db, pedimento_id, tenant_id, data) + if not config: + raise HTTPException(status_code=404, detail="Config updates not found") + + return config + + +@router.delete("/", status_code=204) +async def delete_config_updates( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete config updates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoConfigUpdatesService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Config updates not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py new file mode 100644 index 00000000..1bb61d4c --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py @@ -0,0 +1,111 @@ +""" +Routes for PedimentoCustomsOffices CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_customs_offices import PedimentoCustomsOfficesService +from ..dtos.pedimento_customs_offices import ( + PedimentoCustomsOfficesCreate, + PedimentoCustomsOfficesUpdate, + PedimentoCustomsOfficesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/customs-offices") + + +@router.get("/", response_model=List[PedimentoCustomsOfficesResponse]) +async def list_customs_offices( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all customs offices for a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + return offices + + +@router.get("/{office_id}", response_model=PedimentoCustomsOfficesResponse) +async def get_customs_office( + pedimento_id: int, + office_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a specific customs office by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + office = PedimentoCustomsOfficesService.get_by_id(db, office_id, pedimento_id, tenant_id) + if not office: + raise HTTPException(status_code=404, detail="Customs office not found") + + return office + + +@router.post("/", response_model=PedimentoCustomsOfficesResponse, status_code=201) +async def create_customs_office( + pedimento_id: int, + data: PedimentoCustomsOfficesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new customs office""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + office = PedimentoCustomsOfficesService.create(db, data, tenant_id) + return office + + +@router.put("/{office_id}", response_model=PedimentoCustomsOfficesResponse) +async def update_customs_office( + pedimento_id: int, + office_id: int, + data: PedimentoCustomsOfficesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update a customs office""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + office = PedimentoCustomsOfficesService.update(db, office_id, pedimento_id, tenant_id, data) + if not office: + raise HTTPException(status_code=404, detail="Customs office not found") + + return office + + +@router.delete("/{office_id}", status_code=204) +async def delete_customs_office( + pedimento_id: int, + office_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete a customs office""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoCustomsOfficesService.delete(db, office_id, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Customs office not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py new file mode 100644 index 00000000..ae83a91e --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoDates CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_dates import PedimentoDatesService +from ..dtos.pedimento_dates import ( + PedimentoDatesCreate, + PedimentoDatesUpdate, + PedimentoDatesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/dates") + + +@router.get("/", response_model=PedimentoDatesResponse) +async def get_dates( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get dates by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not dates: + raise HTTPException(status_code=404, detail="Pedimento dates not found") + + return dates + + +@router.post("/", response_model=PedimentoDatesResponse, status_code=201) +async def create_dates( + pedimento_id: int, + data: PedimentoDatesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create pedimento dates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + dates = PedimentoDatesService.create(db, data, tenant_id) + return dates + + +@router.put("/", response_model=PedimentoDatesResponse) +async def update_dates( + pedimento_id: int, + data: PedimentoDatesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update pedimento dates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + dates = PedimentoDatesService.update(db, pedimento_id, tenant_id, data) + if not dates: + raise HTTPException(status_code=404, detail="Pedimento dates not found") + + return dates + + +@router.delete("/", status_code=204) +async def delete_dates( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete pedimento dates""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoDatesService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Pedimento dates not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py new file mode 100644 index 00000000..22a0e868 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py @@ -0,0 +1,111 @@ +""" +Routes for PedimentoDecrementables CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_decrementables import PedimentoDecrementablesService +from ..dtos.pedimento_decrementables import ( + PedimentoDecrementablesCreate, + PedimentoDecrementablesUpdate, + PedimentoDecrementablesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/decrementables") + + +@router.get("/", response_model=List[PedimentoDecrementablesResponse]) +async def list_decrementables( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all decrementables for a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + return decrementables + + +@router.get("/{decrementable_id}", response_model=PedimentoDecrementablesResponse) +async def get_decrementable( + pedimento_id: int, + decrementable_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a specific decrementable by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + decrementable = PedimentoDecrementablesService.get_by_id(db, decrementable_id, pedimento_id, tenant_id) + if not decrementable: + raise HTTPException(status_code=404, detail="Decrementable not found") + + return decrementable + + +@router.post("/", response_model=PedimentoDecrementablesResponse, status_code=201) +async def create_decrementable( + pedimento_id: int, + data: PedimentoDecrementablesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new decrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + decrementable = PedimentoDecrementablesService.create(db, data, tenant_id) + return decrementable + + +@router.put("/{decrementable_id}", response_model=PedimentoDecrementablesResponse) +async def update_decrementable( + pedimento_id: int, + decrementable_id: int, + data: PedimentoDecrementablesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update a decrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + decrementable = PedimentoDecrementablesService.update(db, decrementable_id, pedimento_id, tenant_id, data) + if not decrementable: + raise HTTPException(status_code=404, detail="Decrementable not found") + + return decrementable + + +@router.delete("/{decrementable_id}", status_code=204) +async def delete_decrementable( + pedimento_id: int, + decrementable_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete a decrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoDecrementablesService.delete(db, decrementable_id, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Decrementable not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py new file mode 100644 index 00000000..e5cbccab --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py @@ -0,0 +1,111 @@ +""" +Routes for PedimentoIncrementables CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_incrementables import PedimentoIncrementablesService +from ..dtos.pedimento_incrementables import ( + PedimentoIncrementablesCreate, + PedimentoIncrementablesUpdate, + PedimentoIncrementablesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/incrementables") + + +@router.get("/", response_model=List[PedimentoIncrementablesResponse]) +async def list_incrementables( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all incrementables for a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + return incrementables + + +@router.get("/{incrementable_id}", response_model=PedimentoIncrementablesResponse) +async def get_incrementable( + pedimento_id: int, + incrementable_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a specific incrementable by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + incrementable = PedimentoIncrementablesService.get_by_id(db, incrementable_id, pedimento_id, tenant_id) + if not incrementable: + raise HTTPException(status_code=404, detail="Incrementable not found") + + return incrementable + + +@router.post("/", response_model=PedimentoIncrementablesResponse, status_code=201) +async def create_incrementable( + pedimento_id: int, + data: PedimentoIncrementablesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new incrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + incrementable = PedimentoIncrementablesService.create(db, data, tenant_id) + return incrementable + + +@router.put("/{incrementable_id}", response_model=PedimentoIncrementablesResponse) +async def update_incrementable( + pedimento_id: int, + incrementable_id: int, + data: PedimentoIncrementablesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update an incrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + incrementable = PedimentoIncrementablesService.update(db, incrementable_id, pedimento_id, tenant_id, data) + if not incrementable: + raise HTTPException(status_code=404, detail="Incrementable not found") + + return incrementable + + +@router.delete("/{incrementable_id}", status_code=204) +async def delete_incrementable( + pedimento_id: int, + incrementable_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete an incrementable""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoIncrementablesService.delete(db, incrementable_id, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Incrementable not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py new file mode 100644 index 00000000..31e83d8f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoIndexes CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_indexes import PedimentoIndexesService +from ..dtos.pedimento_indexes import ( + PedimentoIndexesCreate, + PedimentoIndexesUpdate, + PedimentoIndexesResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/indexes") + + +@router.get("/", response_model=PedimentoIndexesResponse) +async def get_indexes( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get indexes by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not indexes: + raise HTTPException(status_code=404, detail="Pedimento indexes not found") + + return indexes + + +@router.post("/", response_model=PedimentoIndexesResponse, status_code=201) +async def create_indexes( + pedimento_id: int, + data: PedimentoIndexesCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create pedimento indexes""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + indexes = PedimentoIndexesService.create(db, data, tenant_id) + return indexes + + +@router.put("/", response_model=PedimentoIndexesResponse) +async def update_indexes( + pedimento_id: int, + data: PedimentoIndexesUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update pedimento indexes""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + indexes = PedimentoIndexesService.update(db, pedimento_id, tenant_id, data) + if not indexes: + raise HTTPException(status_code=404, detail="Pedimento indexes not found") + + return indexes + + +@router.delete("/", status_code=204) +async def delete_indexes( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete pedimento indexes""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoIndexesService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Pedimento indexes not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py new file mode 100644 index 00000000..b1ed4eee --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py @@ -0,0 +1,111 @@ +""" +Routes for PedimentoPayments CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_payments import PedimentoPaymentsService +from ..dtos.pedimento_payments import ( + PedimentoPaymentsCreate, + PedimentoPaymentsUpdate, + PedimentoPaymentsResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/payments") + + +@router.get("/", response_model=List[PedimentoPaymentsResponse]) +async def list_payments( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all payments for a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + return payments + + +@router.get("/{payment_id}", response_model=PedimentoPaymentsResponse) +async def get_payment( + pedimento_id: int, + payment_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a specific payment by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + payment = PedimentoPaymentsService.get_by_id(db, payment_id, pedimento_id, tenant_id) + if not payment: + raise HTTPException(status_code=404, detail="Payment not found") + + return payment + + +@router.post("/", response_model=PedimentoPaymentsResponse, status_code=201) +async def create_payment( + pedimento_id: int, + data: PedimentoPaymentsCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new payment""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + payment = PedimentoPaymentsService.create(db, data, tenant_id) + return payment + + +@router.put("/{payment_id}", response_model=PedimentoPaymentsResponse) +async def update_payment( + pedimento_id: int, + payment_id: int, + data: PedimentoPaymentsUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update a payment""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + payment = PedimentoPaymentsService.update(db, payment_id, pedimento_id, tenant_id, data) + if not payment: + raise HTTPException(status_code=404, detail="Payment not found") + + return payment + + +@router.delete("/{payment_id}", status_code=204) +async def delete_payment( + pedimento_id: int, + payment_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete a payment""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoPaymentsService.delete(db, payment_id, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Payment not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py new file mode 100644 index 00000000..d05fbd30 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoRectificationDestination CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_rectification_destination import PedimentoRectificationDestinationService +from ..dtos.pedimento_rectification_destination import ( + PedimentoRectificationDestinationCreate, + PedimentoRectificationDestinationUpdate, + PedimentoRectificationDestinationResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/rectification-destination") + + +@router.get("/", response_model=PedimentoRectificationDestinationResponse) +async def get_rectification_destination( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get rectification destination by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + raise HTTPException(status_code=404, detail="Rectification destination not found") + + return rectification + + +@router.post("/", response_model=PedimentoRectificationDestinationResponse, status_code=201) +async def create_rectification_destination( + pedimento_id: int, + data: PedimentoRectificationDestinationCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create rectification destination""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + rectification = PedimentoRectificationDestinationService.create(db, data, tenant_id) + return rectification + + +@router.put("/", response_model=PedimentoRectificationDestinationResponse) +async def update_rectification_destination( + pedimento_id: int, + data: PedimentoRectificationDestinationUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update rectification destination""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + rectification = PedimentoRectificationDestinationService.update(db, pedimento_id, tenant_id, data) + if not rectification: + raise HTTPException(status_code=404, detail="Rectification destination not found") + + return rectification + + +@router.delete("/", status_code=204) +async def delete_rectification_destination( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete rectification destination""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoRectificationDestinationService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Rectification destination not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py new file mode 100644 index 00000000..1521f371 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoRectificationOrigin CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_rectification_origin import PedimentoRectificationOriginService +from ..dtos.pedimento_rectification_origin import ( + PedimentoRectificationOriginCreate, + PedimentoRectificationOriginUpdate, + PedimentoRectificationOriginResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/rectification-origin") + + +@router.get("/", response_model=PedimentoRectificationOriginResponse) +async def get_rectification_origin( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get rectification origin by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + raise HTTPException(status_code=404, detail="Rectification origin not found") + + return rectification + + +@router.post("/", response_model=PedimentoRectificationOriginResponse, status_code=201) +async def create_rectification_origin( + pedimento_id: int, + data: PedimentoRectificationOriginCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create rectification origin""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + rectification = PedimentoRectificationOriginService.create(db, data, tenant_id) + return rectification + + +@router.put("/", response_model=PedimentoRectificationOriginResponse) +async def update_rectification_origin( + pedimento_id: int, + data: PedimentoRectificationOriginUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update rectification origin""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + rectification = PedimentoRectificationOriginService.update(db, pedimento_id, tenant_id, data) + if not rectification: + raise HTTPException(status_code=404, detail="Rectification origin not found") + + return rectification + + +@router.delete("/", status_code=204) +async def delete_rectification_origin( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete rectification origin""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoRectificationOriginService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Rectification origin not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py new file mode 100644 index 00000000..8365ad2f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py @@ -0,0 +1,111 @@ +""" +Routes for PedimentoTransportMeans CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_transport_means import PedimentoTransportMeansService +from ..dtos.pedimento_transport_means import ( + PedimentoTransportMeansCreate, + PedimentoTransportMeansUpdate, + PedimentoTransportMeansResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/transport-means") + + +@router.get("/", response_model=List[PedimentoTransportMeansResponse]) +async def list_transport_means( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all transport means for a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + transport_means = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id) + return transport_means + + +@router.get("/{transport_mean_id}", response_model=PedimentoTransportMeansResponse) +async def get_transport_mean( + pedimento_id: int, + transport_mean_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a specific transport mean by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + transport_mean = PedimentoTransportMeansService.get_by_id(db, transport_mean_id, pedimento_id, tenant_id) + if not transport_mean: + raise HTTPException(status_code=404, detail="Transport mean not found") + + return transport_mean + + +@router.post("/", response_model=PedimentoTransportMeansResponse, status_code=201) +async def create_transport_mean( + pedimento_id: int, + data: PedimentoTransportMeansCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new transport mean""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + transport_mean = PedimentoTransportMeansService.create(db, data, tenant_id) + return transport_mean + + +@router.put("/{transport_mean_id}", response_model=PedimentoTransportMeansResponse) +async def update_transport_mean( + pedimento_id: int, + transport_mean_id: int, + data: PedimentoTransportMeansUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update a transport mean""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + transport_mean = PedimentoTransportMeansService.update(db, transport_mean_id, pedimento_id, tenant_id, data) + if not transport_mean: + raise HTTPException(status_code=404, detail="Transport mean not found") + + return transport_mean + + +@router.delete("/{transport_mean_id}", status_code=204) +async def delete_transport_mean( + pedimento_id: int, + transport_mean_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete a transport mean""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoTransportMeansService.delete(db, transport_mean_id, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Transport mean not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py new file mode 100644 index 00000000..66164a31 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py @@ -0,0 +1,92 @@ +""" +Routes for PedimentoValidation CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimento_validation import PedimentoValidationService +from ..dtos.pedimento_validation import ( + PedimentoValidationCreate, + PedimentoValidationUpdate, + PedimentoValidationResponse +) + + +router = APIRouter(prefix="/{pedimento_id}/validation") + + +@router.get("/", response_model=PedimentoValidationResponse) +async def get_validation( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get validation by pedimento ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not validation: + raise HTTPException(status_code=404, detail="Pedimento validation not found") + + return validation + + +@router.post("/", response_model=PedimentoValidationResponse, status_code=201) +async def create_validation( + pedimento_id: int, + data: PedimentoValidationCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create pedimento validation""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + # Ensure pedimento_id matches + if data.pedimento_id != pedimento_id: + raise HTTPException(status_code=400, detail="Pedimento ID mismatch") + + validation = PedimentoValidationService.create(db, data, tenant_id) + return validation + + +@router.put("/", response_model=PedimentoValidationResponse) +async def update_validation( + pedimento_id: int, + data: PedimentoValidationUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update pedimento validation""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + validation = PedimentoValidationService.update(db, pedimento_id, tenant_id, data) + if not validation: + raise HTTPException(status_code=404, detail="Pedimento validation not found") + + return validation + + +@router.delete("/", status_code=204) +async def delete_validation( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete pedimento validation""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentoValidationService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Pedimento validation not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py new file mode 100644 index 00000000..2dd457ef --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py @@ -0,0 +1,118 @@ +""" +Routes for Pedimentos CRUD operations +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import Dict, Any, Optional +from core.database import get_core_db +from core.security import get_current_user, get_tenant_from_token + +from ..services.pedimentos import PedimentosService +from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate, PedimentosResponse + + +router = APIRouter() + + +@router.get("/", response_model=Dict[str, Any]) +async def list_pedimentos( + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(50, ge=1, le=100, description="Page size"), + status: Optional[str] = Query(None, description="Filter by status"), + client_id: Optional[int] = Query(None, description="Filter by client ID"), + year: Optional[str] = Query(None, description="Filter by year"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get all pedimentos with pagination and filters""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + filters = {} + if status: + filters["status"] = status + if client_id: + filters["client_id"] = client_id + if year: + filters["year"] = year + + skip = (page - 1) * page_size + items, total = PedimentosService.get_all(db, tenant_id, skip, page_size, filters) + + return { + "items": [PedimentosResponse.model_validate(item) for item in items], + "total": total, + "page": page, + "page_size": page_size + } + + +@router.get("/{pedimento_id}", response_model=PedimentosResponse) +async def get_pedimento( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Get a pedimento by ID""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id) + if not pedimento: + raise HTTPException(status_code=404, detail="Pedimento not found") + + return pedimento + + +@router.post("/", response_model=PedimentosResponse, status_code=201) +async def create_pedimento( + data: PedimentosCreate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Create a new pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + pedimento = PedimentosService.create(db, data, tenant_id) + return pedimento + + +@router.put("/{pedimento_id}", response_model=PedimentosResponse) +async def update_pedimento( + pedimento_id: int, + data: PedimentosUpdate, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Update a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + pedimento = PedimentosService.update(db, pedimento_id, tenant_id, data) + if not pedimento: + raise HTTPException(status_code=404, detail="Pedimento not found") + + return pedimento + + +@router.delete("/{pedimento_id}", status_code=204) +async def delete_pedimento( + pedimento_id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """Delete a pedimento""" + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + + success = PedimentosService.delete(db, pedimento_id, tenant_id) + if not success: + raise HTTPException(status_code=404, detail="Pedimento not found") + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py new file mode 100644 index 00000000..40762a99 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigAdditional CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_additional import PedimentoConfigAdditional +from ..dtos.pedimento_config_additional import PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalUpdate + + +class PedimentoConfigAdditionalService: + """Service class for PedimentoConfigAdditional business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigAdditional]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigAdditional).filter( + PedimentoConfigAdditional.pedimento_id == pedimento_id, + PedimentoConfigAdditional.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigAdditionalCreate) -> PedimentoConfigAdditional: + """Create a new config""" + config = PedimentoConfigAdditional(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigAdditionalUpdate + ) -> Optional[PedimentoConfigAdditional]: + """Update config""" + config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py new file mode 100644 index 00000000..7919bdab --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigCalculations CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_calculations import PedimentoConfigCalculations +from ..dtos.pedimento_config_calculations import PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsUpdate + + +class PedimentoConfigCalculationsService: + """Service class for PedimentoConfigCalculations business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigCalculations]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigCalculations).filter( + PedimentoConfigCalculations.pedimento_id == pedimento_id, + PedimentoConfigCalculations.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigCalculationsCreate) -> PedimentoConfigCalculations: + """Create a new config""" + config = PedimentoConfigCalculations(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigCalculationsUpdate + ) -> Optional[PedimentoConfigCalculations]: + """Update config""" + config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py new file mode 100644 index 00000000..e3ed7f3c --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigParameters CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_parameters import PedimentoConfigParameters +from ..dtos.pedimento_config_parameters import PedimentoConfigParametersCreate, PedimentoConfigParametersUpdate + + +class PedimentoConfigParametersService: + """Service class for PedimentoConfigParameters business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigParameters]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigParameters).filter( + PedimentoConfigParameters.pedimento_id == pedimento_id, + PedimentoConfigParameters.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigParametersCreate) -> PedimentoConfigParameters: + """Create a new config""" + config = PedimentoConfigParameters(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigParametersUpdate + ) -> Optional[PedimentoConfigParameters]: + """Update config""" + config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py new file mode 100644 index 00000000..af7b9283 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigSurcharges CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges +from ..dtos.pedimento_config_surcharges import PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesUpdate + + +class PedimentoConfigSurchargesService: + """Service class for PedimentoConfigSurcharges business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigSurcharges]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigSurcharges).filter( + PedimentoConfigSurcharges.pedimento_id == pedimento_id, + PedimentoConfigSurcharges.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigSurchargesCreate) -> PedimentoConfigSurcharges: + """Create a new config""" + config = PedimentoConfigSurcharges(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigSurchargesUpdate + ) -> Optional[PedimentoConfigSurcharges]: + """Update config""" + config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py new file mode 100644 index 00000000..a9566f49 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigUpdateRectification CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_update_rectification import PedimentoConfigUpdateRectification +from ..dtos.pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationUpdate + + +class PedimentoConfigUpdateRectificationService: + """Service class for PedimentoConfigUpdateRectification business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigUpdateRectification]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigUpdateRectification).filter( + PedimentoConfigUpdateRectification.pedimento_id == pedimento_id, + PedimentoConfigUpdateRectification.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigUpdateRectificationCreate) -> PedimentoConfigUpdateRectification: + """Create a new config""" + config = PedimentoConfigUpdateRectification(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigUpdateRectificationUpdate + ) -> Optional[PedimentoConfigUpdateRectification]: + """Update config""" + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py new file mode 100644 index 00000000..bf8a4b97 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoConfigUpdates CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_config_updates import PedimentoConfigUpdates +from ..dtos.pedimento_config_updates import PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesUpdate + + +class PedimentoConfigUpdatesService: + """Service class for PedimentoConfigUpdates business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigUpdates]: + """Get config by pedimento ID""" + return db.query(PedimentoConfigUpdates).filter( + PedimentoConfigUpdates.pedimento_id == pedimento_id, + PedimentoConfigUpdates.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, config_data: PedimentoConfigUpdatesCreate) -> PedimentoConfigUpdates: + """Create a new config""" + config = PedimentoConfigUpdates(**config_data.model_dump()) + db.add(config) + db.commit() + db.refresh(config) + return config + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + config_data: PedimentoConfigUpdatesUpdate + ) -> Optional[PedimentoConfigUpdates]: + """Update config""" + config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return None + + update_data = config_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + db.commit() + db.refresh(config) + return config + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete config""" + config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not config: + return False + + db.delete(config) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py new file mode 100644 index 00000000..521cb5ce --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoCustomsOffices CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_customs_offices import PedimentoCustomsOffices +from ..dtos.pedimento_customs_offices import PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesUpdate + + +class PedimentoCustomsOfficesService: + """Service class for PedimentoCustomsOffices business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoCustomsOffices]: + """Get customs offices by pedimento ID""" + return db.query(PedimentoCustomsOffices).filter( + PedimentoCustomsOffices.pedimento_id == pedimento_id, + PedimentoCustomsOffices.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoCustomsOfficesCreate) -> PedimentoCustomsOffices: + """Create new customs offices""" + offices = PedimentoCustomsOffices(**data.model_dump()) + db.add(offices) + db.commit() + db.refresh(offices) + return offices + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoCustomsOfficesUpdate + ) -> Optional[PedimentoCustomsOffices]: + """Update customs offices""" + offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not offices: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(offices, field, value) + + db.commit() + db.refresh(offices) + return offices + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete customs offices""" + offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not offices: + return False + + db.delete(offices) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py new file mode 100644 index 00000000..84708efd --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoDates CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_dates import PedimentoDates +from ..dtos.pedimento_dates import PedimentoDatesCreate, PedimentoDatesUpdate + + +class PedimentoDatesService: + """Service class for PedimentoDates business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoDates]: + """Get dates by pedimento ID""" + return db.query(PedimentoDates).filter( + PedimentoDates.pedimento_id == pedimento_id, + PedimentoDates.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoDatesCreate) -> PedimentoDates: + """Create new pedimento dates""" + dates = PedimentoDates(**data.model_dump()) + db.add(dates) + db.commit() + db.refresh(dates) + return dates + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoDatesUpdate + ) -> Optional[PedimentoDates]: + """Update pedimento dates""" + dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not dates: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(dates, field, value) + + db.commit() + db.refresh(dates) + return dates + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete pedimento dates""" + dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not dates: + return False + + db.delete(dates) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py new file mode 100644 index 00000000..f9eb2893 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoDecrementables CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_decrementables import PedimentoDecrementables +from ..dtos.pedimento_decrementables import PedimentoDecrementablesCreate, PedimentoDecrementablesUpdate + + +class PedimentoDecrementablesService: + """Service class for PedimentoDecrementables business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoDecrementables]: + """Get decrementables by pedimento ID""" + return db.query(PedimentoDecrementables).filter( + PedimentoDecrementables.pedimento_id == pedimento_id, + PedimentoDecrementables.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoDecrementablesCreate) -> PedimentoDecrementables: + """Create new decrementables""" + decrementables = PedimentoDecrementables(**data.model_dump()) + db.add(decrementables) + db.commit() + db.refresh(decrementables) + return decrementables + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoDecrementablesUpdate + ) -> Optional[PedimentoDecrementables]: + """Update decrementables""" + decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not decrementables: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(decrementables, field, value) + + db.commit() + db.refresh(decrementables) + return decrementables + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete decrementables""" + decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not decrementables: + return False + + db.delete(decrementables) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py new file mode 100644 index 00000000..40bd6c81 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoIncrementables CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_incrementables import PedimentoIncrementables +from ..dtos.pedimento_incrementables import PedimentoIncrementablesCreate, PedimentoIncrementablesUpdate + + +class PedimentoIncrementablesService: + """Service class for PedimentoIncrementables business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoIncrementables]: + """Get incrementables by pedimento ID""" + return db.query(PedimentoIncrementables).filter( + PedimentoIncrementables.pedimento_id == pedimento_id, + PedimentoIncrementables.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoIncrementablesCreate) -> PedimentoIncrementables: + """Create new incrementables""" + incrementables = PedimentoIncrementables(**data.model_dump()) + db.add(incrementables) + db.commit() + db.refresh(incrementables) + return incrementables + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoIncrementablesUpdate + ) -> Optional[PedimentoIncrementables]: + """Update incrementables""" + incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not incrementables: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(incrementables, field, value) + + db.commit() + db.refresh(incrementables) + return incrementables + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete incrementables""" + incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not incrementables: + return False + + db.delete(incrementables) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py new file mode 100644 index 00000000..dbd8fdd5 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoIndexes CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_indexes import PedimentoIndexes +from ..dtos.pedimento_indexes import PedimentoIndexesCreate, PedimentoIndexesUpdate + + +class PedimentoIndexesService: + """Service class for PedimentoIndexes business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoIndexes]: + """Get indexes by pedimento ID""" + return db.query(PedimentoIndexes).filter( + PedimentoIndexes.pedimento_id == pedimento_id, + PedimentoIndexes.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoIndexesCreate) -> PedimentoIndexes: + """Create new indexes""" + indexes = PedimentoIndexes(**data.model_dump()) + db.add(indexes) + db.commit() + db.refresh(indexes) + return indexes + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoIndexesUpdate + ) -> Optional[PedimentoIndexes]: + """Update indexes""" + indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not indexes: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(indexes, field, value) + + db.commit() + db.refresh(indexes) + return indexes + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete indexes""" + indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not indexes: + return False + + db.delete(indexes) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py new file mode 100644 index 00000000..81dc5af1 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoPayments CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_payments import PedimentoPayments +from ..dtos.pedimento_payments import PedimentoPaymentsCreate, PedimentoPaymentsUpdate + + +class PedimentoPaymentsService: + """Service class for PedimentoPayments business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoPayments]: + """Get payments by pedimento ID""" + return db.query(PedimentoPayments).filter( + PedimentoPayments.pedimento_id == pedimento_id, + PedimentoPayments.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoPaymentsCreate) -> PedimentoPayments: + """Create new payments""" + payments = PedimentoPayments(**data.model_dump()) + db.add(payments) + db.commit() + db.refresh(payments) + return payments + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoPaymentsUpdate + ) -> Optional[PedimentoPayments]: + """Update payments""" + payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not payments: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(payments, field, value) + + db.commit() + db.refresh(payments) + return payments + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete payments""" + payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not payments: + return False + + db.delete(payments) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py new file mode 100644 index 00000000..19dc8715 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoRectificationDestination CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_rectification_destination import PedimentoRectificationDestination +from ..dtos.pedimento_rectification_destination import PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationUpdate + + +class PedimentoRectificationDestinationService: + """Service class for PedimentoRectificationDestination business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoRectificationDestination]: + """Get rectification destination by pedimento ID""" + return db.query(PedimentoRectificationDestination).filter( + PedimentoRectificationDestination.pedimento_id == pedimento_id, + PedimentoRectificationDestination.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoRectificationDestinationCreate) -> PedimentoRectificationDestination: + """Create new rectification destination""" + rectification = PedimentoRectificationDestination(**data.model_dump()) + db.add(rectification) + db.commit() + db.refresh(rectification) + return rectification + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoRectificationDestinationUpdate + ) -> Optional[PedimentoRectificationDestination]: + """Update rectification destination""" + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(rectification, field, value) + + db.commit() + db.refresh(rectification) + return rectification + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete rectification destination""" + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + return False + + db.delete(rectification) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py new file mode 100644 index 00000000..1206b137 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoRectificationOrigin CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_rectification_origin import PedimentoRectificationOrigin +from ..dtos.pedimento_rectification_origin import PedimentoRectificationOriginCreate, PedimentoRectificationOriginUpdate + + +class PedimentoRectificationOriginService: + """Service class for PedimentoRectificationOrigin business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoRectificationOrigin]: + """Get rectification origin by pedimento ID""" + return db.query(PedimentoRectificationOrigin).filter( + PedimentoRectificationOrigin.pedimento_id == pedimento_id, + PedimentoRectificationOrigin.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoRectificationOriginCreate) -> PedimentoRectificationOrigin: + """Create new rectification origin""" + rectification = PedimentoRectificationOrigin(**data.model_dump()) + db.add(rectification) + db.commit() + db.refresh(rectification) + return rectification + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoRectificationOriginUpdate + ) -> Optional[PedimentoRectificationOrigin]: + """Update rectification origin""" + rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(rectification, field, value) + + db.commit() + db.refresh(rectification) + return rectification + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete rectification origin""" + rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not rectification: + return False + + db.delete(rectification) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py new file mode 100644 index 00000000..8384523f --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoTransportMeans CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_transport_means import PedimentoTransportMeans +from ..dtos.pedimento_transport_means import PedimentoTransportMeansCreate, PedimentoTransportMeansUpdate + + +class PedimentoTransportMeansService: + """Service class for PedimentoTransportMeans business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoTransportMeans]: + """Get transport means by pedimento ID""" + return db.query(PedimentoTransportMeans).filter( + PedimentoTransportMeans.pedimento_id == pedimento_id, + PedimentoTransportMeans.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoTransportMeansCreate) -> PedimentoTransportMeans: + """Create new transport means""" + transport = PedimentoTransportMeans(**data.model_dump()) + db.add(transport) + db.commit() + db.refresh(transport) + return transport + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoTransportMeansUpdate + ) -> Optional[PedimentoTransportMeans]: + """Update transport means""" + transport = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not transport: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(transport, field, value) + + db.commit() + db.refresh(transport) + return transport + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete transport means""" + transport = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not transport: + return False + + db.delete(transport) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py new file mode 100644 index 00000000..8db22ede --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py @@ -0,0 +1,60 @@ +""" +Service layer for PedimentoValidation CRUD operations +""" +from typing import Optional +from sqlalchemy.orm import Session + +from ..models.pedimento_validation import PedimentoValidation +from ..dtos.pedimento_validation import PedimentoValidationCreate, PedimentoValidationUpdate + + +class PedimentoValidationService: + """Service class for PedimentoValidation business logic""" + + @staticmethod + def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoValidation]: + """Get validation by pedimento ID""" + return db.query(PedimentoValidation).filter( + PedimentoValidation.pedimento_id == pedimento_id, + PedimentoValidation.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, data: PedimentoValidationCreate) -> PedimentoValidation: + """Create new validation""" + validation = PedimentoValidation(**data.model_dump()) + db.add(validation) + db.commit() + db.refresh(validation) + return validation + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + data: PedimentoValidationUpdate + ) -> Optional[PedimentoValidation]: + """Update validation""" + validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not validation: + return None + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(validation, field, value) + + db.commit() + db.refresh(validation) + return validation + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """Delete validation""" + validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + if not validation: + return False + + db.delete(validation) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py new file mode 100644 index 00000000..e22717c2 --- /dev/null +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py @@ -0,0 +1,140 @@ +""" +Service layer for Pedimentos CRUD operations +""" +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import desc +from fastapi import HTTPException + +from ..models.pedimentos import Pedimentos +from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate + + +class PedimentosService: + """Service class for Pedimentos business logic""" + + @staticmethod + def get_all( + db: Session, + tenant_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None + ) -> tuple[List[Pedimentos], int]: + """ + Get all pedimentos for a tenant with pagination and filters + + Args: + db: Database session + tenant_id: Tenant ID + skip: Number of records to skip + limit: Maximum number of records to return + filters: Optional filters dict + + Returns: + Tuple of (list of pedimentos, total count) + """ + query = db.query(Pedimentos).filter(Pedimentos.tenant_id == tenant_id) + + if filters: + if filters.get("status"): + query = query.filter(Pedimentos.status == filters["status"]) + if filters.get("client_id"): + query = query.filter(Pedimentos.client_id == filters["client_id"]) + if filters.get("year"): + query = query.filter(Pedimentos.year == filters["year"]) + + total = query.count() + items = query.order_by(desc(Pedimentos.created_at)).offset(skip).limit(limit).all() + + return items, total + + @staticmethod + def get_by_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[Pedimentos]: + """ + Get a pedimento by ID + + Args: + db: Database session + pedimento_id: Pedimento ID + tenant_id: Tenant ID + + Returns: + Pedimento or None if not found + """ + return db.query(Pedimentos).filter( + Pedimentos.id == pedimento_id, + Pedimentos.tenant_id == tenant_id + ).first() + + @staticmethod + def create(db: Session, pedimento_data: PedimentosCreate, tenant_id: int) -> Pedimentos: + """ + Create a new pedimento + + Args: + db: Database session + pedimento_data: Pedimento creation data + + Returns: + Created pedimento + """ + pedimento = Pedimentos(**pedimento_data.model_dump()) + pedimento.tenant_id = 1 + + db.add(pedimento) + db.commit() + db.refresh(pedimento) + return pedimento + + @staticmethod + def update( + db: Session, + pedimento_id: int, + tenant_id: int, + pedimento_data: PedimentosUpdate + ) -> Optional[Pedimentos]: + """ + Update a pedimento + + Args: + db: Database session + pedimento_id: Pedimento ID + tenant_id: Tenant ID + pedimento_data: Updated data + + Returns: + Updated pedimento or None if not found + """ + pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id) + if not pedimento: + return None + + update_data = pedimento_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(pedimento, field, value) + + db.commit() + db.refresh(pedimento) + return pedimento + + @staticmethod + def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: + """ + Delete a pedimento + + Args: + db: Database session + pedimento_id: Pedimento ID + tenant_id: Tenant ID + + Returns: + True if deleted, False if not found + """ + pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id) + if not pedimento: + return False + + db.delete(pedimento) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py new file mode 100644 index 00000000..06ebeac3 --- /dev/null +++ b/backend/api/v1/modules/a76/router.py @@ -0,0 +1,20 @@ +""" +Router principal de API v1 +Agrega todos los módulos de la aplicación +""" +from fastapi import APIRouter + +# Importar routers de módulos +from .auth import router as auth_router +from .tenants import router as tenants_router +from .licenses import router as licenses_router +from .pedmientos.router import router as pedimentos_router + +# Router principal +router = APIRouter() + +# Registrar módulos +router.include_router(auth_router) +router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"]) +router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"]) +router.include_router(pedimentos_router, prefix="/a76") diff --git a/backend/api/v1/modules/a76/tenants/routes.py b/backend/api/v1/modules/a76/tenants/routes.py index da18099f..2768c025 100644 --- a/backend/api/v1/modules/a76/tenants/routes.py +++ b/backend/api/v1/modules/a76/tenants/routes.py @@ -10,7 +10,7 @@ from core.security import get_current_user, has_role from .dto import TenantCreateDTO, TenantUpdateDTO, TenantResponseDTO, TenantListResponseDTO from .service import TenantService -router = APIRouter(prefix="/tenants", tags=["Tenants"]) +router = APIRouter(prefix="/tenants") @router.post("/", response_model=TenantResponseDTO, status_code=201) diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py index 55672bfd..b0db57d2 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py @@ -8,7 +8,7 @@ from .dto import CodePedimentoRegimenDTO from typing import Any, Dict -router = APIRouter(prefix="/code-pedimento-regimens", tags=["Code Pedimento Regimens"]) +router = APIRouter(prefix="/code-pedimento-regimens") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/containers/routes.py b/backend/api/v1/modules/public/reference_data/containers/routes.py index 83a52e1a..73265a2a 100644 --- a/backend/api/v1/modules/public/reference_data/containers/routes.py +++ b/backend/api/v1/modules/public/reference_data/containers/routes.py @@ -8,7 +8,7 @@ from .dto import ContainerDTO from typing import Any, Dict -router = APIRouter(prefix="/containers", tags=["Containers"]) +router = APIRouter(prefix="/containers") diff --git a/backend/api/v1/modules/public/reference_data/countries/routes.py b/backend/api/v1/modules/public/reference_data/countries/routes.py index 950140f3..c2271122 100644 --- a/backend/api/v1/modules/public/reference_data/countries/routes.py +++ b/backend/api/v1/modules/public/reference_data/countries/routes.py @@ -8,7 +8,7 @@ from .dto import CountryDTO from typing import Any, Dict -router = APIRouter(prefix="/countries", tags=["Countries"]) +router = APIRouter(prefix="/countries") diff --git a/backend/api/v1/modules/public/reference_data/currency_types/routes.py b/backend/api/v1/modules/public/reference_data/currency_types/routes.py index 5987808f..a878a7a8 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/routes.py @@ -8,7 +8,7 @@ from .dto import CurrencyTypeDTO from typing import Any, Dict -router = APIRouter(prefix="/currency-types", tags=["Currency Types"]) +router = APIRouter(prefix="/currency-types") diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py index 70225245..fdf636a6 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py @@ -8,7 +8,7 @@ from .dto import CustomsSectionDTO from typing import Any, Dict -router = APIRouter(prefix="/customs-sections", tags=["Customs Sections"]) +router = APIRouter(prefix="/customs-sections") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py index 9c0ca9f0..8f25ac54 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py @@ -8,7 +8,7 @@ from .dto import CustomsWarehouseDTO from typing import Any, Dict -router = APIRouter(prefix="/customs-warehouses", tags=["Customs Warehouses"]) +router = APIRouter(prefix="/customs-warehouses") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/routes.py b/backend/api/v1/modules/public/reference_data/incoterms/routes.py index 3bc6ea7b..894a1833 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/routes.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/routes.py @@ -8,7 +8,7 @@ from .dto import IncotermDTO from typing import Any, Dict -router = APIRouter(prefix="/incoterms", tags=["Incoterms"]) +router = APIRouter(prefix="/incoterms") diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py index eddcf8e3..9cd997a1 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py @@ -8,7 +8,7 @@ from .dto import InvoiceTypeDTO from typing import Any, Dict -router = APIRouter(prefix="/invoice-types", tags=["Invoice Types"]) +router = APIRouter(prefix="/invoice-types") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/material_types/routes.py b/backend/api/v1/modules/public/reference_data/material_types/routes.py index da83e152..dab0b4ec 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/material_types/routes.py @@ -8,7 +8,7 @@ from .dto import MaterialTypeDTO from typing import Any, Dict -router = APIRouter(prefix="/material-types", tags=["Material Types"]) +router = APIRouter(prefix="/material-types") diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py index 104de6b8..ba27a187 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py @@ -8,7 +8,7 @@ from .dto import PaymentMethodDTO from typing import Any, Dict -router = APIRouter(prefix="/payment-methods", tags=["Payment Methods"]) +router = APIRouter(prefix="/payment-methods") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py index d00ac957..34097e0b 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py @@ -8,7 +8,7 @@ from .dto import PedimentoCodeDTO from typing import Any, Dict -router = APIRouter(prefix="/pedimento-codes", tags=["Pedimento Codes"]) +router = APIRouter(prefix="/pedimento-codes") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py index a3cb398a..593d7a53 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py @@ -8,7 +8,7 @@ from .dto import RegimenPedimentoDTO from typing import Any, Dict -router = APIRouter(prefix="/pedimento-regimens", tags=["Pedimento Regimens"]) +router = APIRouter(prefix="/pedimento-regimens") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/router.py b/backend/api/v1/modules/public/reference_data/router.py new file mode 100644 index 00000000..e8a350a4 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/router.py @@ -0,0 +1,45 @@ +""" +Router principal de API v1 +Agrega todos los módulos de la aplicación +""" +from fastapi import APIRouter + +from .pedimento_codes.routes import router as pedimento_codes_router +from .payment_methods.routes import router as payment_methods_router +from .containers.routes import router as containers_router +from .countries.routes import router as countries_router +from .material_types.routes import router as material_types_router +from .currency_types.routes import router as currency_types_router +from .states.routes import router as states_router +from .transport_types.routes import router as transport_types_router +from .customs_warehouses.routes import router as customs_warehouses_router +from .valuation_methods.routes import router as valuation_methods_router +from .sectors.routes import router as sectors_router +from .transport_modes.routes import router as transport_modes_router +from .customs_sections.routes import router as customs_sections_router +from .invoice_types.routes import router as invoice_types_router +from .code_pedimento_regimens.routes import router as code_pedimento_regimens_router +from .pedimento_regimens.routes import router as pedimento_regimens_router +from .incoterms.routes import router as incoterms_router + +# Router principal +router = APIRouter() + +# Registrar módulos +router.include_router(pedimento_codes_router, prefix="/refrence_data", tags=["public / refrence_data / pedimento_codes"]) +router.include_router(payment_methods_router, prefix="/refrence_data", tags=["public / refrence_data / payment_methods"]) +router.include_router(containers_router, prefix="/refrence_data", tags=["public / refrence_data / containers"]) +router.include_router(countries_router, prefix="/refrence_data", tags=["public / refrence_data / countries"]) +router.include_router(material_types_router, prefix="/refrence_data", tags=["public / refrence_data / material_types"]) +router.include_router(currency_types_router, prefix="/refrence_data", tags=["public / refrence_data / currency_types"]) +router.include_router(states_router, prefix="/refrence_data", tags=["public / refrence_data / states"]) +router.include_router(transport_types_router, prefix="/refrence_data", tags=["public / refrence_data / transport_types"]) +router.include_router(customs_warehouses_router, prefix="/refrence_data", tags=["public / refrence_data / customs_warehouses"]) +router.include_router(valuation_methods_router, prefix="/refrence_data", tags=["public / refrence_data / valuation_methods"]) +router.include_router(sectors_router, prefix="/refrence_data", tags=["public / public / refrence_data / sectors"]) +router.include_router(transport_modes_router, prefix="/refrence_data", tags=["public / refrence_data / transport_modes"]) +router.include_router(customs_sections_router, prefix="/refrence_data", tags=["public / refrence_data / customs_sections"]) +router.include_router(invoice_types_router, prefix="/refrence_data", tags=["public / refrence_data / invoice_types"]) +router.include_router(code_pedimento_regimens_router, prefix="/refrence_data", tags=["public / refrence_data / code_pedimento_regimens"]) +router.include_router(pedimento_regimens_router, prefix="/refrence_data", tags=["public / refrence_data / pedimento_regimens"]) +router.include_router(incoterms_router, prefix="/refrence_data", tags=["public / refrence_data / incoterms"]) \ No newline at end of file diff --git a/backend/api/v1/modules/public/reference_data/sectors/routes.py b/backend/api/v1/modules/public/reference_data/sectors/routes.py index dd4b5694..9ba896c6 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/routes.py +++ b/backend/api/v1/modules/public/reference_data/sectors/routes.py @@ -8,7 +8,7 @@ from .dto import SectorDTO from typing import Any, Dict -router = APIRouter(prefix="/sectors", tags=["Sectors"]) +router = APIRouter(prefix="/sectors") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/states/routes.py b/backend/api/v1/modules/public/reference_data/states/routes.py index e63a8fd0..e9cf3cf4 100644 --- a/backend/api/v1/modules/public/reference_data/states/routes.py +++ b/backend/api/v1/modules/public/reference_data/states/routes.py @@ -8,7 +8,7 @@ from .dto import StateDTO from typing import Any, Dict -router = APIRouter(prefix="/states", tags=["States"]) +router = APIRouter(prefix="/states") diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py index 883b8763..24d0406e 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py @@ -8,7 +8,7 @@ from .dto import TransportModeDTO from typing import Any, Dict -router = APIRouter(prefix="/transport-modes", tags=["Transport Modes"]) +router = APIRouter(prefix="/transport-modes") diff --git a/backend/api/v1/modules/public/reference_data/transport_types/routes.py b/backend/api/v1/modules/public/reference_data/transport_types/routes.py index fbcfd42f..737416a7 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/routes.py @@ -8,7 +8,7 @@ from .dto import TransportTypeDTO from typing import Any, Dict -router = APIRouter(prefix="/transport-types", tags=["Transport Types"]) +router = APIRouter(prefix="/transport-types") @router.get("/", response_model=Dict[str, Any]) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py index b4971d32..2296a7fc 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py @@ -8,7 +8,7 @@ from .dto import ValuationMethodDTO from typing import Any, Dict -router = APIRouter(prefix="/valuation-methods", tags=["Valuation Methods"]) +router = APIRouter(prefix="/valuation-methods") diff --git a/backend/api/v1/modules/public/router.py b/backend/api/v1/modules/public/router.py new file mode 100644 index 00000000..bcaec4b1 --- /dev/null +++ b/backend/api/v1/modules/public/router.py @@ -0,0 +1,13 @@ +""" +Router principal de API v1 +Agrega todos los módulos de la aplicación +""" +from fastapi import APIRouter + +from .reference_data.router import router as reference_data_router + +# Router principal +router = APIRouter() + +# Registrar módulos +router.include_router(reference_data_router, prefix="/public") diff --git a/backend/api/v1/router.py b/backend/api/v1/router.py index ffb93465..5c80eefe 100644 --- a/backend/api/v1/router.py +++ b/backend/api/v1/router.py @@ -5,56 +5,17 @@ Agrega todos los módulos de la aplicación from fastapi import APIRouter # Importar routers de módulos -from .modules.a76.auth import router as auth_router -from .modules.a76.tenants import router as tenants_router - -from .modules.public.reference_data.pedimento_codes.routes import router as pedimento_codes_router -from .modules.public.reference_data.payment_methods.routes import router as payment_methods_router -from .modules.public.reference_data.containers.routes import router as containers_router -from .modules.public.reference_data.countries.routes import router as countries_router -from .modules.public.reference_data.material_types.routes import router as material_types_router -from .modules.public.reference_data.currency_types.routes import router as currency_types_router -from .modules.public.reference_data.states.routes import router as states_router -from .modules.public.reference_data.transport_types.routes import router as transport_types_router -from .modules.public.reference_data.customs_warehouses.routes import router as customs_warehouses_router -from .modules.public.reference_data.valuation_methods.routes import router as valuation_methods_router -from .modules.public.reference_data.sectors.routes import router as sectors_router -from .modules.public.reference_data.transport_modes.routes import router as transport_modes_router -from .modules.public.reference_data.customs_sections.routes import router as customs_sections_router -from .modules.public.reference_data.invoice_types.routes import router as invoice_types_router -from .modules.public.reference_data.code_pedimento_regimens.routes import router as code_pedimento_regimens_router -from .modules.public.reference_data.pedimento_regimens.routes import router as pedimento_regimens_router -from .modules.public.reference_data.incoterms.routes import router as incoterms_router -from .modules.a76.licenses import router as licenses_router +from .modules.a76.router import router as a76_router +from .modules.public.router import router as public_router # Router principal router = APIRouter() # Registrar módulos -router.include_router(auth_router) -router.include_router(tenants_router) -router.include_router(licenses_router) -router.include_router(pedimento_codes_router) -router.include_router(payment_methods_router) -router.include_router(containers_router) -router.include_router(countries_router) -router.include_router(material_types_router) -router.include_router(currency_types_router) -router.include_router(states_router) -router.include_router(transport_types_router) -router.include_router(customs_warehouses_router) -router.include_router(valuation_methods_router) -router.include_router(sectors_router) -router.include_router(transport_modes_router) -router.include_router(customs_sections_router) -router.include_router(invoice_types_router) -router.include_router(code_pedimento_regimens_router) -router.include_router(pedimento_regimens_router) -router.include_router(incoterms_router) +router.include_router(a76_router) +router.include_router(public_router) # Health check - - @router.get("/status") def status(): """Health check de la API""" diff --git a/backend/main.py b/backend/main.py index 3932471a..a11f226f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,11 +4,9 @@ Backend API con FastAPI + Keycloak + SQLAlchemy """ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from contextlib import asynccontextmanager import logging from core.config import settings -from core.database import init_db from core.middleware import ( TenantMiddleware, LicenseValidationMiddleware, @@ -51,7 +49,6 @@ app.add_middleware(TenantMiddleware) # Registrar routers app.include_router(api_v1_router, prefix="/api/v1") - @app.get("/api/") async def root(): """Root endpoint""" diff --git a/models.py b/models.py index dc0a617c..e69de29b 100644 --- a/models.py +++ b/models.py @@ -1,135 +0,0 @@ -from typing import List, Optional - -from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, SmallInteger, String, Table, UniqueConstraint, text -from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship -from sqlalchemy.orm.base import Mapped - -Base = declarative_base() -metadata = Base.metadata - - -class Gdatosvu(Base): - __tablename__ = 'gdatosvu' - __table_args__ = ( - PrimaryKeyConstraint('id_empageaadu', name='gdatosvu_pkey'), - ) - - id_empageaadu = mapped_column(String(10)) - ruta_arch_cer = mapped_column(String(1500)) - ruta_arch_key = mapped_column(String(1500)) - clave_acceso_fiel = mapped_column(String(50)) - usuario_webservice = mapped_column(String(100)) - clave_acceso_webservice = mapped_column(String(100)) - email_vu = mapped_column(String(800)) - tipo_figura_vu = mapped_column(String(30)) - ruta_vu_central = mapped_column(String(1500)) - ruta_archivos_xml = mapped_column(String(1500)) - rfc_consulta = mapped_column(String(30)) - toma_configuracion_vu = mapped_column(String(30)) - unidad_medida_vu = mapped_column(String(3)) - rfc_validacion_vu = mapped_column(String(30)) - ruta_archivo_cfdi = mapped_column(String(5000)) - ruta_archivo_key_cfdi = mapped_column(String(5000)) - fecha_venc_cer_cfdi = mapped_column(Date) - fecha_venc_key_cfdi = mapped_column(Date) - contrasena_cfdi = mapped_column(String(200)) - ruta_guardar_xml = mapped_column(String(5000)) - ruta_app_cfdi = mapped_column(String(5000)) - ruta_app_pac = mapped_column(String(5000)) - ruta_archivocancelacion = mapped_column(String(5000)) - contrasena_cancelacion = mapped_column(String(200)) - usuario_anam = mapped_column(String(100)) - contrasena_anam = mapped_column(String(200)) - fecha_creacion = mapped_column(DateTime, server_default=text('now()')) - fecha_actualizacion = mapped_column(DateTime) - - -class Gempresa(Base): - __tablename__ = 'gempresa' - __table_args__ = ( - PrimaryKeyConstraint('id_emp', name='gempresa_pkey'), - UniqueConstraint('consecutivo', name='gempresa_consecutivo_key') - ) - - id_emp = mapped_column(String(3), server_default=text("'EMP'::character varying")) - consecutivo = mapped_column(Boolean, server_default=text('true')) - nombre = mapped_column(String(255)) - rfc = mapped_column(String(30)) - actpreponderante = mapped_column(String(255)) - programa = mapped_column(String(10)) - numeroprograma = mapped_column(String(40)) - prosec = mapped_column(SmallInteger) - autorizacionprosec = mapped_column(String(20)) - manufacterid = mapped_column(String(25)) - broker_emp = mapped_column(String(10)) - responsable = mapped_column(String(80)) - respnombre = mapped_column(String(20)) - resppaterno = mapped_column(String(20)) - respmaterno = mapped_column(String(20)) - rfcresponsable = mapped_column(String(30)) - puesto = mapped_column(String(30)) - logo = mapped_column(String(255)) - tienelineaexpress = mapped_column(Boolean) - tipoformatoped = mapped_column(String(19)) - codigoanterior = mapped_column(SmallInteger) - esempresaservicio = mapped_column(Boolean) - nombrecliente = mapped_column(String(300)) - modosubmaquila = mapped_column(String(7)) - curp = mapped_column(String(19)) - nombrebdinter = mapped_column(String(100)) - ctpat_svi = mapped_column(String(100)) - numdeexportadorconfiable = mapped_column(String(50)) - claveprevalidador = mapped_column(String(20)) - septimaenmienda = mapped_column(Boolean) - fecha_creacion = mapped_column(DateTime, server_default=text('now()')) - fecha_actualizacion = mapped_column(DateTime) - - gempresa_sucursales: Mapped[List['GempresaSucursales']] = relationship('GempresaSucursales', uselist=True, back_populates='gempresa') - -t_gempresa_certificacion = Table( - 'gempresa_certificacion', metadata, - Column('id_empresa', String(3), nullable=False), - Column('esempresacertificada', Boolean), - Column('registroempcert', String(40)), - Column('fechainicialempcert', Date), - Column('fechafinalempcert', Date), - Column('fechacertificacionanexo31', Date), - Column('numerocertificacionanexo31', String(50)), - Column('modalidadanexo31', String(50)), - Column('tipoempresaanexo31', String(50)), - Column('empresaneec', Boolean), - Column('empresaoea', Boolean), - Column('empresarfe', Boolean), - Column('fecharenovacioncertificaciona31', Date), - Column('fechafinalcertificaciona31', Date), - Column('fecha_creacion', DateTime, server_default=text('now()')), - Column('fecha_actualizacion', DateTime), - ForeignKeyConstraint(['id_empresa'], ['gempresa.id_emp'], name='gempresa_certificacion_id_empresa_fkey') -) - - -class GempresaSucursales(Base): - __tablename__ = 'gempresa_sucursales' - __table_args__ = ( - ForeignKeyConstraint(['id_empresa'], ['gempresa.id_emp'], name='gempresa_sucursales_id_empresa_fkey'), - PrimaryKeyConstraint('id_sucursal', name='gempresa_sucursales_pkey') - ) - - id_empresa = mapped_column(String(3), nullable=False) - id_sucursal = mapped_column(Integer) - indicador = mapped_column(String(25)) - calle = mapped_column(String(255)) - num_ext = mapped_column(String(70)) - num_int = mapped_column(String(70)) - codigo_postal = mapped_column(String(15)) - colonia = mapped_column(String(50)) - ciudad = mapped_column(String(50)) - municipio = mapped_column(String(50)) - estado = mapped_column(String(40)) - pais = mapped_column(String(5)) - telefono = mapped_column(String(30)) - email = mapped_column(String(100)) - fecha_creacion = mapped_column(DateTime, server_default=text('now()')) - fecha_actualizacion = mapped_column(DateTime) - - gempresa: Mapped['Gempresa'] = relationship('Gempresa', back_populates='gempresa_sucursales') diff --git a/models_ped.py b/models_ped.py new file mode 100644 index 00000000..233407cf --- /dev/null +++ b/models_ped.py @@ -0,0 +1,27 @@ +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class PedimentoValidation(Base): + __tablename__ = 'pedimento_validation' + __table_args__ = ( + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_validation'), + PrimaryKeyConstraint('id', name='pedimento_validation_pkey'), + UniqueConstraint('pedimento_id', name='pedimento_validation_pedimento_id_key'), + {'schema': 'a76'} + ) + + id = mapped_column(Integer) + pedimento_id = mapped_column(Integer, nullable=False) + validator = mapped_column(String(3)) + validation_ack = mapped_column(String(8)) + pre_ack = mapped_column(String(8)) + line_signature = mapped_column(String(50)) + electronic_signature = mapped_column(String(999)) + certificate_number = mapped_column(String(99)) + validator_id = mapped_column(Integer) + responsible_id = mapped_column(Integer) + created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + + pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_validation') From cf18c8e07df95fa48aac6f4ce89735b23a3294a8 Mon Sep 17 00:00:00 2001 From: acazares Date: Thu, 6 Nov 2025 18:35:47 -0600 Subject: [PATCH 15/16] feat: Implement create/edit dialog for pedimentos management - Added create-edit-dialog component for creating and editing pedimentos. - Integrated dialog with data table actions for editing existing pedimentos. - Implemented infinite scroll functionality in the data table for loading more pedimentos. - Enhanced server-side loading of pedimentos with authentication checks. - Added filtering options for pedimentos based on status, client ID, and year. - Improved error handling and user feedback for actions like creating, editing, and deleting pedimentos. --- backend/core/config.py | 2 +- .../src/lib/api/dashboard/a76/pedimentos.ts | 126 ++++++ .../refrence_data/code_pedimento_regimens.ts | 8 +- .../api/dashboard/refrence_data/containers.ts | 8 +- .../api/dashboard/refrence_data/countries.ts | 10 +- .../dashboard/refrence_data/currency_types.ts | 10 +- .../refrence_data/customs_sections.ts | 10 +- .../refrence_data/customs_warehouses.ts | 10 +- .../api/dashboard/refrence_data/incoterms.ts | 10 +- .../dashboard/refrence_data/invoice_types.ts | 10 +- .../dashboard/refrence_data/material_types.ts | 10 +- .../refrence_data/payment_methods.ts | 10 +- .../refrence_data/pedimento_codes.ts | 10 +- .../refrence_data/pedimento_regimens.ts | 10 +- .../api/dashboard/refrence_data/sectors.ts | 10 +- .../lib/api/dashboard/refrence_data/states.ts | 10 +- .../refrence_data/transport_modes.ts | 10 +- .../refrence_data/transport_types.ts | 10 +- .../refrence_data/valuation_methods.ts | 10 +- .../dashboard/pedimentos/columns.ts | 234 ++++++++++ .../pedimentos/create-edit-dialog.svelte | 425 ++++++++++++++++++ .../pedimentos/data-table-actions.svelte | 179 ++++++++ .../dashboard/pedimentos/data-table.svelte | 123 +++++ .../src/lib/components/sidebar/modules.ts | 10 +- .../dashboard/pedimentos/+page.server.ts | 64 +++ .../routes/dashboard/pedimentos/+page.svelte | 352 +++++++++++++++ .../code_pedimento_regimens/+page.server.ts | 2 +- .../reference_data/containers/+page.server.ts | 2 +- .../reference_data/countries/+page.server.ts | 2 +- .../currency_types/+page.server.ts | 2 +- .../customs_sections/+page.server.ts | 2 +- .../customs_warehouses/+page.server.ts | 2 +- .../reference_data/incoterms/+page.server.ts | 2 +- .../invoice_types/+page.server.ts | 2 +- .../material_types/+page.server.ts | 2 +- .../payment_methods/+page.server.ts | 2 +- .../pedimento_codes/+page.server.ts | 2 +- .../pedimento_regimens/+page.server.ts | 2 +- .../reference_data/sectors/+page.server.ts | 2 +- .../reference_data/states/+page.server.ts | 2 +- .../transport_modes/+page.server.ts | 2 +- .../transport_types/+page.server.ts | 2 +- .../valuation_methods/+page.server.ts | 2 +- 43 files changed, 1609 insertions(+), 106 deletions(-) create mode 100644 frontend/src/lib/api/dashboard/a76/pedimentos.ts create mode 100644 frontend/src/lib/components/dashboard/pedimentos/columns.ts create mode 100644 frontend/src/lib/components/dashboard/pedimentos/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/pedimentos/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/pedimentos/data-table.svelte create mode 100644 frontend/src/routes/dashboard/pedimentos/+page.server.ts create mode 100644 frontend/src/routes/dashboard/pedimentos/+page.svelte diff --git a/backend/core/config.py b/backend/core/config.py index cfc509b6..83d290ad 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -15,7 +15,7 @@ class Settings(BaseSettings): ENVIRONMENT: str = "development" # Database - Core (Shared) - CORE_DB_HOST: str = "localhost" + CORE_DB_HOST: str = "postgres-a76" CORE_DB_PORT: int = 5432 CORE_DB_NAME: str = "anexo76_core" CORE_DB_USER: str = "postgres" diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts new file mode 100644 index 00000000..7428e18e --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -0,0 +1,126 @@ +/** + * API Client para Pedimentos + * Gestiona las operaciones CRUD para pedimentos + */ +import { api } from '$lib/api'; + +export interface Pedimento { + id: number; + tenant_id: number; + year?: string | null; + customs_office?: string | null; + license?: string | null; + pedimento_number?: string | null; + client_id?: number | null; + operation_type?: number | null; + pedimento_type?: number | null; + pedimento_key?: string | null; + regime?: string | null; + status?: string | null; + usd_value?: number | null; + paid_price?: number | null; + gross_weight?: number | null; + exchange_rate?: number | null; + created_at: string; +} + +export interface PedimentoListResponse { + items: Pedimento[]; + total: number; + page: number; + page_size: number; +} + +export interface CreatePedimentoData { + year?: string | null; + customs_office?: string | null; + license?: string | null; + pedimento_number?: string | null; + client_id?: number | null; + operation_type?: number | null; + pedimento_type?: number | null; + pedimento_key?: string | null; + regime?: string | null; + status?: string | null; + usd_value?: number | null; + paid_price?: number | null; + gross_weight?: number | null; + exchange_rate?: number | null; +} + +export interface UpdatePedimentoData { + year?: string | null; + customs_office?: string | null; + license?: string | null; + pedimento_number?: string | null; + client_id?: number | null; + operation_type?: number | null; + pedimento_type?: number | null; + pedimento_key?: string | null; + regime?: string | null; + status?: string | null; + usd_value?: number | null; + paid_price?: number | null; + gross_weight?: number | null; + exchange_rate?: number | null; +} + +export interface PedimentoFilters { + status?: string; + client_id?: number; + year?: string; +} + +/** + * API para Pedimentos + */ +export const pedimentosApi = { + /** + * Lista todos los pedimentos con paginación y filtros + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + * @param filters - Filtros opcionales + */ + list: (page = 1, pageSize = 50, filters?: PedimentoFilters) => { + let url = `/v1/a76/pedimentos?page=${page}&page_size=${pageSize}`; + + if (filters?.status) { + url += `&status=${encodeURIComponent(filters.status)}`; + } + if (filters?.client_id) { + url += `&client_id=${filters.client_id}`; + } + if (filters?.year) { + url += `&year=${encodeURIComponent(filters.year)}`; + } + + return api.get(url); + }, + + /** + * Obtiene un pedimento por ID + * @param id - ID del pedimento + */ + get: (id: number) => api.get(`/v1/a76/pedimentos/${id}`), + + /** + * Crea un nuevo pedimento + * @param data - Datos del pedimento a crear + */ + create: (data: CreatePedimentoData) => + api.post('/v1/a76/pedimentos', data), + + /** + * Actualiza un pedimento existente + * @param id - ID del pedimento a actualizar + * @param data - Datos a actualizar + */ + update: (id: number, data: UpdatePedimentoData) => + api.put(`/v1/a76/pedimentos/${id}`, data), + + /** + * Elimina un pedimento + * @param id - ID del pedimento a eliminar + */ + delete: (id: number) => api.delete(`/v1/a76/pedimentos/${id}`) +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts index 837216f8..fd8e4d4a 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts @@ -41,7 +41,7 @@ export const codePedimentoRegimensApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/code-pedimento-regimens?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/code-pedimento-regimens?page=${page}&page_size=${pageSize}` ), /** @@ -55,7 +55,7 @@ export const codePedimentoRegimensApi = { * @param data - Datos del code pedimento regimen a crear */ create: (data: CreateCodePedimentoRegimenData) => - api.post('/v1/code-pedimento-regimens', data), + api.post('/v1/public/refrence_data/code-pedimento-regimens', data), /** * Actualiza un code pedimento regimen existente @@ -63,11 +63,11 @@ export const codePedimentoRegimensApi = { * @param data - Datos a actualizar */ update: (id: number, data: UpdateCodePedimentoRegimenData) => - api.put(`/v1/code-pedimento-regimens/${id}`, data), + api.put(`/v1/public/refrence_data/code-pedimento-regimens/${id}`, data), /** * Elimina un code pedimento regimen * @param id - ID del code pedimento regimen a eliminar */ - delete: (id: number) => api.delete(`/v1/code-pedimento-regimens/${id}`) + delete: (id: number) => api.delete(`/v1/public/refrence_data/code-pedimento-regimens/${id}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/containers.ts b/frontend/src/lib/api/dashboard/refrence_data/containers.ts index 1bd2a7fc..64294fbe 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/containers.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/containers.ts @@ -37,7 +37,7 @@ export const containersApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/containers?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/containers?page=${page}&page_size=${pageSize}` ), /** @@ -51,7 +51,7 @@ export const containersApi = { * @param data - Datos del container a crear */ create: (data: CreateContainerData) => - api.post('/v1/containers', data), + api.post('/v1/public/refrence_data/containers', data), /** * Actualiza un container existente @@ -59,11 +59,11 @@ export const containersApi = { * @param data - Datos a actualizar */ update: (key: number, data: UpdateContainerData) => - api.put(`/v1/containers/${key}`, data), + api.put(`/v1/public/refrence_data/containers/${key}`, data), /** * Elimina un container * @param key - ID del container a eliminar */ - delete: (key: number) => api.delete(`/v1/containers/${key}`) + delete: (key: number) => api.delete(`/v1/public/refrence_data/containers/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/countries.ts b/frontend/src/lib/api/dashboard/refrence_data/countries.ts index 66fba25e..55bea4b4 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/countries.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/countries.ts @@ -46,21 +46,21 @@ export const countriesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/countries?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/countries?page=${page}&page_size=${pageSize}` ), /** * Obtiene un país por su clave M3 * @param m3_key - Clave M3 del país */ - get: (m3_key: string) => api.get(`/v1/countries/${m3_key}`), + get: (m3_key: string) => api.get(`/v1/public/refrence_data/countries/${m3_key}`), /** * Crea un nuevo país * @param data - Datos del país a crear */ create: (data: CreateCountryData) => - api.post('/v1/countries', data), + api.post('/v1/public/refrence_data/countries', data), /** * Actualiza un país existente @@ -68,11 +68,11 @@ export const countriesApi = { * @param data - Datos a actualizar */ update: (m3_key: string, data: UpdateCountryData) => - api.put(`/v1/countries/${m3_key}`, data), + api.put(`/v1/public/refrence_data/countries/${m3_key}`, data), /** * Elimina un país * @param m3_key - Clave M3 del país a eliminar */ - delete: (m3_key: string) => api.delete(`/v1/countries/${m3_key}`) + delete: (m3_key: string) => api.delete(`/v1/public/refrence_data/countries/${m3_key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts index 68fdec93..a3912934 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts @@ -40,21 +40,21 @@ export const currencyTypesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/currency-types?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/currency-types?page=${page}&page_size=${pageSize}` ), /** * Obtiene un tipo de moneda por código * @param code - Código del tipo de moneda */ - get: (code: string) => api.get(`/v1/currency-types/${code}`), + get: (code: string) => api.get(`/v1/public/refrence_data/currency-types/${code}`), /** * Crea un nuevo tipo de moneda * @param data - Datos del tipo de moneda a crear */ create: (data: CreateCurrencyTypeData) => - api.post('/v1/currency-types', data), + api.post('/v1/public/refrence_data/currency-types', data), /** * Actualiza un tipo de moneda existente @@ -62,11 +62,11 @@ export const currencyTypesApi = { * @param data - Datos a actualizar */ update: (code: string, data: UpdateCurrencyTypeData) => - api.put(`/v1/currency-types/${code}`, data), + api.put(`/v1/public/refrence_data/currency-types/${code}`, data), /** * Elimina un tipo de moneda * @param code - Código del tipo de moneda a eliminar */ - delete: (code: string) => api.delete(`/v1/currency-types/${code}`) + delete: (code: string) => api.delete(`/v1/public/refrence_data/currency-types/${code}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts index e67620ff..5ee4141b 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts @@ -37,21 +37,21 @@ export const customsSectionsApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/customs-sections?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/customs-sections?page=${page}&page_size=${pageSize}` ), /** * Obtiene una sección aduanera por código * @param customs_code - Código de la sección aduanera */ - get: (customs_code: string) => api.get(`/v1/customs-sections/${customs_code}`), + get: (customs_code: string) => api.get(`/v1/public/refrence_data/customs-sections/${customs_code}`), /** * Crea una nueva sección aduanera * @param data - Datos de la sección aduanera a crear */ create: (data: CreateCustomsSectionData) => - api.post('/v1/customs-sections', data), + api.post('/v1/public/refrence_data/customs-sections', data), /** * Actualiza una sección aduanera existente @@ -59,11 +59,11 @@ export const customsSectionsApi = { * @param data - Datos a actualizar */ update: (customs_code: string, data: UpdateCustomsSectionData) => - api.put(`/v1/customs-sections/${customs_code}`, data), + api.put(`/v1/public/refrence_data/customs-sections/${customs_code}`, data), /** * Elimina una sección aduanera * @param customs_code - Código de la sección aduanera a eliminar */ - delete: (customs_code: string) => api.delete(`/v1/customs-sections/${customs_code}`) + delete: (customs_code: string) => api.delete(`/v1/public/refrence_data/customs-sections/${customs_code}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts index 3ef02d19..5b2bb3c1 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts @@ -40,7 +40,7 @@ export const customsWarehousesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/customs-warehouses?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/customs-warehouses?page=${page}&page_size=${pageSize}` ), /** @@ -49,14 +49,14 @@ export const customsWarehousesApi = { * @param customs - Aduana asociada */ get: (key: string, customs: string) => - api.get(`/v1/customs-warehouses/${key}/${customs}`), + api.get(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}`), /** * Crea un nuevo recinto fiscalizado * @param data - Datos del recinto fiscalizado a crear */ create: (data: CreateCustomsWarehouseData) => - api.post('/v1/customs-warehouses', data), + api.post('/v1/public/refrence_data/customs-warehouses', data), /** * Actualiza un recinto fiscalizado existente @@ -65,7 +65,7 @@ export const customsWarehousesApi = { * @param data - Datos a actualizar */ update: (key: string, customs: string, data: UpdateCustomsWarehouseData) => - api.put(`/v1/customs-warehouses/${key}/${customs}`, data), + api.put(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}`, data), /** * Elimina un recinto fiscalizado @@ -73,5 +73,5 @@ export const customsWarehousesApi = { * @param customs - Aduana asociada */ delete: (key: string, customs: string) => - api.delete(`/v1/customs-warehouses/${key}/${customs}`) + api.delete(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts index ece31dd7..a83609e1 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts @@ -40,21 +40,21 @@ export const incotermsApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/incoterms?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/incoterms?page=${page}&page_size=${pageSize}` ), /** * Obtiene un incoterm por código * @param code - Código del incoterm */ - get: (code: string) => api.get(`/v1/incoterms/${code}`), + get: (code: string) => api.get(`/v1/public/refrence_data/incoterms/${code}`), /** * Crea un nuevo incoterm * @param data - Datos del incoterm a crear */ create: (data: CreateIncotermData) => - api.post('/v1/incoterms', data), + api.post('/v1/public/refrence_data/incoterms', data), /** * Actualiza un incoterm existente @@ -62,11 +62,11 @@ export const incotermsApi = { * @param data - Datos a actualizar */ update: (code: string, data: UpdateIncotermData) => - api.put(`/v1/incoterms/${code}`, data), + api.put(`/v1/public/refrence_data/incoterms/${code}`, data), /** * Elimina un incoterm * @param code - Código del incoterm a eliminar */ - delete: (code: string) => api.delete(`/v1/incoterms/${code}`) + delete: (code: string) => api.delete(`/v1/public/refrence_data/incoterms/${code}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts index 8cfbfd46..8b132732 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts @@ -43,21 +43,21 @@ export const invoiceTypesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/invoice-types?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/invoice-types?page=${page}&page_size=${pageSize}` ), /** * Obtiene un tipo de factura por key * @param key - Clave del tipo de factura */ - get: (key: string) => api.get(`/v1/invoice-types/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/invoice-types/${key}`), /** * Crea un nuevo tipo de factura * @param data - Datos del tipo de factura a crear */ create: (data: CreateInvoiceTypeData) => - api.post('/v1/invoice-types', data), + api.post('/v1/public/refrence_data/invoice-types', data), /** * Actualiza un tipo de factura existente @@ -65,11 +65,11 @@ export const invoiceTypesApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateInvoiceTypeData) => - api.put(`/v1/invoice-types/${key}`, data), + api.put(`/v1/public/refrence_data/invoice-types/${key}`, data), /** * Elimina un tipo de factura * @param key - Clave del tipo de factura a eliminar */ - delete: (key: string) => api.delete(`/v1/invoice-types/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/invoice-types/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/material_types.ts b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts index d3156e5f..e9457401 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/material_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts @@ -40,21 +40,21 @@ export const materialTypesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/material-types?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/material-types?page=${page}&page_size=${pageSize}` ), /** * Obtiene un tipo de material por key * @param key - Clave del tipo de material */ - get: (key: string) => api.get(`/v1/material-types/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/material-types/${key}`), /** * Crea un nuevo tipo de material * @param data - Datos del tipo de material a crear */ create: (data: CreateMaterialTypeData) => - api.post('/v1/material-types', data), + api.post('/v1/public/refrence_data/material-types', data), /** * Actualiza un tipo de material existente @@ -62,11 +62,11 @@ export const materialTypesApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateMaterialTypeData) => - api.put(`/v1/material-types/${key}`, data), + api.put(`/v1/public/refrence_data/material-types/${key}`, data), /** * Elimina un tipo de material * @param key - Clave del tipo de material a eliminar */ - delete: (key: string) => api.delete(`/v1/material-types/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/material-types/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts index 5706765b..cf651f59 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts @@ -37,21 +37,21 @@ export const paymentMethodsApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/payment-methods?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/payment-methods?page=${page}&page_size=${pageSize}` ), /** * Obtiene un método de pago por key * @param key - Clave del método de pago */ - get: (key: string) => api.get(`/v1/payment-methods/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/payment-methods/${key}`), /** * Crea un nuevo método de pago * @param data - Datos del método de pago a crear */ create: (data: CreatePaymentMethodData) => - api.post('/v1/payment-methods', data), + api.post('/v1/public/refrence_data/payment-methods', data), /** * Actualiza un método de pago existente @@ -59,11 +59,11 @@ export const paymentMethodsApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdatePaymentMethodData) => - api.put(`/v1/payment-methods/${key}`, data), + api.put(`/v1/public/refrence_data/payment-methods/${key}`, data), /** * Elimina un método de pago * @param key - Clave del método de pago a eliminar */ - delete: (key: string) => api.delete(`/v1/payment-methods/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/payment-methods/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts index c50c3d59..36d0d46e 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts @@ -37,21 +37,21 @@ export const pedimentoCodesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/pedimento-codes?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/pedimento-codes?page=${page}&page_size=${pageSize}` ), /** * Obtiene una clave de pedimento por code * @param code - Código de la clave de pedimento */ - get: (code: string) => api.get(`/v1/pedimento-codes/${code}`), + get: (code: string) => api.get(`/v1/public/refrence_data/pedimento-codes/${code}`), /** * Crea una nueva clave de pedimento * @param data - Datos de la clave de pedimento a crear */ create: (data: CreatePedimentoCodeData) => - api.post('/v1/pedimento-codes', data), + api.post('/v1/public/refrence_data/pedimento-codes', data), /** * Actualiza una clave de pedimento existente @@ -59,11 +59,11 @@ export const pedimentoCodesApi = { * @param data - Datos a actualizar */ update: (code: string, data: UpdatePedimentoCodeData) => - api.put(`/v1/pedimento-codes/${code}`, data), + api.put(`/v1/public/refrence_data/pedimento-codes/${code}`, data), /** * Elimina una clave de pedimento * @param code - Código de la clave de pedimento a eliminar */ - delete: (code: string) => api.delete(`/v1/pedimento-codes/${code}`) + delete: (code: string) => api.delete(`/v1/public/refrence_data/pedimento-codes/${code}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts index 0a3dbe0b..2fdf716c 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts @@ -37,21 +37,21 @@ export const pedimentoRegimensApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/pedimento-regimens?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/pedimento-regimens?page=${page}&page_size=${pageSize}` ), /** * Obtiene un régimen de pedimento por code * @param code - Código del régimen de pedimento */ - get: (code: string) => api.get(`/v1/pedimento-regimens/${code}`), + get: (code: string) => api.get(`/v1/public/refrence_data/pedimento-regimens/${code}`), /** * Crea un nuevo régimen de pedimento * @param data - Datos del régimen de pedimento a crear */ create: (data: CreatePedimentoRegimenData) => - api.post('/v1/pedimento-regimens', data), + api.post('/v1/public/refrence_data/pedimento-regimens', data), /** * Actualiza un régimen de pedimento existente @@ -59,11 +59,11 @@ export const pedimentoRegimensApi = { * @param data - Datos a actualizar */ update: (code: string, data: UpdatePedimentoRegimenData) => - api.put(`/v1/pedimento-regimens/${code}`, data), + api.put(`/v1/public/refrence_data/pedimento-regimens/${code}`, data), /** * Elimina un régimen de pedimento * @param code - Código del régimen de pedimento a eliminar */ - delete: (code: string) => api.delete(`/v1/pedimento-regimens/${code}`) + delete: (code: string) => api.delete(`/v1/public/refrence_data/pedimento-regimens/${code}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/sectors.ts b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts index 01013760..1d334cf0 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/sectors.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts @@ -40,21 +40,21 @@ export const sectorsApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/sectors?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/sectors?page=${page}&page_size=${pageSize}` ), /** * Obtiene un sector por key * @param key - Clave del sector */ - get: (key: string) => api.get(`/v1/sectors/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/sectors/${key}`), /** * Crea un nuevo sector * @param data - Datos del sector a crear */ create: (data: CreateSectorData) => - api.post('/v1/sectors', data), + api.post('/v1/public/refrence_data/sectors', data), /** * Actualiza un sector existente @@ -62,11 +62,11 @@ export const sectorsApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateSectorData) => - api.put(`/v1/sectors/${key}`, data), + api.put(`/v1/public/refrence_data/sectors/${key}`, data), /** * Elimina un sector * @param key - Clave del sector a eliminar */ - delete: (key: string) => api.delete(`/v1/sectors/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/sectors/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/states.ts b/frontend/src/lib/api/dashboard/refrence_data/states.ts index 339a65c0..ed24c5c4 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/states.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/states.ts @@ -43,21 +43,21 @@ export const statesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/states?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/states?page=${page}&page_size=${pageSize}` ), /** * Obtiene un estado por m3_key * @param m3Key - Clave M3 del estado */ - get: (m3Key: string) => api.get(`/v1/states/${m3Key}`), + get: (m3Key: string) => api.get(`/v1/public/refrence_data/states/${m3Key}`), /** * Crea un nuevo estado * @param data - Datos del estado a crear */ create: (data: CreateStateData) => - api.post('/v1/states', data), + api.post('/v1/public/refrence_data/states', data), /** * Actualiza un estado existente @@ -65,11 +65,11 @@ export const statesApi = { * @param data - Datos a actualizar */ update: (m3Key: string, data: UpdateStateData) => - api.put(`/v1/states/${m3Key}`, data), + api.put(`/v1/public/refrence_data/states/${m3Key}`, data), /** * Elimina un estado * @param m3Key - Clave M3 del estado a eliminar */ - delete: (m3Key: string) => api.delete(`/v1/states/${m3Key}`) + delete: (m3Key: string) => api.delete(`/v1/public/refrence_data/states/${m3Key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts index 277df5e8..f62d6b56 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts @@ -37,21 +37,21 @@ export const transportModesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/transport-modes?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/transport-modes?page=${page}&page_size=${pageSize}` ), /** * Obtiene un modo de transporte por key * @param key - Clave del modo de transporte */ - get: (key: string) => api.get(`/v1/transport-modes/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/transport-modes/${key}`), /** * Crea un nuevo modo de transporte * @param data - Datos del modo de transporte a crear */ create: (data: CreateTransportModeData) => - api.post('/v1/transport-modes', data), + api.post('/v1/public/refrence_data/transport-modes', data), /** * Actualiza un modo de transporte existente @@ -59,11 +59,11 @@ export const transportModesApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateTransportModeData) => - api.put(`/v1/transport-modes/${key}`, data), + api.put(`/v1/public/refrence_data/transport-modes/${key}`, data), /** * Elimina un modo de transporte * @param key - Clave del modo de transporte a eliminar */ - delete: (key: string) => api.delete(`/v1/transport-modes/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/transport-modes/${key}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts index 5b09910b..bbb2e06b 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts @@ -37,21 +37,21 @@ export const transportTypesApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/transport-types?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/transport-types?page=${page}&page_size=${pageSize}` ), /** * Obtiene un tipo de transporte por transport_code * @param transportCode - Código del tipo de transporte */ - get: (transportCode: string) => api.get(`/v1/transport-types/${transportCode}`), + get: (transportCode: string) => api.get(`/v1/public/refrence_data/transport-types/${transportCode}`), /** * Crea un nuevo tipo de transporte * @param data - Datos del tipo de transporte a crear */ create: (data: CreateTransportTypeData) => - api.post('/v1/transport-types', data), + api.post('/v1/public/refrence_data/transport-types', data), /** * Actualiza un tipo de transporte existente @@ -59,11 +59,11 @@ export const transportTypesApi = { * @param data - Datos a actualizar */ update: (transportCode: string, data: UpdateTransportTypeData) => - api.put(`/v1/transport-types/${transportCode}`, data), + api.put(`/v1/public/refrence_data/transport-types/${transportCode}`, data), /** * Elimina un tipo de transporte * @param transportCode - Código del tipo de transporte a eliminar */ - delete: (transportCode: string) => api.delete(`/v1/transport-types/${transportCode}`) + delete: (transportCode: string) => api.delete(`/v1/public/refrence_data/transport-types/${transportCode}`) }; diff --git a/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts index 54f729ad..a4277151 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts @@ -37,21 +37,21 @@ export const valuationMethodsApi = { */ list: (page = 1, pageSize = 50) => api.get( - `/v1/valuation-methods?page=${page}&page_size=${pageSize}` + `/v1/public/refrence_data/valuation-methods?page=${page}&page_size=${pageSize}` ), /** * Obtiene un método de valoración por key * @param key - Clave del método de valoración */ - get: (key: string) => api.get(`/v1/valuation-methods/${key}`), + get: (key: string) => api.get(`/v1/public/refrence_data/valuation-methods/${key}`), /** * Crea un nuevo método de valoración * @param data - Datos del método de valoración a crear */ create: (data: CreateValuationMethodData) => - api.post('/v1/valuation-methods', data), + api.post('/v1/public/refrence_data/valuation-methods', data), /** * Actualiza un método de valoración existente @@ -59,11 +59,11 @@ export const valuationMethodsApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateValuationMethodData) => - api.put(`/v1/valuation-methods/${key}`, data), + api.put(`/v1/public/refrence_data/valuation-methods/${key}`, data), /** * Elimina un método de valoración * @param key - Clave del método de valoración a eliminar */ - delete: (key: string) => api.delete(`/v1/valuation-methods/${key}`) + delete: (key: string) => api.delete(`/v1/public/refrence_data/valuation-methods/${key}`) }; diff --git a/frontend/src/lib/components/dashboard/pedimentos/columns.ts b/frontend/src/lib/components/dashboard/pedimentos/columns.ts new file mode 100644 index 00000000..30aae1a0 --- /dev/null +++ b/frontend/src/lib/components/dashboard/pedimentos/columns.ts @@ -0,0 +1,234 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; + +export type Pedimento = { + id: number; + tenant_id: number; + year?: string | null; + customs_office?: string | null; + license?: string | null; + pedimento_number?: string | null; + client_id?: number | null; + operation_type?: number | null; + pedimento_type?: number | null; + pedimento_key?: string | null; + regime?: string | null; + status?: string | null; + usd_value?: number | null; + paid_price?: number | null; + gross_weight?: number | null; + exchange_rate?: number | null; + created_at: string; +}; + +/** + * Formatea un número como moneda + */ +function formatCurrency(value?: number | null): string { + if (value === null || value === undefined) return '-'; + return new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value); +} + +/** + * Formatea un número con separadores de miles + */ +function formatNumber(value?: number | null, decimals = 2): string { + if (value === null || value === undefined) return '-'; + return new Intl.NumberFormat('es-MX', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals + }).format(value); +} + +/** + * Formatea una fecha + */ +function formatDate(date?: string | null): string { + if (!date) return '-'; + return new Date(date).toLocaleDateString('es-MX', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +} + +/** + * Obtiene el color del badge según el status + */ +function getStatusColor(status?: string | null): string { + if (!status) return 'bg-gray-100 text-gray-800'; + + const statusLower = status.toLowerCase(); + if (statusLower.includes('activo') || statusLower.includes('completado')) { + return 'bg-green-100 text-green-800'; + } else if (statusLower.includes('pendiente') || statusLower.includes('proceso')) { + return 'bg-yellow-100 text-yellow-800'; + } else if (statusLower.includes('cancelado') || statusLower.includes('rechazado')) { + return 'bg-red-100 text-red-800'; + } + return 'bg-blue-100 text-blue-800'; +} + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => { + const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { + const { id } = getId(); + return { + render: () => + `
#${id}
` + }; + }); + return renderSnippet(idSnippet, { id: row.original.id }); + } + }, + { + accessorKey: "pedimento_number", + header: "Número de Pedimento", + cell: ({ row }) => { + const pedimento = row.original; + const fullNumber = `${pedimento.year || ''}${pedimento.customs_office || ''}${pedimento.license || ''}${pedimento.pedimento_number || ''}`; + + const numberSnippet = createRawSnippet<[{ number: string }]>((getNumber) => { + const { number } = getNumber(); + return { + render: () => + `${number || 'N/A'}` + }; + }); + return renderSnippet(numberSnippet, { number: fullNumber }); + } + }, + { + accessorKey: "client_id", + header: "Cliente", + cell: ({ row }) => { + const clientSnippet = createRawSnippet<[{ clientId?: number | null }]>((getClient) => { + const { clientId } = getClient(); + return { + render: () => + `
${clientId ? `Cliente #${clientId}` : '-'}
` + }; + }); + return renderSnippet(clientSnippet, { clientId: row.original.client_id }); + } + }, + { + accessorKey: "status", + header: "Estado", + cell: ({ row }) => { + const status = row.original.status; + const colorClass = getStatusColor(status); + + const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => { + const { status, colorClass } = getStatus(); + return { + render: () => + ` + ${status || 'N/A'} + ` + }; + }); + return renderSnippet(statusSnippet, { status, colorClass }); + } + }, + { + accessorKey: "usd_value", + header: () => { + const headerSnippet = createRawSnippet(() => { + return { + render: () => `
Valor USD
` + }; + }); + return renderSnippet(headerSnippet, {}); + }, + cell: ({ row }) => { + const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => { + const { value } = getValue(); + return { + render: () => + `
${value}
` + }; + }); + return renderSnippet(valueSnippet, { value: formatCurrency(row.original.usd_value) }); + } + }, + { + accessorKey: "paid_price", + header: () => { + const headerSnippet = createRawSnippet(() => { + return { + render: () => `
Precio Pagado
` + }; + }); + return renderSnippet(headerSnippet, {}); + }, + cell: ({ row }) => { + const priceSnippet = createRawSnippet<[{ price: string }]>((getPrice) => { + const { price } = getPrice(); + return { + render: () => + `
${price}
` + }; + }); + return renderSnippet(priceSnippet, { price: formatCurrency(row.original.paid_price) }); + } + }, + { + accessorKey: "gross_weight", + header: () => { + const headerSnippet = createRawSnippet(() => { + return { + render: () => `
Peso Bruto
` + }; + }); + return renderSnippet(headerSnippet, {}); + }, + cell: ({ row }) => { + const weightSnippet = createRawSnippet<[{ weight: string }]>((getWeight) => { + const { weight } = getWeight(); + return { + render: () => + `
${weight}
` + }; + }); + return renderSnippet(weightSnippet, { weight: formatNumber(row.original.gross_weight, 3) }); + } + }, + { + accessorKey: "created_at", + header: "Fecha de Creación", + cell: ({ row }) => { + const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => { + const { date } = getDate(); + return { + render: () => + `
${date}
` + }; + }); + return renderSnippet(dateSnippet, { date: formatDate(row.original.created_at) }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original, onSuccess }); + } + } + ]; +} + +// Mantener compatibilidad hacia atrás +export const columns = createColumns(); diff --git a/frontend/src/lib/components/dashboard/pedimentos/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/pedimentos/create-edit-dialog.svelte new file mode 100644 index 00000000..e6a80298 --- /dev/null +++ b/frontend/src/lib/components/dashboard/pedimentos/create-edit-dialog.svelte @@ -0,0 +1,425 @@ + + + + + + + {isEditing ? "Editar" : "Nuevo"} Pedimento + + + {isEditing + ? "Modifica los datos del pedimento." + : "Completa los datos para crear un nuevo pedimento."} + + + + + {#if error} +
+ {error} +
+ {/if} + + +
+

Información del Pedimento

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+

Cliente y Operación

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Información Financiera

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + + + + +
+
diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table-actions.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table-actions.svelte new file mode 100644 index 00000000..9df58ec8 --- /dev/null +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table-actions.svelte @@ -0,0 +1,179 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + + + + + + + Ver detalles + + + + + + + Editar + + + + {#if loading} + + + + + {:else} + + + + + + {/if} + Eliminar + + + + +{#if showEditDialog} + + {#await import('./create-edit-dialog.svelte') then { default: CreateEditDialog }} + + {/await} +{/if} diff --git a/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte new file mode 100644 index 00000000..ef98de23 --- /dev/null +++ b/frontend/src/lib/components/dashboard/pedimentos/data-table.svelte @@ -0,0 +1,123 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más... +
+ {:else} +
+ Desplázate para cargar más +
+ {/if} +
+
+
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index ee9b1269..77a9cf17 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -286,23 +286,23 @@ export function getSidebarData(): SidebarData { items: [ { title: m["sidebar.pedimentos.pedimento_management"](), - url: "#", + url: "/dashboard/pedimentos", }, { title: m["sidebar.pedimentos.pedimento_codes"](), - url: "#", + url: "/dashboard/reference_data/pedimento_codes", }, { title: m["sidebar.pedimentos.customs_regimes"](), - url: "#", + url: "/dashboard/reference_data/pedimento_regimens", }, { title: m["sidebar.pedimentos.payment_methods"](), - url: "#", + url: "/dashboard/reference_data/payment_methods", }, { title: m["sidebar.pedimentos.customs_sections"](), - url: "#", + url: "/dashboard/reference_data/customs_sections", }, { title: m["sidebar.pedimentos.anexo_22_app_31"](), diff --git a/frontend/src/routes/dashboard/pedimentos/+page.server.ts b/frontend/src/routes/dashboard/pedimentos/+page.server.ts new file mode 100644 index 00000000..e69dd3c9 --- /dev/null +++ b/frontend/src/routes/dashboard/pedimentos/+page.server.ts @@ -0,0 +1,64 @@ +import type { PageServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; + +export const load: PageServerLoad = async ({ fetch, cookies }) => { + // Verificar autenticación + const token = cookies.get('access_token'); + + if (!token) { + throw redirect(302, '/login'); + } + + try { + // Cargar datos iniciales de pedimentos + // Configurar la URL de la API para SSR + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = import.meta.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + const response = await fetch(`${baseUrl}v1/a76/pedimentos?page=1&page_size=50`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + throw redirect(302, '/login'); + } + + return { + items: [], + total: 0, + page: 1, + page_size: 50, + error: 'Error al cargar pedimentos' + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || 1, + page_size: data.page_size || 50 + }; + } catch (error) { + console.error('Error loading pedimentos:', error); + return { + items: [], + total: 0, + page: 1, + page_size: 50, + error: 'Error al cargar pedimentos' + }; + } +}; diff --git a/frontend/src/routes/dashboard/pedimentos/+page.svelte b/frontend/src/routes/dashboard/pedimentos/+page.svelte new file mode 100644 index 00000000..22f489ee --- /dev/null +++ b/frontend/src/routes/dashboard/pedimentos/+page.svelte @@ -0,0 +1,352 @@ + + +
+ +
+
+

Pedimentos

+

+ Gestiona los pedimentos del sistema +

+
+ +
+ + + + + Filtros + Filtra los pedimentos por diferentes criterios + + +
{ e.preventDefault(); applyFilters(); }} class="grid grid-cols-1 md:grid-cols-4 gap-4"> +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Pedimentos + + Mostrando {allItems.length} de {totalItems} registros + +
+ +
+
+ + + + +
+
+ + + diff --git a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts index ff2f87ee..2ac8d182 100644 --- a/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/code_pedimento_regimens/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/code-pedimento-regimens?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/code-pedimento-regimens?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts b/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts index d63ee421..3210dc37 100644 --- a/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/containers/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/containers?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/containers?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts b/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts index 3c8e022c..f2a7631b 100644 --- a/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/countries/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/countries?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/countries?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts index ac813d0b..a8a17651 100644 --- a/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/currency_types/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/currency-types?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/currency-types?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts index fecf64d7..d0d6683a 100644 --- a/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/customs_sections/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/customs-sections?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/customs-sections?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts index cb9e87b3..a7a11917 100644 --- a/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/customs_warehouses/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/customs-warehouses?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/customs-warehouses?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts index d5b9421b..347e8ac8 100644 --- a/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/incoterms/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/incoterms?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/incoterms?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts index f3b965a7..1ddd9026 100644 --- a/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/invoice_types/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/invoice-types?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/invoice-types?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts index f55dc6b3..fbf97e7e 100644 --- a/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/material_types/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/material-types?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/material-types?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts index 5cabd2a5..f197fd6a 100644 --- a/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/payment_methods/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/payment-methods?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/payment-methods?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts index d4cfdcc6..4cd1e31b 100644 --- a/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/pedimento_codes/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/pedimento-codes?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/pedimento-codes?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts index 842031d1..aa169764 100644 --- a/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/pedimento_regimens/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/pedimento-regimens?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/pedimento-regimens?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts b/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts index fbed6e9e..21eef54e 100644 --- a/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/sectors/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/sectors?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/sectors?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/states/+page.server.ts b/frontend/src/routes/dashboard/reference_data/states/+page.server.ts index 0d62247b..c50ce29e 100644 --- a/frontend/src/routes/dashboard/reference_data/states/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/states/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/states?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/states?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts index 68aa9999..8a7a6969 100644 --- a/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/transport_modes/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/transport-modes?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/transport-modes?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts b/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts index 2704feed..0742b526 100644 --- a/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/transport_types/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/transport-types?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/transport-types?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, diff --git a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts index def89f09..40ffc4bb 100644 --- a/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts +++ b/frontend/src/routes/dashboard/reference_data/valuation_methods/+page.server.ts @@ -33,7 +33,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; const response = await fetch( - `${baseUrl}v1/valuation-methods?page=${page}&page_size=${pageSize}`, + `${baseUrl}v1/public/refrence_data/valuation-methods?page=${page}&page_size=${pageSize}`, { headers: { 'Authorization': `Bearer ${token}`, From ef5c40af925be351487205cadaf18961ab1960c2 Mon Sep 17 00:00:00 2001 From: acazares Date: Thu, 6 Nov 2025 20:29:18 -0600 Subject: [PATCH 16/16] feat: Implement client and provider management service - Added service layer for handling client and provider operations including creation, retrieval, updating, and deletion. - Introduced DTOs for data transfer and validation. - Implemented filtering and pagination for client/provider listing. - Added logging for better traceability of operations. feat: Create parts management module - Developed a complete module for managing parts/components including creation, retrieval, updating, and deletion. - Introduced DTOs for parts with detailed attributes and validation. - Implemented search and filtering capabilities for parts based on various criteria. - Added endpoints for regulatory information retrieval and parts statistics. - Integrated logging for error handling and operational insights. --- .gitignore | 1 - .../versions/03b786378f94_pedimentos.py | 2 +- ...54f2046774d0_create_new_a76_tables_only.py | 16 +- ...reate_a76_tables_company_clients_parts_.py | 355 ------------------ .../a76/{GClass => classes}/__init__.py | 2 +- .../v1/modules/a76/{GClass => classes}/dto.py | 0 .../modules/a76/{GClass => classes}/models.py | 14 +- .../modules/a76/{GClass => classes}/routes.py | 0 .../a76/{GClass => classes}/service.py | 64 ++-- .../__init__.py | 0 .../dto.py | 0 .../models.py | 12 +- .../routes.py | 2 +- .../service.py | 40 +- backend/api/v1/modules/a76/company/routes.py | 2 +- .../modules/a76/{GParts => parts}/__init__.py | 0 .../v1/modules/a76/{GParts => parts}/dto.py | 0 .../modules/a76/{GParts => parts}/models.py | 18 +- .../modules/a76/{GParts => parts}/routes.py | 2 +- .../modules/a76/{GParts => parts}/service.py | 74 ++-- backend/api/v1/modules/a76/router.py | 9 + docs/MODULOS_A76_IMPLEMENTADOS.md | 14 +- docs/RELATIONSHIPS.md | 36 +- docs/SCHEMA_A76_UPDATE.md | 22 +- 24 files changed, 169 insertions(+), 516 deletions(-) delete mode 100644 backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py rename backend/api/v1/modules/a76/{GClass => classes}/__init__.py (76%) rename backend/api/v1/modules/a76/{GClass => classes}/dto.py (100%) rename backend/api/v1/modules/a76/{GClass => classes}/models.py (82%) rename backend/api/v1/modules/a76/{GClass => classes}/routes.py (100%) rename backend/api/v1/modules/a76/{GClass => classes}/service.py (79%) rename backend/api/v1/modules/a76/{client_&_provider => client_and_provider}/__init__.py (100%) rename backend/api/v1/modules/a76/{client_&_provider => client_and_provider}/dto.py (100%) rename backend/api/v1/modules/a76/{client_&_provider => client_and_provider}/models.py (89%) rename backend/api/v1/modules/a76/{client_&_provider => client_and_provider}/routes.py (98%) rename backend/api/v1/modules/a76/{client_&_provider => client_and_provider}/service.py (87%) rename backend/api/v1/modules/a76/{GParts => parts}/__init__.py (100%) rename backend/api/v1/modules/a76/{GParts => parts}/dto.py (100%) rename backend/api/v1/modules/a76/{GParts => parts}/models.py (85%) rename backend/api/v1/modules/a76/{GParts => parts}/routes.py (99%) rename backend/api/v1/modules/a76/{GParts => parts}/service.py (81%) diff --git a/.gitignore b/.gitignore index 14c07767..0af971f5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ downloads/ eggs/ .eggs/ lib64/ -parts/ sdist/ var/ wheels/ diff --git a/backend/alembic/versions/03b786378f94_pedimentos.py b/backend/alembic/versions/03b786378f94_pedimentos.py index d760d4b8..be9283e3 100644 --- a/backend/alembic/versions/03b786378f94_pedimentos.py +++ b/backend/alembic/versions/03b786378f94_pedimentos.py @@ -13,7 +13,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '03b786378f94' -down_revision: Union[str, Sequence[str], None] = '7937209f9718' +down_revision: Union[str, Sequence[str], None] = '54f2046774d0' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py b/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py index 01ae5756..272f1821 100644 --- a/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py +++ b/backend/alembic/versions/54f2046774d0_create_new_a76_tables_only.py @@ -22,7 +22,7 @@ def upgrade() -> None: """Upgrade schema - Create only new A76 tables.""" # Create new A76 tables only (skip existing tenants, licenses, license_usage) - op.create_table('gclient_provider', + op.create_table('client_provider', sa.Column('client_id', sa.String(length=8), nullable=False), sa.Column('type_nat_foreign', sa.String(length=1), nullable=True), sa.Column('name', sa.String(length=256), nullable=True), @@ -81,7 +81,7 @@ def upgrade() -> None: schema='a76' ) - op.create_table('gclasses', + op.create_table('classes', sa.Column('client_key', sa.Integer(), nullable=False), sa.Column('class_code', sa.String(length=8), nullable=False), sa.Column('description_spanish', sa.String(length=500), nullable=True), @@ -98,7 +98,7 @@ def upgrade() -> None: schema='a76' ) - op.create_table('gparts', + op.create_table('parts', sa.Column('client_key', sa.Integer(), nullable=False), sa.Column('part_number', sa.String(length=49), nullable=False), sa.Column('fraction', sa.String(length=10), nullable=True), @@ -151,7 +151,7 @@ def upgrade() -> None: sa.Column('email', sa.String(length=100), nullable=True), sa.Column('contact', sa.String(length=50), nullable=True), sa.Column('reference', sa.String(length=250), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('client_id'), schema='a76' ) @@ -178,7 +178,7 @@ def upgrade() -> None: sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), sa.Column('autse_dates', sa.Integer(), nullable=True), sa.Column('autse_number', sa.String(length=300), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('client_id'), schema='a76' ) @@ -189,7 +189,7 @@ def downgrade() -> None: # Drop tables in reverse dependency order op.drop_table('gclient_provider_programs', schema='a76') op.drop_table('gclient_provider_address', schema='a76') - op.drop_table('gparts', schema='a76') - op.drop_table('gclasses', schema='a76') + op.drop_table('parts', schema='a76') + op.drop_table('classes', schema='a76') op.drop_table('gcompany', schema='a76') - op.drop_table('gclient_provider', schema='a76') \ No newline at end of file + op.drop_table('client_provider', schema='a76') \ No newline at end of file diff --git a/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py b/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py deleted file mode 100644 index 07992a28..00000000 --- a/backend/alembic/versions/eb8a17e5fbde_create_a76_tables_company_clients_parts_.py +++ /dev/null @@ -1,355 +0,0 @@ -"""create_a76_tables_company_clients_parts_classes - -Revision ID: eb8a17e5fbde -Revises: 7937209f9718 -Create Date: 2025-11-06 03:15:17.248159 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'eb8a17e5fbde' -down_revision: Union[str, Sequence[str], None] = '7937209f9718' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema - Create new A76 tables only.""" - - # Crear tabla gcompany - op.create_table('gcompany', - sa.Column('id', sa.String(length=3), nullable=False), - sa.Column('consecutive', sa.Boolean(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=True), - sa.Column('rfc', sa.String(length=30), nullable=True), - sa.Column('main_activity', sa.String(length=255), nullable=True), - sa.Column('program', sa.String(length=10), nullable=True), - sa.Column('program_number', sa.String(length=40), nullable=True), - sa.Column('prosec', sa.SmallInteger(), nullable=True), - sa.Column('prosec_authorization', sa.String(length=20), nullable=True), - sa.Column('manufacturer_id', sa.String(length=25), nullable=True), - sa.Column('broker_company', sa.String(length=10), nullable=True), - sa.Column('responsible', sa.String(length=80), nullable=True), - sa.Column('responsible_name', sa.String(length=20), nullable=True), - sa.Column('responsible_last_name', sa.String(length=20), nullable=True), - sa.Column('responsible_mother_last_name', sa.String(length=20), nullable=True), - sa.Column('responsible_rfc', sa.String(length=30), nullable=True), - sa.Column('position', sa.String(length=30), nullable=True), - sa.Column('logo', sa.String(length=255), nullable=True), - sa.Column('has_express_line', sa.Boolean(), nullable=True), - sa.Column('order_format_type', sa.String(length=19), nullable=True), - sa.Column('previous_code', sa.SmallInteger(), nullable=True), - sa.Column('is_service_company', sa.Boolean(), nullable=True), - sa.Column('client_name', sa.String(length=300), nullable=True), - sa.Column('subassembly_mode', sa.String(length=7), nullable=True), - sa.Column('curp', sa.String(length=19), nullable=True), - sa.Column('inter_db_name', sa.String(length=100), nullable=True), - sa.Column('ctpat_svi', sa.String(length=100), nullable=True), - sa.Column('trusted_exporter_number', sa.String(length=50), nullable=True), - sa.Column('prevalidator_key', sa.String(length=20), nullable=True), - sa.Column('seventh_amendment', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('consecutive'), - schema='a76' - ) - - # Crear tabla gclient_provider - op.create_table('gclient_provider', - sa.Column('client_id', sa.String(length=8), nullable=False), - sa.Column('type_nat_foreign', sa.String(length=1), nullable=True), - sa.Column('name', sa.String(length=256), nullable=True), - sa.Column('short_name', sa.String(length=10), nullable=True), - sa.Column('rfc', sa.String(length=30), nullable=True), - sa.Column('curp', sa.String(length=19), nullable=True), - sa.Column('client_or_provider', sa.String(length=1), nullable=True), - sa.Column('linking', sa.String(length=1), nullable=True), - sa.Column('transform_subassembly', sa.String(length=1), nullable=True), - sa.Column('extra_information', sa.String(length=399), nullable=True), - sa.Column('web_key', sa.String(length=40), nullable=True), - sa.Column('responsible', sa.String(length=80), nullable=True), - sa.Column('position', sa.String(length=30), nullable=True), - sa.Column('incoterm', sa.String(length=19), nullable=True), - sa.Column('is_national_provider', sa.String(length=2), nullable=True), - sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), - sa.PrimaryKeyConstraint('client_id'), - schema='a76' - ) - - # Crear tabla gclasses - op.create_table('gclasses', - sa.Column('client_key', sa.Integer(), nullable=False), - sa.Column('class_code', sa.String(length=8), nullable=False), - sa.Column('description_spanish', sa.String(length=500), nullable=True), - sa.Column('description_english', sa.String(length=500), nullable=True), - sa.Column('material_key', sa.String(length=10), nullable=True), - sa.Column('unit_of_measure', sa.String(length=5), nullable=True), - sa.Column('fraction', sa.String(length=10), nullable=True), - sa.Column('us_fraction', sa.String(length=16), nullable=True), - sa.Column('sub_key', sa.String(length=5), nullable=True), - sa.Column('physical_review', sa.SmallInteger(), nullable=True), - sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True), - sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ), - sa.PrimaryKeyConstraint('client_key', 'class_code'), - schema='a76' - ) - - # Crear tabla gparts - op.create_table('gparts', - sa.Column('client_key', sa.Integer(), nullable=False), - sa.Column('part_number', sa.String(length=49), nullable=False), - sa.Column('fraction', sa.String(length=10), nullable=True), - sa.Column('description_spanish', sa.String(length=500), nullable=True), - sa.Column('description_english', sa.String(length=500), nullable=True), - sa.Column('part_class', sa.String(length=8), nullable=True), - sa.Column('unit_of_measure', sa.String(length=5), nullable=True), - sa.Column('commercial_part_number', sa.String(length=70), nullable=True), - sa.Column('country_of_origin', sa.String(length=3), nullable=True), - sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True), - sa.Column('currency_type', sa.String(length=2), nullable=True), - sa.Column('currency_key', sa.String(length=3), nullable=True), - sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True), - sa.Column('weight_type', sa.String(length=6), nullable=True), - sa.Column('us_fraction', sa.String(length=16), nullable=True), - sa.Column('fda_key', sa.String(length=20), nullable=True), - sa.Column('fcc_key', sa.String(length=30), nullable=True), - sa.Column('license_code', sa.String(length=3), nullable=True), - sa.Column('eccn', sa.String(length=20), nullable=True), - sa.Column('export_code', sa.String(length=2), nullable=True), - sa.Column('exclusion_symbol', sa.String(length=19), nullable=True), - sa.Column('supplier', sa.String(length=14), nullable=True), - sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True), - sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True), - sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), - sa.Column('creation_date', sa.Integer(), nullable=True), - sa.Column('modification_date', sa.Integer(), nullable=True), - sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True), - sa.Column('part_photo', sa.String(length=255), nullable=True), - sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ), - sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ), - sa.PrimaryKeyConstraint('client_key', 'part_number'), - schema='a76' - ) - - # Crear tabla gclient_provider_address - op.create_table('gclient_provider_address', - sa.Column('client_id', sa.String(length=8), nullable=False), - sa.Column('municipality', sa.String(length=150), nullable=True), - sa.Column('streets', sa.String(length=100), nullable=True), - sa.Column('neighborhood', sa.String(length=40), nullable=True), - sa.Column('interior_number', sa.String(length=20), nullable=True), - sa.Column('exterior_number', sa.String(length=20), nullable=True), - sa.Column('postal_code', sa.String(length=15), nullable=True), - sa.Column('city', sa.String(length=30), nullable=True), - sa.Column('state', sa.String(length=30), nullable=True), - sa.Column('country', sa.String(length=3), nullable=True), - sa.Column('phone', sa.String(length=30), nullable=True), - sa.Column('fax_number', sa.String(length=30), nullable=True), - sa.Column('email', sa.String(length=100), nullable=True), - sa.Column('contact', sa.String(length=50), nullable=True), - sa.Column('reference', sa.String(length=250), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('client_id'), - schema='a76' - ) - - # Crear tabla gclient_provider_programs - op.create_table('gclient_provider_programs', - sa.Column('client_id', sa.String(length=8), nullable=False), - sa.Column('program', sa.String(length=7), nullable=True), - sa.Column('program_number', sa.String(length=40), nullable=True), - sa.Column('prosec', sa.SmallInteger(), nullable=True), - sa.Column('prosec_authorization', sa.String(length=20), nullable=True), - sa.Column('secon_auth_date', sa.Integer(), nullable=True), - sa.Column('manufacturer_id', sa.String(length=25), nullable=True), - sa.Column('tax_id', sa.String(length=30), nullable=True), - sa.Column('broker', sa.String(length=6), nullable=True), - sa.Column('import_broker', sa.String(length=6), nullable=True), - sa.Column('transfer_key', sa.String(length=8), nullable=True), - sa.Column('secon_authorization', sa.String(length=20), nullable=True), - sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True), - sa.Column('is_certified_company', sa.String(length=1), nullable=True), - sa.Column('certified_company_registry', sa.String(length=40), nullable=True), - sa.Column('donation_auth_number', sa.String(length=50), nullable=True), - sa.Column('ctpat_svi', sa.String(length=100), nullable=True), - sa.Column('tax_registry_number', sa.String(length=40), nullable=True), - sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), - sa.Column('autse_dates', sa.Integer(), nullable=True), - sa.Column('autse_number', sa.String(length=300), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('client_id'), - schema='a76' - ) - op.create_table('gclasses', - sa.Column('client_key', sa.Integer(), nullable=False), - sa.Column('class_code', sa.String(length=8), nullable=False), - sa.Column('description_spanish', sa.String(length=500), nullable=True), - sa.Column('description_english', sa.String(length=500), nullable=True), - sa.Column('material_key', sa.String(length=10), nullable=True), - sa.Column('unit_of_measure', sa.String(length=5), nullable=True), - sa.Column('fraction', sa.String(length=10), nullable=True), - sa.Column('us_fraction', sa.String(length=16), nullable=True), - sa.Column('sub_key', sa.String(length=5), nullable=True), - sa.Column('physical_review', sa.SmallInteger(), nullable=True), - sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True), - sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ), - sa.PrimaryKeyConstraint('client_key', 'class_code'), - schema='a76' - ) - op.create_table('gclient_provider_address', - sa.Column('client_id', sa.String(length=8), nullable=False), - sa.Column('municipality', sa.String(length=150), nullable=True), - sa.Column('streets', sa.String(length=100), nullable=True), - sa.Column('neighborhood', sa.String(length=40), nullable=True), - sa.Column('interior_number', sa.String(length=20), nullable=True), - sa.Column('exterior_number', sa.String(length=20), nullable=True), - sa.Column('postal_code', sa.String(length=15), nullable=True), - sa.Column('city', sa.String(length=30), nullable=True), - sa.Column('state', sa.String(length=30), nullable=True), - sa.Column('country', sa.String(length=3), nullable=True), - sa.Column('phone', sa.String(length=30), nullable=True), - sa.Column('fax_number', sa.String(length=30), nullable=True), - sa.Column('email', sa.String(length=100), nullable=True), - sa.Column('contact', sa.String(length=50), nullable=True), - sa.Column('reference', sa.String(length=250), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('client_id'), - schema='a76' - ) - op.create_table('gclient_provider_programs', - sa.Column('client_id', sa.String(length=8), nullable=False), - sa.Column('program', sa.String(length=7), nullable=True), - sa.Column('program_number', sa.String(length=40), nullable=True), - sa.Column('prosec', sa.SmallInteger(), nullable=True), - sa.Column('prosec_authorization', sa.String(length=20), nullable=True), - sa.Column('secon_auth_date', sa.Integer(), nullable=True), - sa.Column('manufacturer_id', sa.String(length=25), nullable=True), - sa.Column('tax_id', sa.String(length=30), nullable=True), - sa.Column('broker', sa.String(length=6), nullable=True), - sa.Column('import_broker', sa.String(length=6), nullable=True), - sa.Column('transfer_key', sa.String(length=8), nullable=True), - sa.Column('secon_authorization', sa.String(length=20), nullable=True), - sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True), - sa.Column('is_certified_company', sa.String(length=1), nullable=True), - sa.Column('certified_company_registry', sa.String(length=40), nullable=True), - sa.Column('donation_auth_number', sa.String(length=50), nullable=True), - sa.Column('ctpat_svi', sa.String(length=100), nullable=True), - sa.Column('tax_registry_number', sa.String(length=40), nullable=True), - sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), - sa.Column('autse_dates', sa.Integer(), nullable=True), - sa.Column('autse_number', sa.String(length=300), nullable=True), - sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('client_id'), - schema='a76' - ) - op.create_table('gparts', - sa.Column('client_key', sa.Integer(), nullable=False), - sa.Column('part_number', sa.String(length=49), nullable=False), - sa.Column('fraction', sa.String(length=10), nullable=True), - sa.Column('description_spanish', sa.String(length=500), nullable=True), - sa.Column('description_english', sa.String(length=500), nullable=True), - sa.Column('part_class', sa.String(length=8), nullable=True), - sa.Column('unit_of_measure', sa.String(length=5), nullable=True), - sa.Column('commercial_part_number', sa.String(length=70), nullable=True), - sa.Column('country_of_origin', sa.String(length=3), nullable=True), - sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True), - sa.Column('currency_type', sa.String(length=2), nullable=True), - sa.Column('currency_key', sa.String(length=3), nullable=True), - sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True), - sa.Column('weight_type', sa.String(length=6), nullable=True), - sa.Column('us_fraction', sa.String(length=16), nullable=True), - sa.Column('fda_key', sa.String(length=20), nullable=True), - sa.Column('fcc_key', sa.String(length=30), nullable=True), - sa.Column('license_code', sa.String(length=3), nullable=True), - sa.Column('eccn', sa.String(length=20), nullable=True), - sa.Column('export_code', sa.String(length=2), nullable=True), - sa.Column('exclusion_symbol', sa.String(length=19), nullable=True), - sa.Column('supplier', sa.String(length=14), nullable=True), - sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True), - sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True), - sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True), - sa.Column('creation_date', sa.Integer(), nullable=True), - sa.Column('modification_date', sa.Integer(), nullable=True), - sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True), - sa.Column('part_photo', sa.String(length=255), nullable=True), - sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ), - sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ), - sa.PrimaryKeyConstraint('client_key', 'part_number'), - schema='a76' - ) - op.create_table('license_usage', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('tenant_id', sa.Integer(), nullable=False), - sa.Column('period_start', sa.DateTime(timezone=True), nullable=False), - sa.Column('period_end', sa.DateTime(timezone=True), nullable=False), - sa.Column('active_users', sa.Integer(), nullable=True), - sa.Column('storage_used_gb', sa.Integer(), nullable=True), - sa.Column('operations_count', sa.Integer(), nullable=True), - sa.Column('api_calls_count', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='a76' - ) - op.create_index(op.f('ix_a76_license_usage_id'), 'license_usage', ['id'], unique=False, schema='a76') - op.create_index(op.f('ix_a76_license_usage_tenant_id'), 'license_usage', ['tenant_id'], unique=False, schema='a76') - op.create_table('licenses', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('tenant_id', sa.Integer(), nullable=False), - sa.Column('plan', sa.Enum('FREE', 'BASIC', 'PROFESSIONAL', 'ENTERPRISE', name='licenseplan'), nullable=False), - sa.Column('status', sa.Enum('ACTIVE', 'EXPIRED', 'SUSPENDED', 'PENDING', 'CANCELLED', name='licensestatus'), nullable=False), - sa.Column('max_users', sa.Integer(), nullable=False), - sa.Column('max_storage_gb', sa.Integer(), nullable=False), - sa.Column('max_monthly_operations', sa.Integer(), nullable=False), - sa.Column('feature_api_access', sa.Boolean(), nullable=True), - sa.Column('feature_advanced_reports', sa.Boolean(), nullable=True), - sa.Column('feature_integrations', sa.Boolean(), nullable=True), - sa.Column('feature_dedicated_support', sa.Boolean(), nullable=True), - sa.Column('starts_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='a76' - ) - op.create_index(op.f('ix_a76_licenses_id'), 'licenses', ['id'], unique=False, schema='a76') - op.create_index(op.f('ix_a76_licenses_tenant_id'), 'licenses', ['tenant_id'], unique=True, schema='a76') - op.drop_constraint(op.f('fk_regimenped'), 'code_pedimento_regimens', type_='foreignkey') - op.drop_constraint(op.f('fk_codeped'), 'code_pedimento_regimens', type_='foreignkey') - op.create_foreign_key('fk_regimenped', 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'], source_schema='public', referent_schema='public') - op.create_foreign_key('fk_codeped', 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'], source_schema='public', referent_schema='public') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('fk_codeped', 'code_pedimento_regimens', schema='public', type_='foreignkey') - op.drop_constraint('fk_regimenped', 'code_pedimento_regimens', schema='public', type_='foreignkey') - op.create_foreign_key(op.f('fk_codeped'), 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code']) - op.create_foreign_key(op.f('fk_regimenped'), 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code']) - op.drop_index(op.f('ix_a76_licenses_tenant_id'), table_name='licenses', schema='a76') - op.drop_index(op.f('ix_a76_licenses_id'), table_name='licenses', schema='a76') - op.drop_table('licenses', schema='a76') - op.drop_index(op.f('ix_a76_license_usage_tenant_id'), table_name='license_usage', schema='a76') - op.drop_index(op.f('ix_a76_license_usage_id'), table_name='license_usage', schema='a76') - op.drop_table('license_usage', schema='a76') - op.drop_table('gparts', schema='a76') - op.drop_table('gclient_provider_programs', schema='a76') - op.drop_table('gclient_provider_address', schema='a76') - op.drop_table('gclasses', schema='a76') - op.drop_index(op.f('ix_a76_tenants_slug'), table_name='tenants', schema='a76') - op.drop_index(op.f('ix_a76_tenants_name'), table_name='tenants', schema='a76') - op.drop_index(op.f('ix_a76_tenants_id'), table_name='tenants', schema='a76') - op.drop_table('tenants', schema='a76') - op.drop_table('gcompany', schema='a76') - op.drop_table('gclient_provider', schema='a76') - # ### end Alembic commands ### diff --git a/backend/api/v1/modules/a76/GClass/__init__.py b/backend/api/v1/modules/a76/classes/__init__.py similarity index 76% rename from backend/api/v1/modules/a76/GClass/__init__.py rename to backend/api/v1/modules/a76/classes/__init__.py index 09dc90d1..8d0a42ce 100644 --- a/backend/api/v1/modules/a76/GClass/__init__.py +++ b/backend/api/v1/modules/a76/classes/__init__.py @@ -1,5 +1,5 @@ """ -Módulo de GClass +Módulo de Class """ from .routes import router diff --git a/backend/api/v1/modules/a76/GClass/dto.py b/backend/api/v1/modules/a76/classes/dto.py similarity index 100% rename from backend/api/v1/modules/a76/GClass/dto.py rename to backend/api/v1/modules/a76/classes/dto.py diff --git a/backend/api/v1/modules/a76/GClass/models.py b/backend/api/v1/modules/a76/classes/models.py similarity index 82% rename from backend/api/v1/modules/a76/GClass/models.py rename to backend/api/v1/modules/a76/classes/models.py index 77a6765a..9cd42b53 100644 --- a/backend/api/v1/modules/a76/GClass/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -11,15 +11,15 @@ import enum from typing import TYPE_CHECKING, List, Optional if TYPE_CHECKING: - from api.v1.modules.a76.GParts.models import GPart + from api.v1.modules.a76.parts.models import Part from api.v1.modules.public.reference_data.material_types.models import MaterialType -class GClass(Base): +class Class(Base): """ Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF """ - __tablename__ = "gclasses" + __tablename__ = "classes" __table_args__ = {"schema": "a76"} # Primary key compuesta @@ -48,14 +48,14 @@ class GClass(Base): # Inverse relationship with GParts that have this class parts = relationship( - "GPart", - primaryjoin="and_(GClass.client_key == GPart.client_key, GClass.class_code == GPart.part_class)", - foreign_keys="[GPart.client_key, GPart.part_class]", + "Part", + primaryjoin="and_(Class.client_key == Part.client_key, Class.class_code == Part.part_class)", + foreign_keys="[Part.client_key, Part.part_class]", viewonly=True, back_populates="part_class_info" ) def __repr__(self): - return f"" + return f"" diff --git a/backend/api/v1/modules/a76/GClass/routes.py b/backend/api/v1/modules/a76/classes/routes.py similarity index 100% rename from backend/api/v1/modules/a76/GClass/routes.py rename to backend/api/v1/modules/a76/classes/routes.py diff --git a/backend/api/v1/modules/a76/GClass/service.py b/backend/api/v1/modules/a76/classes/service.py similarity index 79% rename from backend/api/v1/modules/a76/GClass/service.py rename to backend/api/v1/modules/a76/classes/service.py index ceed5c56..6a11895b 100644 --- a/backend/api/v1/modules/a76/GClass/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -8,7 +8,7 @@ from fastapi import HTTPException from typing import List, Optional import logging -from .models import GClass +from .models import Class from .dto import ( ClassCreateDTO, ClassUpdateDTO, @@ -42,10 +42,10 @@ class ClassService: """ try: # Verificar que no exista la clase - existing = self.db.query(GClass).filter( + existing = self.db.query(Class).filter( and_( - GClass.client_key == class_data.client_key, - GClass.class_code == class_data.class_code + Class.client_key == class_data.client_key, + Class.class_code == class_data.class_code ) ).first() @@ -56,7 +56,7 @@ class ClassService: ) # Crear clase - db_class = GClass( + db_class = Class( client_key=class_data.client_key, class_code=class_data.class_code, description_spanish=class_data.description_spanish, @@ -100,10 +100,10 @@ class ClassService: Returns: ClassResponseDTO o None si no existe """ - class_obj = self.db.query(GClass).filter( + class_obj = self.db.query(Class).filter( and_( - GClass.client_key == client_key, - GClass.class_code == class_code + Class.client_key == client_key, + Class.class_code == class_code ) ).first() @@ -128,33 +128,33 @@ class ClassService: Returns: ClassListDTO con la lista paginada """ - query = self.db.query(GClass) + query = self.db.query(Class) # Aplicar filtros si se proporcionan if search_params: if search_params.client_key: - query = query.filter(GClass.client_key == search_params.client_key) + query = query.filter(Class.client_key == search_params.client_key) if search_params.class_code: - query = query.filter(GClass.class_code.ilike(f"%{search_params.class_code}%")) + query = query.filter(Class.class_code.ilike(f"%{search_params.class_code}%")) if search_params.description: description_pattern = f"%{search_params.description}%" query = query.filter( or_( - GClass.description_spanish.ilike(description_pattern), - GClass.description_english.ilike(description_pattern) + Class.description_spanish.ilike(description_pattern), + Class.description_english.ilike(description_pattern) ) ) if search_params.material_key: - query = query.filter(GClass.material_key.ilike(f"%{search_params.material_key}%")) + query = query.filter(Class.material_key.ilike(f"%{search_params.material_key}%")) if search_params.fraction: - query = query.filter(GClass.fraction.ilike(f"%{search_params.fraction}%")) + query = query.filter(Class.fraction.ilike(f"%{search_params.fraction}%")) if search_params.physical_review is not None: - query = query.filter(GClass.physical_review == search_params.physical_review) + query = query.filter(Class.physical_review == search_params.physical_review) # Contar total total = query.count() @@ -184,10 +184,10 @@ class ClassService: Returns: ClassResponseDTO actualizado o None si no existe """ - class_obj = self.db.query(GClass).filter( + class_obj = self.db.query(Class).filter( and_( - GClass.client_key == client_key, - GClass.class_code == class_code + Class.client_key == client_key, + Class.class_code == class_code ) ).first() @@ -222,10 +222,10 @@ class ClassService: Returns: True si se eliminó, False si no existe """ - class_obj = self.db.query(GClass).filter( + class_obj = self.db.query(Class).filter( and_( - GClass.client_key == client_key, - GClass.class_code == class_code + Class.client_key == client_key, + Class.class_code == class_code ) ).first() @@ -244,40 +244,40 @@ class ClassService: def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: """Busca clases por fracción arancelaria""" - classes = self.db.query(GClass).filter(GClass.fraction.ilike(f"%{fraction}%")).all() + classes = self.db.query(Class).filter(Class.fraction.ilike(f"%{fraction}%")).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] def search_by_client(self, client_key: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: """Obtiene todas las clases de un cliente específico""" - classes = self.db.query(GClass).filter(GClass.client_key == client_key).offset(skip).limit(limit).all() + classes = self.db.query(Class).filter(Class.client_key == client_key).offset(skip).limit(limit).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: """Busca clases por clave de material""" - classes = self.db.query(GClass).filter(GClass.material_key.ilike(f"%{material_key}%")).all() + classes = self.db.query(Class).filter(Class.material_key.ilike(f"%{material_key}%")).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] def get_classes_by_physical_review(self, physical_review: int) -> List[ClassBasicDTO]: """Obtiene clases por indicador de revisión física""" - classes = self.db.query(GClass).filter(GClass.physical_review == physical_review).all() + classes = self.db.query(Class).filter(Class.physical_review == physical_review).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] def get_classes_statistics(self) -> dict: """Obtiene estadísticas básicas de clases""" - total_classes = self.db.query(GClass).count() + total_classes = self.db.query(Class).count() # Contar por clientes - clients_count = self.db.query(GClass.client_key).distinct().count() + clients_count = self.db.query(Class.client_key).distinct().count() # Contar por revisión física physical_review_stats = {} for i in range(3): # Asumiendo valores 0, 1, 2 - count = self.db.query(GClass).filter(GClass.physical_review == i).count() + count = self.db.query(Class).filter(Class.physical_review == i).count() physical_review_stats[f"physical_review_{i}"] = count # Contar clases con fracciones - with_fraction = self.db.query(GClass).filter(GClass.fraction.isnot(None)).count() - with_us_fraction = self.db.query(GClass).filter(GClass.us_fraction.isnot(None)).count() + with_fraction = self.db.query(Class).filter(Class.fraction.isnot(None)).count() + with_us_fraction = self.db.query(Class).filter(Class.us_fraction.isnot(None)).count() return { "total_classes": total_classes, @@ -289,6 +289,6 @@ class ClassService: def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]: """Obtiene clases por unidad de medida""" - classes = self.db.query(GClass).filter(GClass.unit_of_measure == unit_of_measure).all() + classes = self.db.query(Class).filter(Class.unit_of_measure == unit_of_measure).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] diff --git a/backend/api/v1/modules/a76/client_&_provider/__init__.py b/backend/api/v1/modules/a76/client_and_provider/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/client_&_provider/__init__.py rename to backend/api/v1/modules/a76/client_and_provider/__init__.py diff --git a/backend/api/v1/modules/a76/client_&_provider/dto.py b/backend/api/v1/modules/a76/client_and_provider/dto.py similarity index 100% rename from backend/api/v1/modules/a76/client_&_provider/dto.py rename to backend/api/v1/modules/a76/client_and_provider/dto.py diff --git a/backend/api/v1/modules/a76/client_&_provider/models.py b/backend/api/v1/modules/a76/client_and_provider/models.py similarity index 89% rename from backend/api/v1/modules/a76/client_&_provider/models.py rename to backend/api/v1/modules/a76/client_and_provider/models.py index 14834953..af4df115 100644 --- a/backend/api/v1/modules/a76/client_&_provider/models.py +++ b/backend/api/v1/modules/a76/client_and_provider/models.py @@ -8,11 +8,11 @@ from core.database import Base import enum -class GClientProvider(Base): +class ClientProvider(Base): """ Modelo para la tabla GClientesPro - Información de clientes y proveedores """ - __tablename__ = "gclient_provider" + __tablename__ = "client_provider" __table_args__ = {"schema": "a76"} # Primary key @@ -48,7 +48,7 @@ class GClientProviderAddress(Base): __table_args__ = {"schema": "a76"} # Primary key (foreign key) - client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + client_id = Column(String(8), ForeignKey('a76.client_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) # Address information municipality = Column(String(150), nullable=True) @@ -67,7 +67,7 @@ class GClientProviderAddress(Base): reference = Column(String(250), nullable=True) # Relationship - client_provider = relationship("GClientProvider", back_populates="address") + client_provider = relationship("ClientProvider", back_populates="address") class GClientProviderPrograms(Base): @@ -78,7 +78,7 @@ class GClientProviderPrograms(Base): __table_args__ = {"schema": "a76"} # Primary key (foreign key) - client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) + client_id = Column(String(8), ForeignKey('a76.client_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) # Program information program = Column(String(7), nullable=True) @@ -103,6 +103,6 @@ class GClientProviderPrograms(Base): autse_number = Column(String(300), nullable=True) # Relationship - client_provider = relationship("GClientProvider", back_populates="programs") + client_provider = relationship("ClientProvider", back_populates="programs") diff --git a/backend/api/v1/modules/a76/client_&_provider/routes.py b/backend/api/v1/modules/a76/client_and_provider/routes.py similarity index 98% rename from backend/api/v1/modules/a76/client_&_provider/routes.py rename to backend/api/v1/modules/a76/client_and_provider/routes.py index fb9edd50..02d215b8 100644 --- a/backend/api/v1/modules/a76/client_&_provider/routes.py +++ b/backend/api/v1/modules/a76/client_and_provider/routes.py @@ -16,7 +16,7 @@ from .dto import ( ClientProviderListDTO ) -router = APIRouter(prefix="/clients-providers", tags=["Clients & Providers"]) +router = APIRouter(prefix="/clients-providers") @router.post("/", response_model=ClientProviderResponseDTO, status_code=status.HTTP_201_CREATED) diff --git a/backend/api/v1/modules/a76/client_&_provider/service.py b/backend/api/v1/modules/a76/client_and_provider/service.py similarity index 87% rename from backend/api/v1/modules/a76/client_&_provider/service.py rename to backend/api/v1/modules/a76/client_and_provider/service.py index a9573c10..99340da0 100644 --- a/backend/api/v1/modules/a76/client_&_provider/service.py +++ b/backend/api/v1/modules/a76/client_and_provider/service.py @@ -8,7 +8,7 @@ from fastapi import HTTPException from typing import List, Optional import logging -from .models import GClientProvider, GClientProviderAddress, GClientProviderPrograms +from .models import ClientProvider, GClientProviderAddress, GClientProviderPrograms from .dto import ( ClientProviderCreateDTO, ClientProviderUpdateDTO, @@ -43,12 +43,12 @@ class ClientProviderService: """ try: # Verificar que no exista el cliente - existing = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_data.client_id).first() + existing = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_data.client_id).first() if existing: raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists") # Crear cliente/proveedor principal - db_client = GClientProvider( + db_client = ClientProvider( client_id=client_data.client_id, type_nat_foreign=client_data.type_nat_foreign, name=client_data.name, @@ -118,10 +118,10 @@ class ClientProviderService: def _get_client_with_relations(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """Método privado para obtener cliente con relaciones""" - client = self.db.query(GClientProvider).options( - joinedload(GClientProvider.address), - joinedload(GClientProvider.programs) - ).filter(GClientProvider.client_id == client_id).first() + client = self.db.query(ClientProvider).options( + joinedload(ClientProvider.address), + joinedload(ClientProvider.programs) + ).filter(ClientProvider.client_id == client_id).first() if not client: return None @@ -148,25 +148,25 @@ class ClientProviderService: Returns: ClientProviderListDTO con la lista paginada """ - query = self.db.query(GClientProvider) + query = self.db.query(ClientProvider) # Aplicar filtros if search: search_pattern = f"%{search}%" query = query.filter( or_( - GClientProvider.name.ilike(search_pattern), - GClientProvider.short_name.ilike(search_pattern), - GClientProvider.rfc.ilike(search_pattern), - GClientProvider.client_id.ilike(search_pattern) + ClientProvider.name.ilike(search_pattern), + ClientProvider.short_name.ilike(search_pattern), + ClientProvider.rfc.ilike(search_pattern), + ClientProvider.client_id.ilike(search_pattern) ) ) if client_or_provider: - query = query.filter(GClientProvider.client_or_provider == client_or_provider) + query = query.filter(ClientProvider.client_or_provider == client_or_provider) if enabled_only: - query = query.filter(GClientProvider.enabled_disabled == 1) + query = query.filter(ClientProvider.enabled_disabled == 1) # Contar total total = query.count() @@ -195,7 +195,7 @@ class ClientProviderService: Returns: ClientProviderResponseDTO actualizado o None si no existe """ - client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return None @@ -257,7 +257,7 @@ class ClientProviderService: Returns: True si se eliminó, False si no existe """ - client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return False @@ -273,24 +273,24 @@ class ClientProviderService: def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: """Obtiene solo clientes (C)""" - query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'C') + query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'C') clients = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: """Obtiene solo proveedores (P)""" - query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'P') + query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'P') providers = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(provider) for provider in providers] def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]: """Busca clientes/proveedores por RFC""" - clients = self.db.query(GClientProvider).filter(GClientProvider.rfc.ilike(f"%{rfc}%")).all() + clients = self.db.query(ClientProvider).filter(ClientProvider.rfc.ilike(f"%{rfc}%")).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """Cambia el estado habilitado/deshabilitado""" - client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() + client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return None diff --git a/backend/api/v1/modules/a76/company/routes.py b/backend/api/v1/modules/a76/company/routes.py index 4473591d..1b72f7a0 100644 --- a/backend/api/v1/modules/a76/company/routes.py +++ b/backend/api/v1/modules/a76/company/routes.py @@ -10,7 +10,7 @@ from core.security import get_current_user, has_role from .service import CompanyService from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO -router = APIRouter(prefix="/company", tags=["Company"]) +router = APIRouter(prefix="/company") @router.post("/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED) diff --git a/backend/api/v1/modules/a76/GParts/__init__.py b/backend/api/v1/modules/a76/parts/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/GParts/__init__.py rename to backend/api/v1/modules/a76/parts/__init__.py diff --git a/backend/api/v1/modules/a76/GParts/dto.py b/backend/api/v1/modules/a76/parts/dto.py similarity index 100% rename from backend/api/v1/modules/a76/GParts/dto.py rename to backend/api/v1/modules/a76/parts/dto.py diff --git a/backend/api/v1/modules/a76/GParts/models.py b/backend/api/v1/modules/a76/parts/models.py similarity index 85% rename from backend/api/v1/modules/a76/GParts/models.py rename to backend/api/v1/modules/a76/parts/models.py index df58cb04..4e787537 100644 --- a/backend/api/v1/modules/a76/GParts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -13,14 +13,14 @@ from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from api.v1.modules.public.reference_data.countries.models import Country from api.v1.modules.public.reference_data.currency_types.models import CurrencyType - from api.v1.modules.a76.GClass.models import GClass + from api.v1.modules.a76.classes.models import Class -class GPart(Base): +class Part(Base): """ Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) """ - __tablename__ = "gparts" + __tablename__ = "parts" __table_args__ = {"schema": "a76"} # Primary key compuesta @@ -72,17 +72,17 @@ class GPart(Base): country = relationship("Country", foreign_keys=[country_of_origin]) currency = relationship("CurrencyType", foreign_keys=[currency_key]) - # Relationship with GClass through composite foreign key - # Note: This requires both client_key and part_class to match client_key and class_code in GClass + # Relationship with Class through composite foreign key + # Note: This requires both client_key and part_class to match client_key and class_code in Class part_class_info = relationship( - "GClass", - primaryjoin="and_(GPart.client_key == GClass.client_key, GPart.part_class == GClass.class_code)", - foreign_keys="[GPart.client_key, GPart.part_class]", + "Class", + primaryjoin="and_(Part.client_key == Class.client_key, Part.part_class == Class.class_code)", + foreign_keys="[Part.client_key, Part.part_class]", viewonly=True, back_populates="parts" ) def __repr__(self): - return f"" + return f"" diff --git a/backend/api/v1/modules/a76/GParts/routes.py b/backend/api/v1/modules/a76/parts/routes.py similarity index 99% rename from backend/api/v1/modules/a76/GParts/routes.py rename to backend/api/v1/modules/a76/parts/routes.py index 36486b04..4757fbc5 100644 --- a/backend/api/v1/modules/a76/GParts/routes.py +++ b/backend/api/v1/modules/a76/parts/routes.py @@ -17,7 +17,7 @@ from .dto import ( PartSearchDTO ) -router = APIRouter(prefix="/parts", tags=["Parts"]) +router = APIRouter(prefix="/parts") @router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED) diff --git a/backend/api/v1/modules/a76/GParts/service.py b/backend/api/v1/modules/a76/parts/service.py similarity index 81% rename from backend/api/v1/modules/a76/GParts/service.py rename to backend/api/v1/modules/a76/parts/service.py index cdc08dc4..11cbb13f 100644 --- a/backend/api/v1/modules/a76/GParts/service.py +++ b/backend/api/v1/modules/a76/parts/service.py @@ -9,7 +9,7 @@ from typing import List, Optional import logging from datetime import datetime -from .models import GPart +from .models import Part from .dto import PartCreateDTO, PartUpdateDTO logger = logging.getLogger(__name__) @@ -21,12 +21,12 @@ class PartService: """ @staticmethod - def create_part(db: Session, part_data: PartCreateDTO) -> GPart: + def create_part(db: Session, part_data: PartCreateDTO) -> Part: """ Crear una nueva parte """ try: - db_part = GPart(**part_data.model_dump()) + db_part = Part(**part_data.model_dump()) db.add(db_part) db.commit() db.refresh(db_part) @@ -41,15 +41,15 @@ class PartService: raise HTTPException(status_code=500, detail="Error creating part") @staticmethod - def get_part(db: Session, client_key: int, part_number: str) -> Optional[GPart]: + def get_part(db: Session, client_key: int, part_number: str) -> Optional[Part]: """ Obtener una parte por clave de cliente y número de parte """ try: - return db.query(GPart).filter( + return db.query(Part).filter( and_( - GPart.client_key == client_key, - GPart.part_number == part_number + Part.client_key == client_key, + Part.part_number == part_number ) ).first() except Exception as e: @@ -65,29 +65,29 @@ class PartService: client_key: Optional[int] = None, fraction: Optional[str] = None, country_of_origin: Optional[str] = None - ) -> tuple[List[GPart], int]: + ) -> tuple[List[Part], int]: """ Obtener partes con paginación y filtros """ try: - query = db.query(GPart) + query = db.query(Part) # Aplicar filtros if search: query = query.filter(or_( - GPart.description_spanish.ilike(f"%{search}%"), - GPart.description_english.ilike(f"%{search}%"), - GPart.part_number.ilike(f"%{search}%") + Part.description_spanish.ilike(f"%{search}%"), + Part.description_english.ilike(f"%{search}%"), + Part.part_number.ilike(f"%{search}%") )) if client_key is not None: - query = query.filter(GPart.client_key == client_key) + query = query.filter(Part.client_key == client_key) if fraction: - query = query.filter(GPart.fraction == fraction) + query = query.filter(Part.fraction == fraction) if country_of_origin: - query = query.filter(GPart.country_of_origin == country_of_origin) + query = query.filter(Part.country_of_origin == country_of_origin) # Contar total total = query.count() @@ -101,26 +101,26 @@ class PartService: raise HTTPException(status_code=500, detail="Error retrieving parts") @staticmethod - def get_parts_by_client(db: Session, client_key: int) -> List[GPart]: + def get_parts_by_client(db: Session, client_key: int) -> List[Part]: """ Obtener todas las partes de un cliente específico """ try: - return db.query(GPart).filter(GPart.client_key == client_key).all() + return db.query(Part).filter(Part.client_key == client_key).all() except Exception as e: logger.error(f"Error getting parts by client: {e}") raise HTTPException(status_code=500, detail="Error retrieving client parts") @staticmethod - def search_parts_by_fraction(db: Session, fraction: str) -> List[GPart]: + def search_parts_by_fraction(db: Session, fraction: str) -> List[Part]: """ Buscar partes por fracción arancelaria """ try: - return db.query(GPart).filter( + return db.query(Part).filter( or_( - GPart.fraction.ilike(f"%{fraction}%"), - GPart.us_fraction.ilike(f"%{fraction}%") + Part.fraction.ilike(f"%{fraction}%"), + Part.us_fraction.ilike(f"%{fraction}%") ) ).all() except Exception as e: @@ -128,29 +128,29 @@ class PartService: raise HTTPException(status_code=500, detail="Error searching parts by fraction") @staticmethod - def search_parts_by_supplier(db: Session, supplier: str) -> List[GPart]: + def search_parts_by_supplier(db: Session, supplier: str) -> List[Part]: """ Buscar partes por proveedor """ try: - return db.query(GPart).filter(GPart.supplier.ilike(f"%{supplier}%")).all() + return db.query(Part).filter(Part.supplier.ilike(f"%{supplier}%")).all() except Exception as e: logger.error(f"Error searching parts by supplier: {e}") raise HTTPException(status_code=500, detail="Error searching parts by supplier") @staticmethod - def search_parts_by_country(db: Session, country_code: str) -> List[GPart]: + def search_parts_by_country(db: Session, country_code: str) -> List[Part]: """ Buscar partes por país de origen """ try: - return db.query(GPart).filter(GPart.country_of_origin == country_code).all() + return db.query(Part).filter(Part.country_of_origin == country_code).all() except Exception as e: logger.error(f"Error searching parts by country: {e}") raise HTTPException(status_code=500, detail="Error searching parts by country") @staticmethod - def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[GPart]: + def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[Part]: """ Actualizar una parte existente """ @@ -190,7 +190,7 @@ class PartService: raise HTTPException(status_code=500, detail="Error deleting part") @staticmethod - def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[GPart]: + def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[Part]: """ Cambiar el estado habilitado/deshabilitado de una parte """ @@ -216,24 +216,24 @@ class PartService: Obtener estadísticas de partes """ try: - total_parts = db.query(GPart).count() + total_parts = db.query(Part).count() # Partes por cliente parts_by_client = db.query( - GPart.client_key, - func.count(GPart.part_number).label('count') - ).group_by(GPart.client_key).all() + Part.client_key, + func.count(Part.part_number).label('count') + ).group_by(Part.client_key).all() # Partes por país de origen parts_by_country = db.query( - GPart.country_of_origin, - func.count(GPart.part_number).label('count') - ).filter(GPart.country_of_origin.isnot(None))\ - .group_by(GPart.country_of_origin).all() + Part.country_of_origin, + func.count(Part.part_number).label('count') + ).filter(Part.country_of_origin.isnot(None))\ + .group_by(Part.country_of_origin).all() # Partes habilitadas vs deshabilitadas - enabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 1).count() - disabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 0).count() + enabled_parts = db.query(Part).filter(Part.enabled_disabled == 1).count() + disabled_parts = db.query(Part).filter(Part.enabled_disabled == 0).count() return { "total_parts": total_parts, diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 06ebeac3..6d695913 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -9,6 +9,10 @@ from .auth import router as auth_router from .tenants import router as tenants_router from .licenses import router as licenses_router from .pedmientos.router import router as pedimentos_router +from .client_and_provider import router as client_and_provider_router +from .company import router as company_router +from .classes import router as classes_router +from .parts import router as parts_router # Router principal router = APIRouter() @@ -18,3 +22,8 @@ router.include_router(auth_router) router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"]) router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"]) router.include_router(pedimentos_router, prefix="/a76") +router.include_router(client_and_provider_router, prefix="/a76", tags=["a76 / clients and providers"]) +router.include_router(company_router, prefix="/a76", tags=["a76 / company"]) +router.include_router(classes_router, prefix="/a76", tags=["a76 / classes"]) +router.include_router(parts_router, prefix="/a76", tags=["a76 / parts"]) + diff --git a/docs/MODULOS_A76_IMPLEMENTADOS.md b/docs/MODULOS_A76_IMPLEMENTADOS.md index 90dd563c..e76b8329 100644 --- a/docs/MODULOS_A76_IMPLEMENTADOS.md +++ b/docs/MODULOS_A76_IMPLEMENTADOS.md @@ -20,7 +20,7 @@ - Control de inventario y clasificación arancelaria - Información regulatoria y de cumplimiento -### Módulo de Clases (GClass) +### Módulo de Clases (Class) - Clasificaciones para sistemas SCAII y SCAF - Información arancelaria detallada - Gestión de fracciones arancelarias y materiales @@ -30,17 +30,17 @@ ## 🔗 Relaciones de Base de Datos ### Relaciones Principales -- **GPart ↔ GClass**: Relación de clave compuesta (client_key, part_class ↔ class_code) -- **GPart → Country**: Clave foránea a public.countries (country_of_origin) -- **GPart → CurrencyType**: Clave foránea a public.currency_types (currency_key) -- **GClass → MaterialType**: Clave foránea a public.material_types (material_key) +- **Part ↔ Class**: Relación de clave compuesta (client_key, part_class ↔ class_code) +- **Part → Country**: Clave foránea a public.countries (country_of_origin) +- **Part → CurrencyType**: Clave foránea a public.currency_types (currency_key) +- **Class → MaterialType**: Clave foránea a public.material_types (material_key) ### Esquema de Relaciones ``` -GPart (Partes) +Part (Partes) ├── País de origen → Country ├── Tipo de moneda → CurrencyType -└── Información de clase → GClass +└── Información de clase → Class └── Tipo de material → MaterialType ``` diff --git a/docs/RELATIONSHIPS.md b/docs/RELATIONSHIPS.md index 362ca6fe..78895954 100644 --- a/docs/RELATIONSHIPS.md +++ b/docs/RELATIONSHIPS.md @@ -2,8 +2,8 @@ ## Resumen de Relaciones Establecidas -### GPart (Tabla: gparts) -El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI. +### Part (Tabla: parts) +El modelo `Part` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI. #### Relaciones: @@ -17,14 +17,14 @@ El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF - Relación: Many-to-One - Propósito: Tipo de moneda para el costo unitario -3. **Con GClass (gclasses)** +3. **Con Class (classes)** - Campos: `(client_key, part_class)` → `(client_key, class_code)` - Relación: Many-to-One (usando primaryjoin complejo) - Propósito: Clasificación de la parte - Atributo: `part_class_info` -### GClass (Tabla: gclasses) -El modelo `GClass` representa las clases de clasificación en sistemas SCAII y SCAF. +### Class (Tabla: classes) +El modelo `Class` representa las clases de clasificación en sistemas SCAII y SCAF. #### Relaciones: @@ -33,24 +33,24 @@ El modelo `GClass` representa las clases de clasificación en sistemas SCAII y S - Relación: Many-to-One - Propósito: Tipo de material de la clase -2. **Con GPart (gparts)** +2. **Con Part (parts)** - Campos: `(client_key, class_code)` → `(client_key, part_class)` - - Relación: One-to-Many (inversa de la relación en GPart) + - Relación: One-to-Many (inversa de la relación en Part) - Propósito: Partes que pertenecen a esta clase - Atributo: `parts` ## Esquema de Relaciones ``` -GPart +Part ├── country (Country) # País de origen ├── currency (CurrencyType) # Tipo de moneda -└── part_class_info (GClass) # Información de clasificación +└── part_class_info (Class) # Información de clasificación └── material_type (MaterialType) # Tipo de material -GClass +Class ├── material_type (MaterialType) # Tipo de material -└── parts (List[GPart]) # Partes que usan esta clase +└── parts (List[Part]) # Partes que usan esta clase ``` ## Uso de las Relaciones @@ -58,13 +58,13 @@ GClass ### En consultas: ```python # Obtener una parte con su información completa -part = session.query(GPart).options( - joinedload(GPart.country), - joinedload(GPart.currency), - joinedload(GPart.part_class_info).joinedload(GClass.material_type) +part = session.query(Part).options( + joinedload(Part.country), + joinedload(Part.currency), + joinedload(Part.part_class_info).joinedload(Class.material_type) ).filter( - GPart.client_key == 1, - GPart.part_number == "PART001" + Part.client_key == 1, + Part.part_number == "PART001" ).first() # Acceder a los datos relacionados @@ -89,7 +89,7 @@ class PartDetailResponseDTO(BaseModel): ## Consideraciones Técnicas -1. **Composite Foreign Keys**: La relación entre `GPart` y `GClass` usa claves foráneas compuestas que requieren `primaryjoin` personalizado. +1. **Composite Foreign Keys**: La relación entre `Part` y `Class` usa claves foráneas compuestas que requieren `primaryjoin` personalizado. 2. **Viewonly Relationships**: Algunas relaciones están marcadas como `viewonly=True` para evitar problemas de escritura accidental. diff --git a/docs/SCHEMA_A76_UPDATE.md b/docs/SCHEMA_A76_UPDATE.md index 5a3fadba..c4587692 100644 --- a/docs/SCHEMA_A76_UPDATE.md +++ b/docs/SCHEMA_A76_UPDATE.md @@ -13,11 +13,11 @@ Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schem | Módulo | Tabla | Schema | Estado | |--------|-------|---------|---------| | **Company** | `gcompany` | `a76` | ✅ Actualizada | -| **Client & Provider** | `gclient_provider` | `a76` | ✅ Actualizada | +| **Client & Provider** | `client_provider` | `a76` | ✅ Actualizada | | **Client & Provider** | `gclient_provider_address` | `a76` | ✅ Actualizada | | **Client & Provider** | `gclient_provider_programs` | `a76` | ✅ Actualizada | -| **GParts** | `gparts` | `a76` | ✅ Actualizada | -| **GClass** | `gclasses` | `a76` | ✅ Actualizada | +| **GParts** | `parts` | `a76` | ✅ Actualizada | +| **Class** | `classes` | `a76` | ✅ Actualizada | | **Licenses** | `licenses` | `a76` | ✅ Ya estaba | | **Licenses** | `license_usage` | `a76` | ✅ Ya estaba | | **Tenants** | `tenants` | `a76` | ✅ Ya estaba | @@ -39,10 +39,10 @@ class GCompany(Base): #### 2. Foreign Keys Actualizadas ```python # ANTES -client_id = Column(String(8), ForeignKey('gclient_provider.client_id'), ...) +client_id = Column(String(8), ForeignKey('client_provider.client_id'), ...) # DESPUÉS -client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id'), ...) +client_id = Column(String(8), ForeignKey('a76.client_provider.client_id'), ...) ``` ### 🏗️ Estructura de Schemas @@ -60,11 +60,11 @@ PostgreSQL Database ├── licenses ├── license_usage ├── gcompany - ├── gclient_provider + ├── client_provider ├── gclient_provider_address ├── gclient_provider_programs - ├── gparts - └── gclasses + ├── parts + └── classes ``` ### 🔗 Relaciones Mantenidas @@ -77,13 +77,13 @@ Las relaciones entre schemas funcionan correctamente: #### Ejemplos de Relaciones Cross-Schema: ```python -# GPart (a76) → Country (public) +# Part (a76) → Country (public) country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key')) -# GPart (a76) → CurrencyType (public) +# Part (a76) → CurrencyType (public) currency_key = Column(String(3), ForeignKey('public.currency_types.code')) -# GClass (a76) → MaterialType (public) +# Class (a76) → MaterialType (public) material_key = Column(String(10), ForeignKey('public.material_types.key')) ```