Compare commits

..

68 Commits

Author SHA1 Message Date
36c3e26231 Merge pull request 'feat(core): normalize sales document origin types' (#63) from feature/sales/62-normalize-sales-document-origin-type into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m14s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/63
2026-05-08 03:16:00 +00:00
3b4f664ae9 feat(core): normalize sales document origin types
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m51s
closes #62
2026-05-08 00:15:11 -03:00
4d5045718f Merge pull request 'feat(core): add sales document core flow with coverage' (#61) from feature/sales/60-sales-document-core-coverage into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m42s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/61
2026-05-07 23:51:22 +00:00
3d55913c31 feat(core): add sales document core flow with coverage
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 5m17s
closes #60
2026-05-07 20:47:34 -03:00
a1317d5dad Merge pull request 'feat(domain): add sales document domain contracts with coverage' (#59) from feature/sales/57-sales-document-domain-contracts-coverage into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m10s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/59
2026-05-07 03:35:24 +00:00
5e894ddca5 feat(domain): add sales document domain contracts with coverage
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 7m39s
- add sales document domain entities
- add coverage-oriented DTO contracts
- add sales document enums/constants
- prepare domain model for mixed coverage scenarios
- align domain contracts with future ARCA integration

closes #57
2026-05-07 00:33:34 -03:00
43f83c7252 Merge pull request 'feat(api): add sales document coverage and period support' (#58) from feature/sales/55-sales-document-coverage into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m32s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/58
2026-05-02 19:25:54 +00:00
0093b13a06 feat(api): add sales document coverage and period support
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 9m26s
closes #55
2026-05-02 16:24:05 -03:00
e75145ec65 Merge pull request 'feat(eos): create sales document persistence model' (#56) from feature/leandro/48-deliverynote-excel-export-ui into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 4m24s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/56
2026-04-30 19:23:29 +00:00
4ee6880e92 feat(eos): create sales document persistence model
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 5m28s
closes #55
2026-04-30 16:20:51 -03:00
d0fd570947 Merge pull request 'Actualizar .gitea/workflows/ci.yml' (#54) from leandro-patch-2 into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 10m57s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/54
2026-04-25 19:25:28 +00:00
5f6a2c8ca9 Actualizar .gitea/workflows/ci.yml
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 2m14s
2026-04-25 19:24:00 +00:00
2f7029467b Merge pull request 'Actualizar .gitea/workflows/ci.yml' (#53) from leandro-patch-2 into master
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 2m33s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/53
2026-04-21 11:56:22 +00:00
1fc91c625a Actualizar .gitea/workflows/ci.yml
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Failing after 1m56s
2026-04-21 11:55:34 +00:00
bfff69fb47 Merge pull request 'feat(sales): enable excel export button in DeliveryNotes UI' (#49) from feature/leandro/48-deliverynote-excel-export-ui into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m16s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/49
2026-03-27 23:57:32 +00:00
49f5a259a0 feat(sales): enable excel export button in DeliveryNotes UI
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 9m14s
Closes #48
2026-03-27 20:57:08 -03:00
569005ec94 Merge pull request 'feat(deliverynote): add excel export with clinical snapshot parsing' (#47) from feature/leandro/46-deliverynote-excel-export into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m13s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/47
2026-03-27 19:04:21 +00:00
8f81614922 feat(deliverynote): add excel export with clinical snapshot parsing
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m55s
- Implement exportfiltered endpoint
- Generate Excel using XLSXExportBase (EPPlus)
- Map Delivery Note summary fields
- Parse ExtraInfoJson into business columns (Professional, Institution, Patient, SurgeryDate)
- Format dates for Excel
- Keep export at header level (no items)

Closes #46
2026-03-27 16:03:40 -03:00
833cc1660f Merge pull request 'Actualizacion de Readme.md' (#45) from leandro-patch-readme into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m2s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/45
2026-03-26 22:01:34 +00:00
263e5e4de8 Actualizacion de Readme.md
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 3m58s
2026-03-26 22:01:06 +00:00
4dc6e5ac92 Merge pull request 'feature/leandro/43-deliverynote-pdf' (#44) from feature/leandro/43-deliverynote-pdf into master
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 15m47s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/44
2026-03-26 16:27:50 +00:00
e8f2e17820 feat(sales): descargar PDF automáticamente al emitir Delivery Note y boton de impresion en consulta.
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 16m10s
closes #43
2026-03-26 13:26:02 -03:00
f403ffa90d feat(documents): agregar template PDF de Delivery Note y endpoint API closes #43 2026-03-25 20:46:38 -03:00
cb1f159ac4 feat(sales): preparar DeliveryNoteDto y consulta para impresión closes #43 2026-03-25 18:33:01 -03:00
b1d48d4eec Merge pull request 'feat(deliverynote): snapshot clínico (ExtraInfo) desde presupuesto' (#42) from feature/leandro/41-deliverynote-extrainfo-snapshot into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 9m36s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/42
2026-03-25 00:48:30 +00:00
c1aa6827b0 feat(deliverynote): snapshot clínico (ExtraInfo) desde presupuesto
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 9m31s
Se implementa la construcción automática de ExtrainfoJson al seleccionar un presupuesto en la pantalla de emisión de Delivery Note.

- Se genera snapshot clínico con Professional, Institution, Patient y SurgeryDate
- Se serializa a JSON plano utilizando System.Text.Json
- Se asigna a Model.ExtraInfoJson para persistencia
- Se limpia el snapshot al deseleccionar o fallar la carga del presupuesto

Se mantiene consistencia con el patrón implementado en Expeditions.
No se modifican contratos ni capas Core/API/Data.

Closes #41
2026-03-24 21:47:02 -03:00
2a9cced311 Merge pull request 'feat(sales): precargar ítems aprobados desde presupuesto en delivery note create' (#40) from feature/leandro/39-deliverynote-precarga-items into master
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 8m9s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/40
2026-03-24 21:06:16 +00:00
80e9a7f146 feat(sales): precargar ítems aprobados desde presupuesto en delivery note create closes #39
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 8m47s
2026-03-24 18:05:37 -03:00
ed06dac9be Merge pull request 'feat(sales): implement delivery note persistence on issue' (#38) from feature/leandro/36-delivery-note-create-ui into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 10m54s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/38
2026-03-24 19:43:34 +00:00
af91f6be5c feat(sales): implement delivery note persistence on issue
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 2m21s
closes #37
2026-03-24 16:34:38 -03:00
3f0e3d425b Merge pull request 'feature/leandro/35-issue-delivery-note' (#36) from feature/leandro/35-issue-delivery-note into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m48s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/36
2026-03-24 13:15:19 +00:00
1b74027195 feat(sales): add delivery note issue endpoint closes #35
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 11m38s
2026-03-24 09:57:52 -03:00
ec990897cb feat(sales): persist delivery note issue in repository closes #35 2026-03-24 02:59:07 -03:00
9327a1dc2a refactor(sales): align delivery note entities with project mapping strategy closes #35 2026-03-23 18:14:00 -03:00
e0bc38d626 feat(sales): add delivery note issue core method closes #35 2026-03-23 12:08:57 -03:00
f6bf3c61e8 feat(sales): add delivery note create DTOs closes #35 2026-03-23 11:31:33 -03:00
6d3ad6aef2 Merge pull request 'feat(sales): exponer aprobación por ítem en QuoteDto para precarga documental.' (#34) from feature/leandro/33-quote-approved-items-dto into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m12s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/34
2026-03-22 21:07:47 +00:00
2d652c89c8 feat(sales): exponer aprobación por ítem en QuoteDto para precarga documental.
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 4m44s
Incluye arreglo en lookup para traer presupuestos aprobados en lugar de emitidos.
Closes #33
2026-03-22 18:05:56 -03:00
24120636b1 Merge pull request 'feat(sales): add delivery note detail drawer Closes #31' (#32) from feature/leandro/31-deliverynote-detail-drawer into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m34s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/32
2026-03-21 16:50:04 +00:00
d3f0a5aa1f feat(sales): add delivery note detail drawer Closes #31
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m9s
2026-03-21 13:48:36 -03:00
9342447598 Merge pull request 'feat(sales): pantalla principal de consulta Delivery Note alineada a Quotes (#29)' (#30) from feature/leandro/29-deliverynote-consulta-principal into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m6s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/30
2026-03-21 15:54:28 +00:00
af635eadda feat(sales): pantalla principal de consulta Delivery Note alineada a Quotes (#29)
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m37s
- Se incorpora /deliverynotes con patrón visual idéntico a Quotes
- Se implementan filtros, tabla y paginación completa
- Se integra búsqueda con endpoint /api/deliverynote/search
- Se utiliza DeliveryNoteSearchParams desde Domain.Generics (sin duplicaciones)
- Se agregan placeholders para Nuevo, Excel y Ver
- Se incorpora navegación en menú

Closes #29
2026-03-21 12:53:53 -03:00
e9ae63ccbe Merge pull request 'feat(sales): agregar búsqueda paginada de Delivery Note (search endpoint + DTO resumen) closes #27' (#28) from feature/leandro/27-deliverynote-search into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 3m27s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/28
2026-03-21 02:04:52 +00:00
63915aa980 feat(sales): agregar búsqueda paginada de Delivery Note (search endpoint + DTO resumen) closes #27
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 7m49s
2026-03-20 23:03:46 -03:00
bb764b4d12 Merge pull request 'feat(sales): incorporar servicio UI para consumo de Delivery Note' (#26) from feature/leandro/25-deliverynote-ui-service into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 14m42s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/26
2026-03-19 21:24:09 +00:00
e02d9b1ac0 feat(sales): incorporar servicio UI para consumo de Delivery Note
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 8m30s
closes #25
2026-03-19 18:23:26 -03:00
228403bac6 Merge pull request 'feat(sales): incorporar DTO de lectura para Delivery Note #23' (#24) from feature/leandro/23-deliverynote-dto-read into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m4s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/24
2026-03-19 20:44:54 +00:00
33fe012b09 feat(sales): incorporar DTO de lectura para Delivery Note #23
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 18m57s
- Se agregan DeliveryNoteDto y DeliveryNoteItemDto
- Se implementa proyección a DTO en PhSDeliveryNoteRepository
- Se extiende IPhSDeliveryNoteRepository con métodos DTO
- Se ajusta DeliveryNoteService para trabajar con DTO
- Se actualiza DeliveryNoteController para devolver DTO
- Se elimina exposición directa de EDeliveryNote en la API

Closes #23
2026-03-19 17:41:49 -03:00
564b171e84 Merge pull request 'feat(sales): exponer API de lectura para Delivery Note closes #21' (#22) from feature/leandro/21-deliverynote-api-read into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m59s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/22
2026-03-19 15:19:36 +00:00
9017097006 feat(sales): exponer API de lectura para Delivery Note closes #21
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 3m44s
2026-03-19 12:11:30 -03:00
109ef556b9 Merge pull request 'refactor(database): organizar scripts SQL en estructura Database' (#20) from refactor/leandro/19-database-structure into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m56s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/20
2026-03-19 13:17:56 +00:00
0b99a89bc1 refactor(database): organizar scripts SQL en estructura Database
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 4m3s
- Se crea carpeta Database con subcarpetas Procedures, Types y Verifications
- Se reubican scripts SQL fuera de la raíz
- Se alinea estructura física con Solution Explorer
Closes #19
2026-03-19 10:17:02 -03:00
587ef24b7d Merge pull request 'feat(sales): agregar servicio core de lectura para Delivery Note' (#18) from feature/leandro/17-delivery-note-core-read-service into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m0s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/18
2026-03-19 04:39:48 +00:00
a498c38d84 feat(sales): agregar servicio core de lectura para Delivery Note
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 4m19s
Closes #17
2026-03-19 01:37:43 -03:00
5d9a80201f Merge pull request 'feat(sales): agregar repositorio base delivery note' (#16) from feature/leandro/13-deliverynote-domain into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m9s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/16
2026-03-17 13:15:13 +00:00
c55581f546 feat(sales): agregar repositorio base delivery note
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 16m32s
Closes #15
2026-03-17 10:08:51 -03:00
579c6ef493 Merge pull request 'feat(sales): agregar entidades domain para delivery note' (#14) from feature/leandro/13-deliverynote-domain into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m52s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/14
2026-03-17 01:09:34 +00:00
2dd8f5b1c7 feat(sales): agregar entidades domain para delivery note
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m17s
Closes #13
2026-03-16 22:08:04 -03:00
ada3c04b9b Merge pull request 'feat(sales): scaffold EF para Delivery Notes' (#12) from feature/leandro/11-deliverynote-scaffold into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m17s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/12
2026-03-16 22:45:04 +00:00
dc08291932 feat(sales): scaffold EF para Delivery Notes
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 4m10s
Closes #11
2026-03-16 19:42:56 -03:00
0c035f50d6 Merge pull request 'feat(stock): reserve stock when expedition moves to EnTransito' (#10) from feature/leandro/9-stock-reservation-on-transit into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m57s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/10
2026-03-15 22:18:37 +00:00
915f78bb40 feat(stock): reserve stock when expedition moves to EnTransito
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 4m23s
Closes #9
2026-03-15 19:17:26 -03:00
b2aebafe55 Merge pull request 'feat(expeditions): permitir transición Emitida → EnTransito desde la consulta' (#8) from feature/leandro/7-expedition-mark-in-transit into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m13s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/8
2026-03-15 00:30:32 +00:00
6419ac8843 feat(expeditions): permitir transición Emitida → EnTransito desde la consulta
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 34m41s
closes #7
2026-03-14 21:23:06 -03:00
d99f1c34d2 Merge pull request 'feat(expeditions): prevent reuse of stockitem_id in active expeditions' (#6) from feature/leandro/5-double-trace-lock into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m18s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/6
2026-03-12 02:38:20 +00:00
6d0a72c01d feat(expeditions): prevent reuse of stockitem_id in active expeditions
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 20m38s
Closes #5
2026-03-11 23:35:51 -03:00
55924ca07a Merge pull request 'feat(expeditions): persist stockitem_id in ExpeditionDetails (traceability base)' (#4) from feature/leandro/3-persist-stockitemid-expeditiondetail into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 4m26s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/4
2026-03-03 02:56:15 +00:00
394c864dfa ffeat(expeditions): persist stockitem_id in ExpeditionDetails (traceability base)
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m37s
- Added stockitem_id column to PhLSM_ExpeditionDetails
- Added FK to PhLSM_StockItem
- Added indexes (StockItem and Expedition_StockItem)
- Updated scaffold models
- Updated UI merge to preserve StockItemId
- CreateFullExpeditionAsync now persists stockitem_id
- Base step to enable logistic states and double-trace prevention

Closes #3
2026-03-02 19:44:49 -03:00
110 changed files with 6658 additions and 565 deletions

View File

@ -11,9 +11,7 @@ on:
jobs: jobs:
build: build:
name: Build and Deploy with Docker Compose name: Build and Deploy with Docker Compose
#runs-on: ubuntu-latest runs-on: ubuntu-latest
runs-on: [self-hosted, ubuntu-22.04]
steps: steps:
# Paso 1: Checkout del código # Paso 1: Checkout del código

4
.gitignore vendored
View File

@ -397,6 +397,10 @@ FodyWeavers.xsd
*.msm *.msm
*.msp *.msp
# Patch files (temporary)
*.patch
*.diff
# JetBrains Rider # JetBrains Rider
*.sln.iml *.sln.iml
/Core/obj/Debug/net8.0/Core.csproj.FileListAbsolute.txt /Core/obj/Debug/net8.0/Core.csproj.FileListAbsolute.txt

View File

@ -0,0 +1,62 @@
using Domain.Dtos.Sales;
using Domain.Generics;
/// <summary>
/// Servicio de dominio para la gestión de consultas de Delivery Note (Remito Ventas).
/// Encapsula el acceso a datos y expone operaciones de lectura para la capa superior.
/// </summary>
public interface IDeliveryNoteDom
{
/// <summary>
/// Busca Delivery Notes con filtros y paginación.
/// </summary>
Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(
int? customerId,
string? customerText,
string? deliveryNoteNumber,
int? quoteId,
string? quoteNumber,
DateTime? issueDateFrom,
DateTime? issueDateTo,
string? status,
int page = 1,
int pageSize = 50);
/// <summary>
/// Exporta a Excel los Delivery Notes filtrados.
/// </summary>
Task<byte[]> ExportFilteredToExcelAsync(DeliveryNoteSearchParams searchParams);
/// <summary>
/// Obtiene un Delivery Note por su identificador único.
/// </summary>
/// <param name="id">Identificador interno del Delivery Note.</param>
/// <returns>
/// El DTO <see cref="DeliveryNoteDto"/> si existe; en caso contrario, null.
/// </returns>
Task<DeliveryNoteDto?> GetDtoByIdAsync(int id);
/// <summary>
/// Obtiene un Delivery Note a partir de su número de documento.
/// </summary>
/// <param name="deliveryNoteNumber">Número del Delivery Note (ej: DN-00000001).</param>
/// <returns>
/// El DTO <see cref="DeliveryNoteDto"/> si existe; en caso contrario, null.
/// </returns>
Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber);
/// <summary>
/// Obtiene todos los Delivery Notes asociados a un presupuesto (Quote).
/// </summary>
/// <param name="quoteId">Identificador del presupuesto relacionado.</param>
/// <returns>
/// Colección de <see cref="DeliveryNoteDto"/> asociadas al presupuesto.
/// Puede estar vacía si no existen registros.
/// </returns>
Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId);
/// <summary>
/// Valida y prepara la emisión de un Delivery Note.
/// </summary>
Task<DeliveryNoteCreateResponse> CreateAndIssueDeliveryNoteAsync(DeliveryNoteCreateRequest request);
}

View File

@ -0,0 +1,10 @@
using Domain.Dtos.Sales;
namespace Core.Interfaces
{
public interface ISalesDocumentDom
{
Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request);
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
}
}

View File

@ -24,5 +24,6 @@ namespace Core.Interfaces.Stock
IEnumerable<ELSExpeditionDetail> details, IEnumerable<ELSExpeditionDetail> details,
int formSeriesId); int formSeriesId);
Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams); Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams);
Task MarkInTransitAsync(int expeditionId);
} }
} }

View File

@ -0,0 +1,295 @@
using Domain.Dtos.Sales;
using Domain.Entities;
using Domain.Generics;
using Models.Interfaces;
using System.Reflection;
using System.Text.Json;
using Transversal.Services;
namespace Core.Services
{
public class DeliveryNoteService(IPhSDeliveryNoteRepository deliveryNoteRepository) : IDeliveryNoteDom
{
private readonly IPhSDeliveryNoteRepository _deliveryNoteRepository = deliveryNoteRepository;
public Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(
int? customerId,
string? customerText,
string? deliveryNoteNumber,
int? quoteId,
string? quoteNumber,
DateTime? issueDateFrom,
DateTime? issueDateTo,
string? status,
int page = 1,
int pageSize = 50)
{
page = page <= 0 ? 1 : page;
pageSize = pageSize <= 0 ? 50 : pageSize;
return _deliveryNoteRepository.SearchAsync(
customerId,
string.IsNullOrWhiteSpace(customerText) ? null : customerText.Trim(),
string.IsNullOrWhiteSpace(deliveryNoteNumber) ? null : deliveryNoteNumber.Trim(),
quoteId,
string.IsNullOrWhiteSpace(quoteNumber) ? null : quoteNumber.Trim(),
issueDateFrom,
issueDateTo,
string.IsNullOrWhiteSpace(status) ? null : status.Trim(),
page,
pageSize);
}
public Task<DeliveryNoteDto?> GetDtoByIdAsync(int id)
{
if (id <= 0)
throw new ArgumentOutOfRangeException(nameof(id), "El identificador del remito es inválido.");
return _deliveryNoteRepository.GetDtoByIdAsync(id);
}
public async Task<byte[]> ExportFilteredToExcelAsync(DeliveryNoteSearchParams searchParams)
{
ArgumentNullException.ThrowIfNull(searchParams);
try
{
var searchResult = await _deliveryNoteRepository.SearchAsync(
searchParams.CustomerId,
string.IsNullOrWhiteSpace(searchParams.CustomerText) ? null : searchParams.CustomerText.Trim(),
string.IsNullOrWhiteSpace(searchParams.DeliveryNoteNumber) ? null : searchParams.DeliveryNoteNumber.Trim(),
searchParams.QuoteId,
string.IsNullOrWhiteSpace(searchParams.QuoteNumber) ? null : searchParams.QuoteNumber.Trim(),
searchParams.IssueDateFrom,
searchParams.IssueDateTo,
string.IsNullOrWhiteSpace(searchParams.Status) ? null : searchParams.Status.Trim(),
searchParams.Page <= 0 ? 1 : searchParams.Page,
searchParams.PageSize <= 0 ? 50 : searchParams.PageSize);
if (searchResult?.Items is null || !searchResult.Items.Any())
throw new Exception("No se encontraron remitos para exportar.");
var items = searchResult.Items.ToList();
var exportRows = new List<DeliveryNoteExcelRow>(items.Count);
foreach (var deliveryNote in items)
{
var dto = await _deliveryNoteRepository.GetDtoByIdAsync(deliveryNote.Id);
var snapshot = DeliveryNoteSnapshotInfo.FromJson(dto?.ExtraInfoJson);
exportRows.Add(new DeliveryNoteExcelRow
{
DeliveryNoteNumber = deliveryNote.DeliveryNoteNumber,
IssueDate = deliveryNote.IssueDate.ToString("dd/MM/yyyy"),
QuoteNumber = deliveryNote.QuoteNumber,
CustomerName = deliveryNote.CustomerName,
Status = deliveryNote.Status,
ProfessionalName = snapshot.ProfessionalName,
InstitutionName = snapshot.InstitutionName,
PatientName = snapshot.PatientName,
SurgeryDate = snapshot.SurgeryDate,
Observations = deliveryNote.Observations,
PrintCount = deliveryNote.PrintCount,
CreatedAt = deliveryNote.CreatedAt.ToString("dd/MM/yyyy HH:mm")
});
}
var stream = new XLSXExportBase();
return stream.ExportExcel(exportRows);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
throw new Exception($"{methodName} Message: {ex.Message}", ex);
}
}
public Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber)
{
if (string.IsNullOrWhiteSpace(deliveryNoteNumber))
throw new ArgumentException("El número de remito es obligatorio.", nameof(deliveryNoteNumber));
return _deliveryNoteRepository.GetDtoByDeliveryNoteNumberAsync(deliveryNoteNumber.Trim());
}
public Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId)
{
if (quoteId <= 0)
throw new ArgumentOutOfRangeException(nameof(quoteId), "El identificador del presupuesto es inválido.");
return _deliveryNoteRepository.GetDtosByQuoteIdAsync(quoteId);
}
public async Task<DeliveryNoteCreateResponse> CreateAndIssueDeliveryNoteAsync(DeliveryNoteCreateRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.DeliveryNoteNumber))
throw new ArgumentException("El número de remito es obligatorio.", nameof(request.DeliveryNoteNumber));
if (request.CustomerId <= 0)
throw new ArgumentException("Debe seleccionar un cliente.", nameof(request.CustomerId));
if (request.IssueDate == default)
throw new ArgumentException("La fecha de emisión es obligatoria.", nameof(request.IssueDate));
if (request.Items is null || request.Items.Count == 0)
throw new InvalidOperationException("Debe incluir al menos un ítem.");
if (request.Items.Any(i => i.Quantity <= 0))
throw new InvalidOperationException("Todas las cantidades deben ser mayores a cero.");
if (request.Items.Any(i => string.IsNullOrWhiteSpace(i.Description)))
throw new InvalidOperationException("Todos los ítems deben incluir descripción.");
var deliveryNoteNumber = request.DeliveryNoteNumber.Trim();
if (await _deliveryNoteRepository.ExistsByDeliveryNoteNumberAsync(deliveryNoteNumber))
throw new InvalidOperationException($"Ya existe un remito con el número '{deliveryNoteNumber}'.");
var now = DateTime.Now;
var entity = new EDeliveryNote
{
Deliverynotenumber = deliveryNoteNumber,
QuoteId = request.QuoteId,
Issuedate = request.IssueDate,
CustomerId = request.CustomerId,
Status = "Emitido",
Observations = string.IsNullOrWhiteSpace(request.Observations) ? null : request.Observations.Trim(),
ExtrainfoJson = string.IsNullOrWhiteSpace(request.ExtraInfoJson) ? null : request.ExtraInfoJson.Trim(),
Printcount = 0,
Createdat = now,
PhSDeliveryNoteDetails = request.Items
.Select((item, index) => new EDeliveryNoteDetail
{
LineNumber = index + 1,
OriginType = item.OriginType,
OriginId = item.OriginId,
QuoteDetailId = item.QuoteDetailId,
Description = item.Description.Trim(),
Quantity = item.Quantity,
Notes = string.IsNullOrWhiteSpace(item.Notes) ? string.Empty : item.Notes.Trim(),
Createdat = now
})
.ToList()
};
var created = await _deliveryNoteRepository.CreateAsync(entity);
return new DeliveryNoteCreateResponse
{
Id = created.Id,
DeliveryNoteNumber = created.Deliverynotenumber
};
}
private sealed class DeliveryNoteExcelRow
{
public string DeliveryNoteNumber { get; set; } = string.Empty;
public string IssueDate { get; set; } = string.Empty;
public string? QuoteNumber { get; set; }
public string? CustomerName { get; set; }
public string? Status { get; set; }
public string? ProfessionalName { get; set; }
public string? InstitutionName { get; set; }
public string? PatientName { get; set; }
public string? SurgeryDate { get; set; }
public string? Observations { get; set; }
public int PrintCount { get; set; }
public string CreatedAt { get; set; } = string.Empty;
}
private sealed class DeliveryNoteSnapshotInfo
{
public string? ProfessionalName { get; private set; }
public string? InstitutionName { get; private set; }
public string? PatientName { get; private set; }
public string? SurgeryDate { get; private set; }
public static DeliveryNoteSnapshotInfo FromJson(string? extraInfoJson)
{
var snapshot = new DeliveryNoteSnapshotInfo();
if (string.IsNullOrWhiteSpace(extraInfoJson))
return snapshot;
try
{
using var document = JsonDocument.Parse(extraInfoJson);
var root = document.RootElement;
snapshot.ProfessionalName = ReadString(root, "professional", "professionalName", "doctor", "doctorName", "medico", "medicoNombre");
snapshot.InstitutionName = ReadString(root, "institution", "institutionName", "hospital", "hospitalName", "institucion", "institucionNombre");
snapshot.PatientName = ReadString(root, "patient", "patientName", "paciente", "pacienteNombre");
snapshot.SurgeryDate = ReadDate(root, "surgeryDate", "estimatedDate", "fechaCirugia", "surgery_date", "estimated_date");
}
catch
{
return snapshot;
}
return snapshot;
}
private static string? ReadString(JsonElement root, params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
if (!TryGetPropertyIgnoreCase(root, propertyName, out var value))
continue;
if (value.ValueKind == JsonValueKind.String)
return value.GetString();
if (value.ValueKind != JsonValueKind.Null && value.ValueKind != JsonValueKind.Undefined)
return value.ToString();
}
return null;
}
private static string? ReadDate(JsonElement root, params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
if (!TryGetPropertyIgnoreCase(root, propertyName, out var value))
continue;
if (value.ValueKind == JsonValueKind.String)
{
var raw = value.GetString();
if (string.IsNullOrWhiteSpace(raw))
return null;
if (DateTime.TryParse(raw, out var parsedDate))
return parsedDate.ToString("dd/MM/yyyy");
return raw;
}
if (value.ValueKind != JsonValueKind.Null && value.ValueKind != JsonValueKind.Undefined)
return value.ToString();
}
return null;
}
private static bool TryGetPropertyIgnoreCase(JsonElement element, string propertyName, out JsonElement value)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
value = default;
return false;
}
}
}
}

View File

@ -0,0 +1,162 @@
using Core.Interfaces;
using Domain.Constants;
using Domain.Dtos.Sales;
using Domain.Entities;
using Models.Interfaces;
namespace Core.Services
{
public class SalesDocumentService(IPhSSalesDocumentRepository salesDocumentRepository) : ISalesDocumentDom
{
private readonly IPhSSalesDocumentRepository _salesDocumentRepository = salesDocumentRepository;
public async Task<SalesDocumentCreateResponse> CreateAsync(SalesDocumentCreateRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (request.CustomerId <= 0)
throw new ArgumentException("Debe seleccionar un cliente.", nameof(request.CustomerId));
if (request.BillToCustomerId <= 0)
throw new ArgumentException("Debe seleccionar un cliente de facturación.", nameof(request.BillToCustomerId));
if (string.IsNullOrWhiteSpace(request.Currency))
throw new ArgumentException("La moneda es obligatoria.", nameof(request.Currency));
if (request.Details is null || request.Details.Count == 0)
throw new InvalidOperationException("Debe incluir al menos un detail.");
if (request.Coverage is null || request.Coverage.Count == 0)
throw new InvalidOperationException("Debe incluir coverage.");
foreach (var detail in request.Details)
ValidateDetail(detail);
var netAmount = request.Details.Sum(x => x.NetAmount);
var taxAmount = request.Details.Sum(x => x.TaxAmount);
var totalAmount = request.Details.Sum(x => x.TotalAmount);
if (totalAmount <= 0)
throw new InvalidOperationException("El total del documento debe ser mayor a cero.");
var now = DateTime.Now;
var entity = new ESalesDocument
{
FormseriesId = request.FormseriesId,
DocumentType = request.DocumentType,
FiscalVoucherType = request.FiscalVoucherType,
FiscalVoucherLetter = request.FiscalVoucherLetter,
Status = (int)SalesDocumentStatus.Draft,
QuoteId = request.QuoteId,
CustomerId = request.CustomerId,
BillToCustomerId = request.BillToCustomerId,
IssueDate = request.IssueDate ?? now,
Currency = request.Currency.Trim(),
ExchangeRate = request.ExchangeRate <= 0 ? 1 : request.ExchangeRate,
NetAmount = netAmount,
TaxAmount = taxAmount,
TotalAmount = totalAmount,
AssociatedDocumentType = request.AssociatedDocumentType,
AssociatedDocumentNumber = request.AssociatedDocumentNumber,
AssociatedDocumentDate = request.AssociatedDocumentDate,
Observations = request.Observations,
ExtraInfoJson = request.ExtraInfoJson,
PeriodFrom = request.PeriodFrom,
PeriodTo = request.PeriodTo,
Createdat = now,
PhSSalesDocumentDetails = request.Details.Select(x => new ESalesDocumentDetail
{
LineNumber = x.LineNumber,
OriginType = x.OriginType.ToStorageCode(),
OriginId = ResolveOriginId(x),
QuoteDetailId = ResolveQuoteDetailId(x),
ProductId = x.ProductId,
Description = x.Description.Trim(),
Quantity = x.Quantity,
AuthorizedUnitPrice = x.AuthorizedUnitPrice,
AuthorizedAmount = x.AuthorizedAmount,
BilledPercentage = x.BilledPercentage,
UnitPrice = x.UnitPrice,
NetAmount = x.NetAmount,
TaxAmount = x.TaxAmount,
TotalAmount = x.TotalAmount,
OriginSnapshotJson = x.OriginSnapshotJson,
Createdat = now
}).ToList(),
PhSSalesDocumentCoverages = request.Coverage.Select(x => new ESalesDocumentCoverage
{
QuoteId = x.QuoteId,
QuoteDetailId = x.QuoteDetailId,
CoverageType = x.CoverageType,
CoveragePercentage = x.CoveragePercentage,
CoverageAmount = x.CoverageAmount,
PeriodFrom = x.PeriodFrom,
PeriodTo = x.PeriodTo,
Notes = x.Notes,
Createdat = now
}).ToList()
};
var created = await _salesDocumentRepository.CreateAsync(entity);
return new SalesDocumentCreateResponse
{
Id = created.Id,
InternalDocumentNumber = created.InternalDocumentNumber
};
}
public Task<SalesDocumentDto?> GetDtoByIdAsync(int id)
{
if (id <= 0)
throw new ArgumentOutOfRangeException(nameof(id));
return _salesDocumentRepository.GetDtoByIdAsync(id);
}
private static void ValidateDetail(SalesDocumentCreateDetailRequest detail)
{
if (detail.LineNumber <= 0)
throw new ArgumentException("El número de línea del detail debe ser mayor a cero.", nameof(detail.LineNumber));
if (!Enum.IsDefined(typeof(SalesDocumentOriginType), detail.OriginType))
throw new ArgumentException("El tipo de origen del detail no es válido.", nameof(detail.OriginType));
if (string.IsNullOrWhiteSpace(detail.Description))
throw new ArgumentException("La descripción del detail es obligatoria.", nameof(detail.Description));
if (detail.Quantity <= 0)
throw new ArgumentException("La cantidad del detail debe ser mayor a cero.", nameof(detail.Quantity));
var hasOriginId = detail.OriginId.HasValue && detail.OriginId.Value > 0;
var hasQuoteDetailId = detail.QuoteDetailId.HasValue && detail.QuoteDetailId.Value > 0;
if (detail.OriginType != SalesDocumentOriginType.Manual && !hasOriginId && !hasQuoteDetailId)
throw new ArgumentException("Debe informar OriginId o QuoteDetailId para trazabilidad del origen.", nameof(detail.OriginId));
if (detail.OriginType == SalesDocumentOriginType.QuoteDetail && !hasQuoteDetailId && !hasOriginId)
throw new ArgumentException("Debe informar QuoteDetailId u OriginId para líneas originadas en presupuesto.", nameof(detail.QuoteDetailId));
}
private static int? ResolveOriginId(SalesDocumentCreateDetailRequest detail)
{
if (detail.OriginId.HasValue && detail.OriginId.Value > 0)
return detail.OriginId;
return detail.OriginType == SalesDocumentOriginType.QuoteDetail
? detail.QuoteDetailId
: null;
}
private static int? ResolveQuoteDetailId(SalesDocumentCreateDetailRequest detail)
{
if (detail.QuoteDetailId.HasValue && detail.QuoteDetailId.Value > 0)
return detail.QuoteDetailId;
return detail.OriginType == SalesDocumentOriginType.QuoteDetail
? detail.OriginId
: null;
}
}
}

View File

@ -1,4 +1,5 @@
using Core.Interfaces.Stock; using Core.Interfaces.Stock;
using Domain.Constants;
using Domain.Dtos.Stock; using Domain.Dtos.Stock;
using Domain.Entities; using Domain.Entities;
using Domain.Generics; using Domain.Generics;
@ -12,32 +13,155 @@ namespace Core.Services.Stock
{ {
#region Declaraciones #region Declaraciones
private readonly IExpeditionRepository _repo; private readonly IExpeditionRepository _repo;
public ExpeditionService(IExpeditionRepository repo) => _repo = repo; private readonly IPhLSMStockItemRepository _stockItemRepository;
public ExpeditionService(
IExpeditionRepository repo,
IPhLSMStockItemRepository stockItemRepository)
{
_repo = repo;
_stockItemRepository = stockItemRepository;
}
#endregion #endregion
#region Guardado completo de expedicion (encabezado + detalles) #region Guardado completo de expedicion (encabezado + detalles)
public async Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync( public async Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(
ELSExpeditionHeader header, ELSExpeditionHeader header,
IEnumerable<ELSExpeditionDetail> details, IEnumerable<ELSExpeditionDetail> details,
int formSeriesId) int formSeriesId)
{ {
if (header is null) throw new ArgumentNullException(nameof(header)); if (header is null)
throw new ArgumentNullException(nameof(header));
if (details is null || !details.Any()) if (details is null || !details.Any())
throw new InvalidOperationException("Debe incluir al menos un ítem."); throw new InvalidOperationException("Debe incluir al menos un ítem.");
if (formSeriesId <= 0) if (formSeriesId <= 0)
throw new ArgumentOutOfRangeException(nameof(formSeriesId), "Serie inválida."); throw new ArgumentOutOfRangeException(nameof(formSeriesId), "Serie inválida.");
// Reemplazo directo de la colección (más claro que Clear()+Add) var detailList = details.ToList();
header.PhLsmExpeditionDetails = details.ToList();
ValidateNoDuplicateStockItems(detailList);
await ValidateSerializedConflictsAsync(detailList);
await ValidateStockAvailabilityAsync(detailList);
header.PhLsmExpeditionDetails = detailList;
return await _repo.CreateFullExpeditionAsync(header, formSeriesId); return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
} }
private static void ValidateNoDuplicateStockItems(List<ELSExpeditionDetail> detailList)
{
var duplicateIds = detailList
.Where(d => d.StockitemId > 0)
.GroupBy(d => d.StockitemId)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.OrderBy(x => x)
.ToList();
if (duplicateIds.Count == 0)
return;
var msg = "No se puede emitir la expedición. " +
"El mismo StockItem fue seleccionado más de una vez: " +
string.Join(", ", duplicateIds);
throw new InvalidOperationException(msg);
}
private async Task ValidateSerializedConflictsAsync(List<ELSExpeditionDetail> detailList)
{
var requestedStockItemIds = detailList
.Where(d => d.StockitemId > 0)
.Select(d => d.StockitemId)
.Distinct()
.ToList();
if (requestedStockItemIds.Count == 0)
return;
var conflicts = await _repo.CheckStockItemConflictsAsync(
requestedStockItemIds,
ignoreExpeditionId: null);
if (conflicts.Count == 0)
return;
var lines = new List<string>
{
$"No se puede emitir la expedición: se detectaron {conflicts.Count} stock items serializados ya asignados a expediciones activas."
};
foreach (var conflict in conflicts
.OrderBy(x => x.StockitemId)
.ThenBy(x => x.Expeditionnumber))
{
var statusLabel = ((ExpeditionStatus)conflict.Status).ToLabel();
lines.Add($"• StockItem {conflict.StockitemId} → {conflict.Expeditionnumber} ({statusLabel})");
}
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
}
private async Task ValidateStockAvailabilityAsync(List<ELSExpeditionDetail> detailList)
{
var requestedByStockItem = detailList
.Where(d => d.StockitemId > 0 && d.Quantity > 0)
.GroupBy(d => d.StockitemId)
.Select(g => new
{
StockitemId = g.Key,
RequestedQuantity = g.Sum(x => x.Quantity)
})
.ToList();
if (requestedByStockItem.Count == 0)
return;
var availability = await _stockItemRepository.GetAvailabilityByStockItemIdsAsync(
requestedByStockItem.Select(x => x.StockitemId));
var availabilityMap = availability.ToDictionary(x => x.StockitemId);
var errors = new List<string>();
foreach (var request in requestedByStockItem.OrderBy(x => x.StockitemId))
{
if (!availabilityMap.TryGetValue(request.StockitemId, out var stock))
{
errors.Add($"• StockItem {request.StockitemId} → no fue encontrado en stock.");
continue;
}
var hasSerial = !string.IsNullOrWhiteSpace(stock.Serial);
// Los serializados ya se validan por exclusividad en ValidateSerializedConflictsAsync
if (hasSerial)
continue;
if (request.RequestedQuantity > stock.AvailableQuantity)
{
errors.Add(
$"• StockItem {request.StockitemId} → solicitado: {request.RequestedQuantity}, disponible: {stock.AvailableQuantity}.");
}
}
if (errors.Count == 0)
return;
var lines = new List<string>
{
"No se puede emitir la expedición: algunos stock items no serializados no tienen cantidad disponible suficiente."
};
lines.AddRange(errors);
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
}
#endregion
// Otros métodos de la clase...
public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber) public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
#endregion
public Task<PagedResult<ExpeditionDto>> SearchAsync( public Task<PagedResult<ExpeditionDto>> SearchAsync(
string? expeditionNumber, string? expeditionNumber,
string? status, string? status,
@ -49,7 +173,6 @@ namespace Core.Services.Stock
=> _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize); => _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize);
public Task<ExpeditionDto?> GetDtoByIdAsync(int id) public Task<ExpeditionDto?> GetDtoByIdAsync(int id)
=> _repo.GetDtoByIdAsync(id); => _repo.GetDtoByIdAsync(id);
public async Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams) public async Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams)
{ {
try try
@ -97,5 +220,12 @@ namespace Core.Services.Stock
throw new Exception($"{ex.Message}", ex); throw new Exception($"{ex.Message}", ex);
} }
} }
public async Task MarkInTransitAsync(int expeditionId)
{
if (expeditionId <= 0)
throw new ArgumentException("El identificador de la expedición no es válido.");
await _repo.MarkInTransitAsync(expeditionId);
}
} }
} }

View File

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Data.Entities
{
public partial class PhOH_Tickets
{
[Key]
public System.Guid TicketId { get; set; }
public string Titulo { get; set; } = string.Empty;
public string Descripcion { get; set; } = string.Empty;
public string Prioridad { get; set; } = string.Empty;
public string Estado { get; set; } = string.Empty;
public string CreadorUsuarioId { get; set; } = string.Empty;
public Nullable<System.DateTime> FechaCreacion { get; set; }
public string AsignadoAUsuarioId { get; set; } = string.Empty;
public Nullable<System.DateTime> FechaEjecucion { get; set; }
public string Categoria { get; set; } = string.Empty;
public string Comentarios { get; set; } = string.Empty;
public string Departamento { get; set; } = string.Empty;
public string Impacto { get; set; } = string.Empty;
public string Urgencia { get; set; } = string.Empty;
public Nullable<bool> EsSolicitudCliente { get; set; }
public string AdjuntoArchivo { get; set; } = string.Empty;
}
}

View File

@ -1,17 +0,0 @@
namespace Data.Entities
{
public partial class Tickets_Dashboard_Result
{
public System.Guid TicketId { get; set; }
public string Titulo { get; set; }=string.Empty;
public string Prioridad { get; set; } = string.Empty;
public string Estado { get; set; } = string.Empty;
public string CreadorUsuarioId { get; set; } = string.Empty;
public Nullable<System.DateTime> FechaCreacion { get; set; }
public string AsignadoAUsuarioId { get; set; } = string.Empty;
public string Categoria { get; set; } =string.Empty;
public string Departamento { get; set; } = string.Empty;
public string Impacto { get; set; } = string.Empty;
public string Urgencia { get; set; } = string.Empty;
}
}

View File

@ -1,8 +0,0 @@
namespace Data.Entities
{
public partial class Tickets_GetSummary_Result
{
public string Estado { get; set; } = string.Empty;
public Nullable<int> Cantidad { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using Domain.Entities;
namespace Data.Interfaces
{
public interface ITicketRepository
{
IEnumerable<ETicket> GetAll();
ETicket GetById(Guid ticketId);
IEnumerable<ETickets_GetSummary> GetSummary();
IEnumerable<ETicket_Dashboard> GetTicketDashboard(string Estado, string Orden);
void InsertTicket(ETicket ticket);
}
}

View File

@ -1,87 +0,0 @@
using Data.Entities;
using System.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Extensions.Configuration;
namespace Data.Models
{
public class OperationsHubContext : DbContext
{
// Constructor que permite la inyección de dependencias
public OperationsHubContext(DbContextOptions<OperationsHubContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
// Leer la cadena de conexión desde app.config
var connectionString = ConfigurationManager.ConnectionStrings["OperationsHub"]?.ConnectionString;
if (connectionString == null)
{
throw new InvalidOperationException("No se encontró la cadena de conexión 'OperationsHub'.");
}
optionsBuilder.UseSqlServer(connectionString);
// Prueba de conexión: Intento de apertura de conexión
using (var connection = new SqlConnection(connectionString))
{
connection.Open(); // Lanza excepción si falla
System.Diagnostics.Debug.WriteLine("Conexión a la base de datos exitosa.");
connection.Close();
}
}
}
public bool TestConnection(string connectionString)
{
using (var connection = new SqlConnection(connectionString))
{
try
{
connection.Open();
return true; // Conexión exitosa
}
catch (Exception ex)
{
// Manejar la excepción, tal vez loguearla
Console.WriteLine($"Error al conectar: {ex.Message}");
return false; // Conexión fallida
}
}
}
public DbSet<PhOH_Tickets> Tickets { get; set; }
public DbSet<Tickets_Dashboard_Result> TicketsDashboardResults { get; set; }
public DbSet<Tickets_GetSummary_Result> TicketsSummaryResults { get; set; }
public async Task<List<Tickets_Dashboard_Result>> Tickets_DashboardAsync(string estadoParam, string ordenParam)
{
var estadoParamSql = new SqlParameter("@EstadoParam", estadoParam ?? (object)DBNull.Value);
var ordenParamSql = new SqlParameter("@OrdenParam", ordenParam ?? (object)DBNull.Value);
// Consulta usando FromSqlRaw sobre el DbSet correspondiente
return await TicketsDashboardResults
.FromSqlRaw("EXEC Tickets_Dashboard @EstadoParam, @OrdenParam", estadoParamSql, ordenParamSql)
.ToListAsync();
}
public async Task<List<Tickets_GetSummary_Result>> Tickets_GetSummaryAsync()
{
// Consulta usando FromSqlRaw sobre el DbSet correspondiente
return await TicketsSummaryResults
.FromSqlRaw("EXEC Tickets_GetSummary")
.ToListAsync();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PhOH_Tickets>().ToTable("PhOH_Tickets");
// Marcar los DbSet de resultados como entidades de solo lectura
modelBuilder.Entity<Tickets_Dashboard_Result>().HasNoKey();
modelBuilder.Entity<Tickets_GetSummary_Result>().HasNoKey();
}
}
}

View File

@ -1,131 +0,0 @@
using Data.Interfaces;
using Domain.Entities;
using Data.Entities;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
namespace Data.Repositories
{
public class TicketRepository : ITicketRepository
{
#region Declaraciones y Constructor
private readonly OperationsHubContext _dbConnection;
public TicketRepository(OperationsHubContext dbConnection)
{
_dbConnection = dbConnection;
}
#endregion
#region Metodos de clase
public async Task<ETicket> GetByIdAsync(Guid ticketId)
{
try
{
var ticket = await _dbConnection.Tickets
.FirstOrDefaultAsync(t => t.TicketId == ticketId);
if (ticket == null) return new ETicket();
var eTicket = MapEntity<PhOH_Tickets, ETicket>(ticket);
return eTicket;
}
catch (Exception ex)
{
throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} Message: {ex.Message}", ex);
}
}
public async Task<IEnumerable<ETicket>> GetAllAsync()
{
var tickets = await _dbConnection.Tickets.ToListAsync();
return tickets.Select(ticket => MapEntity<PhOH_Tickets, ETicket>(ticket));
}
public async Task InsertTicketAsync(ETicket ticket)
{
try
{
ticket.TicketId = Guid.NewGuid();
var dataTicket = MapEntity<ETicket, PhOH_Tickets>(ticket);
await _dbConnection.Tickets.AddAsync(dataTicket);
await _dbConnection.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} Message: {ex.InnerException?.Message}", ex);
}
catch (Exception ex)
{
throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} Message: {ex.Message}", ex);
}
}
//public IEnumerable<ETickets_GetSummary> GetSummary()
//{
// var summary_Results = _dbConnection.Tickets_GetSummary();
// return summary_Results.Select(item =>
// {
// var eSummary = Activator.CreateInstance<ETickets_GetSummary>();
// foreach (var propertyInfo in typeof(Tickets_GetSummary_Result).GetProperties())
// {
// var value = propertyInfo.GetValue(item);
// typeof(ETickets_GetSummary).GetProperty(propertyInfo.Name)?.SetValue(eSummary, value);
// }
// return eSummary;
// });
//}
//public IEnumerable<ETicket_Dashboard> GetTicketDashboard(string Estado, string Orden)
//{
// var ticketDashboard_Results = _dbConnection.Tickets_Dashboard(Estado, Orden);
// return (ticketDashboard_Results.Select(item =>
// {
// var eTicketDashboard = Activator.CreateInstance<ETicket_Dashboard>();
// foreach (var propertyInfo in typeof(Tickets_Dashboard_Result).GetProperties())
// {
// var value = propertyInfo.GetValue(item);
// typeof(ETicket_Dashboard).GetProperty(propertyInfo.Name)?.SetValue(eTicketDashboard, value);
// }
// return eTicketDashboard;
// }));
//}
#endregion
#region Métodos Auxiliares
private TDestination MapEntity<TSource, TDestination>(TSource source) where TDestination : new()
{
var destination = new TDestination();
foreach (var propertyInfo in typeof(TSource).GetProperties())
{
var value = propertyInfo.GetValue(source);
typeof(TDestination).GetProperty(propertyInfo.Name)?.SetValue(destination, value);
}
return destination;
}
public ETicket GetById(Guid ticketId)
{
throw new NotImplementedException();
}
public IEnumerable<ETicket> GetAll()
{
throw new NotImplementedException();
}
public void InsertTicket(ETicket ticket)
{
throw new NotImplementedException();
}
public IEnumerable<ETickets_GetSummary> GetSummary()
{
throw new NotImplementedException();
}
public IEnumerable<ETicket_Dashboard> GetTicketDashboard(string Estado, string Orden)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@ -0,0 +1,44 @@
USE [phronCare_OperationsHub]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Procedure: PhLSM_Expedition_CheckStockItemConflicts
-- Module: Logistics / Stock (PhLSM)
-- Purpose: Detect serialized stock items already assigned
-- to active expeditions.
-- Author: Leandro Rojas
-- Created: 2026-03-05
-- =============================================
ALTER PROCEDURE [dbo].[PhLSM_Expedition_CheckStockItemConflicts]
(
@StockItemIds dbo.PhLSM_StockItemIdList READONLY,
@IgnoreExpeditionId INT = NULL
)
AS
BEGIN
SET NOCOUNT ON;
SELECT
d.stockitem_id AS StockitemId,
h.id AS ExpeditionId,
h.expeditionnumber AS Expeditionnumber,
h.status AS Status
FROM dbo.PhLSM_ExpeditionDetails d
INNER JOIN @StockItemIds ids
ON ids.stockitem_id = d.stockitem_id
INNER JOIN dbo.PhLSM_ExpeditionHeaders h
ON h.id = d.expedition_id
INNER JOIN dbo.PhLSM_StockItem si
ON si.id = d.stockitem_id
WHERE h.status NOT IN (5,6) -- 5=Cerrada, 6=Anulada
AND (@IgnoreExpeditionId IS NULL OR h.id <> @IgnoreExpeditionId)
AND si.serial IS NOT NULL
AND LTRIM(RTRIM(si.serial)) <> '';
END
GO

View File

@ -0,0 +1,36 @@
USE [phronCare_OperationsHub]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Procedure: PhLSM_Stock_GetAvailabilityByStockItemIds
-- Module: Logistics / Stock (PhLSM)
-- Purpose: Returns quantity and availability data
-- for the requested stock items.
-- Author: Leandro Rojas
-- Created: 2026-03-09
-- =============================================
CREATE OR ALTER PROCEDURE [dbo].[PhLSM_Stock_GetAvailabilityByStockItemIds]
(
@StockItemIds dbo.PhLSM_StockItemIdList READONLY
)
AS
BEGIN
SET NOCOUNT ON;
SELECT
si.id AS StockitemId,
si.quantity AS Quantity,
ISNULL(si.reserved_quantity, 0) AS ReservedQuantity,
si.quantity - ISNULL(si.reserved_quantity, 0) AS AvailableQuantity,
si.serial AS Serial
FROM dbo.PhLSM_StockItem si
INNER JOIN @StockItemIds ids
ON ids.stockitem_id = si.id;
END
GO

View File

@ -0,0 +1,5 @@
CREATE TYPE dbo.PhLSM_StockItemIdList AS TABLE
(
stockitem_id INT NOT NULL
);
GO

View File

@ -0,0 +1,135 @@
/* =========================================================
VERIFICACIÓN STORY #9
Expedición -> EnTransito + reservas de stock
========================================================= */
SET NOCOUNT ON;
DECLARE @ExpeditionNumber VARCHAR(50) = 'X-00000054'; -- cambiar
DECLARE @ExpeditionId INT;
SELECT @ExpeditionId = id
FROM dbo.PhLSM_ExpeditionHeaders
WHERE expeditionnumber = @ExpeditionNumber;
IF @ExpeditionId IS NULL
BEGIN
RAISERROR('Expedición no encontrada',16,1);
RETURN;
END
PRINT 'ExpeditionId: ' + CAST(@ExpeditionId AS VARCHAR);
-------------------------------------------------------------
-- 1. CABECERA
-------------------------------------------------------------
PRINT '===== CABECERA =====';
SELECT
id,
expeditionnumber,
issuedate,
status,
observations
FROM dbo.PhLSM_ExpeditionHeaders
WHERE id = @ExpeditionId;
-------------------------------------------------------------
-- 2. DETALLES
-------------------------------------------------------------
PRINT '===== DETALLES =====';
SELECT
d.id,
d.expedition_id,
d.product_id,
d.stockitem_id,
d.quantity,
d.batch,
d.serial,
d.expiration
FROM dbo.PhLSM_ExpeditionDetails d
WHERE d.expedition_id = @ExpeditionId
ORDER BY d.id;
-------------------------------------------------------------
-- 3. RESERVAS CREADAS
-------------------------------------------------------------
PRINT '===== RESERVAS =====';
SELECT
r.id,
r.source_type,
r.source_id,
r.stockitem_id,
r.reserved_quantity,
r.status,
r.createdat
FROM dbo.PhLSM_StockReservation r
WHERE r.source_type = 1
AND r.source_id = @ExpeditionId
ORDER BY r.id;
-------------------------------------------------------------
-- 4. STOCK ITEMS AFECTADOS
-------------------------------------------------------------
PRINT '===== STOCK ITEMS =====';
SELECT
si.id,
si.product_id,
si.location_id,
si.quantity,
si.reserved_quantity,
(si.quantity - si.reserved_quantity) AS available
FROM dbo.PhLSM_StockItem si
WHERE si.id IN
(
SELECT stockitem_id
FROM dbo.PhLSM_ExpeditionDetails
WHERE expedition_id = @ExpeditionId
);
-------------------------------------------------------------
-- 5. VALIDACIÓN DETALLE VS RESERVA
-------------------------------------------------------------
PRINT '===== VALIDACION =====';
SELECT
d.id AS detail_id,
d.stockitem_id,
d.quantity AS requested,
r.reserved_quantity,
CASE
WHEN r.id IS NULL THEN 'FALTA RESERVA'
WHEN r.reserved_quantity <> d.quantity THEN 'CANTIDAD DISTINTA'
ELSE 'OK'
END AS resultado
FROM dbo.PhLSM_ExpeditionDetails d
LEFT JOIN dbo.PhLSM_StockReservation r
ON r.source_id = d.expedition_id
AND r.stockitem_id = d.stockitem_id
AND r.source_type = 1
AND r.status = 1
WHERE d.expedition_id = @ExpeditionId;
-------------------------------------------------------------
-- 6. CHEQUEO DE DUPLICADOS
-------------------------------------------------------------
PRINT '===== DUPLICADOS =====';
SELECT
stockitem_id,
COUNT(*) AS cantidad
FROM dbo.PhLSM_StockReservation
WHERE source_type = 1
AND source_id = @ExpeditionId
AND status = 1
GROUP BY stockitem_id
HAVING COUNT(*) > 1;

View File

@ -4,9 +4,9 @@
{ {
Quote, Quote,
Expedition, Expedition,
DeliveryNote,
Invoice, Invoice,
Order, Order,
Remito,
Certificate Certificate
} }
} }

View File

@ -2,6 +2,7 @@
using Documents.Interfaces; using Documents.Interfaces;
using Documents.Models; using Documents.Models;
using Domain.Dtos; // QuoteDto using Domain.Dtos; // QuoteDto
using Domain.Dtos.Sales; // DeliveryNoteDto
using Domain.Dtos.Stock; // ExpeditionDto using Domain.Dtos.Stock; // ExpeditionDto
using Transversal.Interfaces; using Transversal.Interfaces;
@ -57,6 +58,7 @@ public class DocumentTemplateService : IDocumentTemplateService
private static string ResolveTemplate(DocumentType type) => type switch private static string ResolveTemplate(DocumentType type) => type switch
{ {
DocumentType.Quote => "Quotes/Template_v1.cshtml", DocumentType.Quote => "Quotes/Template_v1.cshtml",
DocumentType.DeliveryNote => "DeliveryNotes/Template_v1.cshtml",
DocumentType.Expedition => "Expeditions/Template_v1.cshtml", DocumentType.Expedition => "Expeditions/Template_v1.cshtml",
_ => "Shared/Template_Generic.cshtml" _ => "Shared/Template_Generic.cshtml"
}; };
@ -72,6 +74,9 @@ public class DocumentTemplateService : IDocumentTemplateService
case ExpeditionDto e: case ExpeditionDto e:
e.LogoBase64 = base64; e.LogoBase64 = base64;
break; break;
case DeliveryNoteDto d:
d.LogoBase64 = base64;
break;
default: default:
// Si no tiene LogoBase64, no hacemos nada. // Si no tiene LogoBase64, no hacemos nada.
break; break;

View File

@ -0,0 +1,182 @@
@using System
@using System.Globalization
@using System.Text.Json
@using Domain.Dtos.Sales
@model DeliveryNoteDto
@{
Layout = null;
var ci = CultureInfo.GetCultureInfo("es-AR");
CultureInfo.CurrentCulture = ci;
CultureInfo.CurrentUICulture = ci;
SurgerySnapshot snap;
if (string.IsNullOrWhiteSpace(Model.ExtraInfoJson))
{
snap = new SurgerySnapshot();
}
else
{
try
{
snap = JsonSerializer.Deserialize<SurgerySnapshot>(Model.ExtraInfoJson) ?? new SurgerySnapshot();
}
catch
{
snap = new SurgerySnapshot();
}
}
var reprintText = Model.PrintCount > 0 ? (" — Reimpresión " + Model.PrintCount) : string.Empty;
}
@functions {
public class SurgerySnapshot
{
public string? Professional { get; set; }
public string? Institution { get; set; }
public string? Patient { get; set; }
public DateTime? SurgeryDate { get; set; }
}
public static string FQty(decimal q) => q.ToString("G29", CultureInfo.InvariantCulture);
public static string FDate(DateTime? d) => d.HasValue ? d.Value.ToString("dd/MM/yyyy") : string.Empty;
public static string FText(string? value) => string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
public static string FOrigin(byte originType) => originType switch
{
1 => "Presupuesto",
2 => "Manual",
_ => originType.ToString()
};
}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Remito @Model.DeliveryNoteNumber</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
@@page { size: A4; margin: 10mm 9mm 10mm 9mm; }
html, body { font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #000; }
.sheet { width: 100%; }
.header { display: grid; grid-template-columns: 1.6fr 1fr; gap: 10px; align-items: start; }
.company { line-height: 1.2; }
.company .tagline { margin-top: 4px; font-size: 11px; }
.doc-title { text-align: right; }
.doc-title h1 { font-size: 22px; margin: 0 0 4px 0; letter-spacing: .4px; }
.doc-title .num { font-weight: bold; font-size: 14px; }
.doc-title .date { margin-top: 3px; }
.hr { border-bottom: 1px solid #000; margin: 8px 0; }
.info-block table, .snapshot table, .items table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.info-block td, .snapshot td { padding: 3px 4px; vertical-align: top; }
.info-block .lbl, .snapshot .lbl { width: 18%; font-weight: 600; }
.info-block .val, .snapshot .val { width: 32%; word-break: break-word; }
.section-title { font-size: 13px; font-weight: 700; margin: 10px 0 6px; }
.items { margin-top: 8px; }
.items thead th { border: 1px solid #000; background: #eaeaea; color: #000; padding: 4px 4px; text-align: center; font-weight: 700; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.items tbody td { border: 1px solid #000; padding: 4px 4px; vertical-align: top; }
.items thead { display: table-header-group; }
.col-line { width: 7%; text-align: center; }
.col-desc { width: 43%; text-align: left; }
.col-qty { width: 10%; text-align: center; }
.col-origin { width: 14%; text-align: center; }
.col-originid { width: 10%; text-align: center; }
.col-notes { width: 16%; text-align: left; word-break: break-word; }
.observ { margin-top: 10px; min-height: 52px; border: 1px dashed #888; padding: 8px; white-space: pre-wrap; }
.footer { margin-top: 12px; padding-top: 6px; border-top: 1px solid #000; font-size: 11px; text-align: center; }
.muted { color: #111; }
.avoid-break { page-break-inside: avoid; }
</style>
</head>
<body>
<div class="sheet">
<div class="header">
<div class="company">
@if (!string.IsNullOrWhiteSpace(Model.LogoBase64))
{
<img src="data:image/png;base64,@Model.LogoBase64" alt="Logo" style="height:48px; margin-bottom:2px;" />
}
<div class="tagline muted">Documento generado por PhronCare</div>
</div>
<div class="doc-title">
<h1>Remito</h1>
<div class="num">@Model.DeliveryNoteNumber@reprintText</div>
<div class="date">Fecha: @Model.IssueDate.ToString("dd/MM/yyyy")</div>
</div>
</div>
<div class="hr"></div>
<div class="info-block avoid-break">
<table>
<tr>
<td class="lbl">Cliente</td>
<td class="val">@FText(Model.CustomerName)</td>
<td class="lbl">Estado</td>
<td class="val">@FText(Model.Status)</td>
</tr>
<tr>
<td class="lbl">Presupuesto</td>
<td class="val">@FText(Model.QuoteNumber)</td>
<td class="lbl">ID interno</td>
<td class="val">@Model.Id</td>
</tr>
</table>
</div>
<div class="section-title">Contexto clínico</div>
<div class="snapshot avoid-break">
<table>
<tr>
<td class="lbl">Profesional</td>
<td class="val">@FText(snap.Professional)</td>
<td class="lbl">Institución</td>
<td class="val">@FText(snap.Institution)</td>
</tr>
<tr>
<td class="lbl">Paciente</td>
<td class="val">@FText(snap.Patient)</td>
<td class="lbl">Fecha cirugía</td>
<td class="val">@FDate(snap.SurgeryDate)</td>
</tr>
</table>
</div>
<div class="section-title">Detalle de ítems</div>
<div class="items">
<table>
<thead>
<tr>
<th class="col-line">#</th>
<th class="col-desc">Descripción</th>
<th class="col-qty">Cantidad</th>
<th class="col-origin">Origen</th>
<th class="col-originid">Ref.</th>
<th class="col-notes">Notas</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items.OrderBy(i => i.LineNumber))
{
<tr>
<td class="col-line">@item.LineNumber</td>
<td class="col-desc">@FText(item.Description)</td>
<td class="col-qty">@FQty(item.Quantity)</td>
<td class="col-origin">@FOrigin(item.OriginType)</td>
<td class="col-originid">@(item.OriginId?.ToString() ?? "-")</td>
<td class="col-notes">@FText(item.Notes)</td>
</tr>
}
</tbody>
</table>
</div>
<div class="section-title">Observaciones</div>
<div class="observ">@FText(Model.Observations)</div>
<div class="footer">Impreso el @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</div>
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
namespace Domain.Constants
{
public enum DeliveryNoteItemOriginType : byte
{
QuoteDetail = 1,
SalesProduct = 2,
StockProduct = 3,
Manual = 4
}
}

View File

@ -0,0 +1,10 @@
namespace Domain.Constants
{
public enum SalesDocumentCoverageType : int
{
Direct = 1,
Capita = 2,
Adjustment = 3,
Manual = 4
}
}

View File

@ -0,0 +1,11 @@
namespace Domain.Constants
{
public enum SalesDocumentOriginType : int
{
Manual = 1,
QuoteDetail = 2,
Adjustment = 3,
Capita = 4,
DeliveryNote = 5
}
}

View File

@ -0,0 +1,18 @@
namespace Domain.Constants
{
public static class SalesDocumentOriginTypeExtensions
{
public static string ToStorageCode(this SalesDocumentOriginType originType)
{
return originType switch
{
SalesDocumentOriginType.Manual => "MANUAL",
SalesDocumentOriginType.QuoteDetail => "QUOTE",
SalesDocumentOriginType.Adjustment => "ADJUSTMENT",
SalesDocumentOriginType.Capita => "CAPITA",
SalesDocumentOriginType.DeliveryNote => "DELIVERY_NOTE",
_ => throw new ArgumentOutOfRangeException(nameof(originType), originType, "Tipo de origen de documento de venta no soportado.")
};
}
}
}

View File

@ -0,0 +1,10 @@
namespace Domain.Constants
{
public enum SalesDocumentStatus : int
{
Draft = 1,
Validated = 2,
Issued = 3,
Cancelled = 4
}
}

View File

@ -0,0 +1,12 @@
namespace Domain.Constants
{
public enum SalesDocumentType : int
{
Invoice = 1,
DebitNote = 2,
CreditNote = 3,
CreditInvoice = 4,
CreditDebitNote = 5,
CreditCreditNote = 6
}
}

View File

@ -0,0 +1,12 @@
namespace Domain.Constants
{
public enum SalesFiscalDocumentStatus : int
{
None = 0,
Pending = 1,
Authorized = 2,
Rejected = 3,
Error = 4,
PendingReconciliation = 5
}
}

View File

@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Dtos\Sales\" />
</ItemGroup>
</Project> </Project>

View File

@ -36,5 +36,25 @@
/// Total del ítem (Subtotal + TaxAmount). /// Total del ítem (Subtotal + TaxAmount).
/// </summary> /// </summary>
public decimal Total { get; set; } public decimal Total { get; set; }
/// <summary>
/// Indica si el renglón fue aprobado durante el proceso de autorización.
/// </summary>
public bool Approved { get; set; }
/// <summary>
/// Cantidad aprobada para el renglón. Puede diferir de la cantidad originalmente cotizada.
/// </summary>
public int? ApprovedQuantity { get; set; }
/// <summary>
/// Precio unitario aprobado para el renglón.
/// </summary>
public decimal? ApprovedUnitPrice { get; set; }
/// <summary>
/// Importe total aprobado para el renglón.
/// </summary>
public decimal? ApprovedAmount { get; set; }
} }
} }

View File

@ -0,0 +1,12 @@
namespace Domain.Dtos.Sales
{
public class DeliveryNoteCreateItemRequest
{
public byte OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? Notes { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace Domain.Dtos.Sales
{
public class DeliveryNoteCreateRequest
{
public string DeliveryNoteNumber { get; set; } = string.Empty;
public int? QuoteId { get; set; }
public DateTime IssueDate { get; set; }
public int CustomerId { get; set; }
public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; }
public List<DeliveryNoteCreateItemRequest> Items { get; set; } = new();
}
}

View File

@ -0,0 +1,8 @@
namespace Domain.Dtos.Sales
{
public class DeliveryNoteCreateResponse
{
public int Id { get; set; }
public string DeliveryNoteNumber { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Domain.Dtos.Sales
{
/// <summary>
/// DTO de lectura para Delivery Note.
/// Representa la cabecera del remito con su detalle de ítems.
/// </summary>
public class DeliveryNoteDto
{
public int Id { get; set; }
public string DeliveryNoteNumber { get; set; } = string.Empty;
public string CustomerName { get; set; } = string.Empty;
public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public int? SalesInvoiceId { get; set; }
public DateTime IssueDate { get; set; }
public int CustomerId { get; set; }
public string Status { get; set; } = string.Empty;
public string? LogoBase64 { get; set; }
public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; }
public int PrintCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public List<DeliveryNoteItemDto> Items { get; set; } = new();
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace Domain.Dtos.Sales
{
/// <summary>
/// DTO de lectura para el detalle de un Delivery Note.
/// </summary>
public class DeliveryNoteItemDto
{
public int Id { get; set; }
public int DeliverynoteId { get; set; }
public int LineNumber { get; set; }
public byte OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string Notes { get; set; } = string.Empty;
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace Domain.Dtos.Sales
{
public class DeliveryNoteSummaryDto
{
public int Id { get; set; }
public string DeliveryNoteNumber { get; set; } = string.Empty;
public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public DateTime IssueDate { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string? Observations { get; set; }
public int PrintCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
}
}

View File

@ -0,0 +1,19 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentCoverageDto
{
public int Id { get; set; }
public int SalesDocumentId { get; set; }
public int? SalesDocumentDetailId { get; set; }
public int QuoteId { get; set; }
public int? QuoteDetailId { get; set; }
public int CoverageType { get; set; }
public decimal? CoveragePercentage { get; set; }
public decimal? CoverageAmount { get; set; }
public DateTime? PeriodFrom { get; set; }
public DateTime? PeriodTo { get; set; }
public string? Notes { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentCreateCoverageRequest
{
public int? SalesDocumentDetailId { get; set; }
public int QuoteId { get; set; }
public int? QuoteDetailId { get; set; }
public int CoverageType { get; set; }
public decimal? CoveragePercentage { get; set; }
public decimal? CoverageAmount { get; set; }
public DateTime? PeriodFrom { get; set; }
public DateTime? PeriodTo { get; set; }
public string? Notes { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using Domain.Constants;
namespace Domain.Dtos.Sales
{
public class SalesDocumentCreateDetailRequest
{
public int LineNumber { get; set; }
public SalesDocumentOriginType OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public int? ProductId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal? AuthorizedUnitPrice { get; set; }
public decimal? AuthorizedAmount { get; set; }
public decimal? BilledPercentage { get; set; }
public decimal UnitPrice { get; set; }
public decimal NetAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TotalAmount { get; set; }
public string? OriginSnapshotJson { get; set; }
}
}

View File

@ -0,0 +1,25 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentCreateRequest
{
public int? FormseriesId { get; set; }
public int DocumentType { get; set; }
public int? FiscalVoucherType { get; set; }
public string? FiscalVoucherLetter { get; set; }
public int? QuoteId { get; set; }
public int CustomerId { get; set; }
public int BillToCustomerId { get; set; }
public DateTime? IssueDate { get; set; }
public string Currency { get; set; } = string.Empty;
public decimal ExchangeRate { get; set; }
public string? AssociatedDocumentType { get; set; }
public string? AssociatedDocumentNumber { get; set; }
public DateTime? AssociatedDocumentDate { get; set; }
public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; }
public DateTime? PeriodFrom { get; set; }
public DateTime? PeriodTo { get; set; }
public List<SalesDocumentCreateDetailRequest> Details { get; set; } = new();
public List<SalesDocumentCreateCoverageRequest> Coverage { get; set; } = new();
}
}

View File

@ -0,0 +1,8 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentCreateResponse
{
public int Id { get; set; }
public string? InternalDocumentNumber { get; set; }
}
}

View File

@ -0,0 +1,25 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentDetailDto
{
public int Id { get; set; }
public int SalesDocumentId { get; set; }
public int LineNumber { get; set; }
public string OriginType { get; set; } = string.Empty;
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public int? ProductId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal? AuthorizedUnitPrice { get; set; }
public decimal? AuthorizedAmount { get; set; }
public decimal? BilledPercentage { get; set; }
public decimal UnitPrice { get; set; }
public decimal NetAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TotalAmount { get; set; }
public string? OriginSnapshotJson { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
}
}

View File

@ -0,0 +1,37 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentDto
{
public int Id { get; set; }
public int? FormseriesId { get; set; }
public int? InternalSequenceNumber { get; set; }
public string? InternalDocumentNumber { get; set; }
public int DocumentType { get; set; }
public int? FiscalVoucherType { get; set; }
public string? FiscalVoucherLetter { get; set; }
public int Status { get; set; }
public int? QuoteId { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public int BillToCustomerId { get; set; }
public string BillToCustomerName { get; set; } = string.Empty;
public DateTime? IssueDate { get; set; }
public string Currency { get; set; } = string.Empty;
public decimal ExchangeRate { get; set; }
public decimal NetAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TotalAmount { get; set; }
public string? AssociatedDocumentType { get; set; }
public string? AssociatedDocumentNumber { get; set; }
public DateTime? AssociatedDocumentDate { get; set; }
public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; }
public DateTime? PeriodFrom { get; set; }
public DateTime? PeriodTo { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public List<SalesDocumentDetailDto> Details { get; set; } = new();
public List<SalesDocumentCoverageDto> Coverage { get; set; } = new();
public SalesFiscalDocumentDto? FiscalDocument { get; set; }
}
}

View File

@ -0,0 +1,24 @@
namespace Domain.Dtos.Sales
{
public class SalesDocumentSummaryDto
{
public int Id { get; set; }
public string? InternalDocumentNumber { get; set; }
public int DocumentType { get; set; }
public int Status { get; set; }
public int? QuoteId { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public int BillToCustomerId { get; set; }
public string BillToCustomerName { get; set; } = string.Empty;
public DateTime? IssueDate { get; set; }
public string Currency { get; set; } = string.Empty;
public decimal NetAmount { get; set; }
public decimal TaxAmount { get; set; }
public decimal TotalAmount { get; set; }
public DateTime? PeriodFrom { get; set; }
public DateTime? PeriodTo { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Domain.Dtos.Sales
{
public class SalesFiscalDocumentAssociationDto
{
public int Id { get; set; }
public int SalesFiscalDocumentId { get; set; }
public int? AssociatedSalesDocumentId { get; set; }
public int AssociatedVoucherType { get; set; }
public short AssociatedPointOfSale { get; set; }
public int AssociatedVoucherNumber { get; set; }
public string? AssociatedVoucherCuit { get; set; }
public DateTime? AssociatedVoucherDate { get; set; }
public DateTime Createdat { get; set; }
}
}

View File

@ -0,0 +1,29 @@
namespace Domain.Dtos.Sales
{
public class SalesFiscalDocumentDto
{
public int Id { get; set; }
public int SalesDocumentId { get; set; }
public int FiscalStatus { get; set; }
public string Environment { get; set; } = string.Empty;
public short? PointOfSale { get; set; }
public int? VoucherType { get; set; }
public string? VoucherLetter { get; set; }
public int? VoucherNumber { get; set; }
public string? Cae { get; set; }
public DateTime? CaeExpirationDate { get; set; }
public string? RequestFingerprint { get; set; }
public bool IsFinal { get; set; }
public string? ArcaRequestPayloadJson { get; set; }
public string? ArcaResponsePayloadJson { get; set; }
public string? ErrorsJson { get; set; }
public string? EventsJson { get; set; }
public string? ObservationsJson { get; set; }
public DateTime? AttemptedAtUtc { get; set; }
public DateTime? CompletedAtUtc { get; set; }
public bool ReconciledAfterTimeout { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public List<SalesFiscalDocumentAssociationDto> Associations { get; set; } = new();
}
}

View File

@ -0,0 +1,11 @@
namespace Domain.Dtos.Stock
{
public sealed class StockItemAvailabilityDto
{
public int StockitemId { get; set; }
public decimal Quantity { get; set; }
public decimal ReservedQuantity { get; set; }
public decimal AvailableQuantity { get; set; }
public string? Serial { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace Domain.Dtos.Stock
{
public sealed class StockItemExpeditionConflictDto
{
public int StockitemId { get; set; }
public int ExpeditionId { get; set; }
public string Expeditionnumber { get; set; } = string.Empty;
public int Status { get; set; }
}
}

View File

@ -7,6 +7,7 @@
public class StockSnapshotItem public class StockSnapshotItem
{ {
public int ProductId { get; set; } public int ProductId { get; set; }
public int StockitemId { get; set; }
public string? ProductName { get; set; } = string.Empty; public string? ProductName { get; set; } = string.Empty;
public int LocationId { get; set; } public int LocationId { get; set; }
public string Batch { get; set; } = string.Empty; public string Batch { get; set; } = string.Empty;

View File

@ -0,0 +1,31 @@
namespace Domain.Entities
{
public class EDeliveryNote
{
public int Id { get; set; }
public string Deliverynotenumber { get; set; } = null!;
public int? QuoteId { get; set; }
public int? SalesinvoiceId { get; set; }
public DateTime Issuedate { get; set; }
public int CustomerId { get; set; }
public string Status { get; set; } = null!;
public string? Observations { get; set; }
public string? ExtrainfoJson { get; set; }
public int Printcount { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<EDeliveryNoteDetail> PhSDeliveryNoteDetails { get; set; } = new List<EDeliveryNoteDetail>();
}
}

View File

@ -0,0 +1,32 @@
namespace Domain.Entities
{
public partial class EDeliveryNoteDetail
{
public int Id { get; set; }
public int DeliverynoteId { get; set; }
public int LineNumber { get; set; }
public byte OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public string Description { get; set; } = null!;
public decimal Quantity { get; set; }
public string Notes { get; set; } = null!;
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
//public virtual EDeliveryNote Deliverynote { get; set; } = null!;
//public virtual EQuoteDetail? QuoteDetail { get; set; }
}
}

View File

@ -17,6 +17,11 @@
/// </summary> /// </summary>
public int ProductId { get; set; } public int ProductId { get; set; }
/// <summary>
/// Referencia a StockItem (PhLSM_StockItem)
/// </summary>
public int StockitemId { get; set; }
/// <summary> /// <summary>
/// Cantidad solicitada del producto /// Cantidad solicitada del producto
/// </summary> /// </summary>

View File

@ -0,0 +1,135 @@
namespace Domain.Entities
{
public partial class ESalesDocument
{
/// <summary>
/// Identificador interno del documento comercial.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Talonario/serie interna existente en PhronCare. Reutiliza PhS_FormSeries para numeracion interna.
/// </summary>
public int? FormseriesId { get; set; }
/// <summary>
/// Numero secuencial interno asignado al emitir internamente el documento. No corresponde al numero fiscal ARCA.
/// </summary>
public int? InternalSequenceNumber { get; set; }
/// <summary>
/// Numero visible interno del documento, formado desde la serie/talonario interno. Puede diferir del numero fiscal.
/// </summary>
public string? InternalDocumentNumber { get; set; }
/// <summary>
/// Tipo comercial interno del documento. Ejemplos: Invoice, DebitNote, CreditNote, CreditInvoice, CreditDebitNote, CreditCreditNote.
/// </summary>
public int DocumentType { get; set; }
/// <summary>
/// Tipo de comprobante fiscal AFIP/ARCA previsto para autorizacion futura. Ejemplos: 1, 6, 11, 201, 202, 203.
/// </summary>
public int? FiscalVoucherType { get; set; }
/// <summary>
/// Letra fiscal prevista del comprobante: A, B, C u otras segun configuracion fiscal.
/// </summary>
public string? FiscalVoucherLetter { get; set; }
/// <summary>
/// Estado comercial interno. Ejemplos: Draft, Validated, Issued, Cancelled. Independiente del estado fiscal.
/// </summary>
public int Status { get; set; }
/// <summary>
/// Presupuesto origen opcional. Puede ser NULL para ventas manuales o de escritorio.
/// </summary>
public int? QuoteId { get; set; }
/// <summary>
/// Cliente origen de la operacion comercial.
/// </summary>
public int CustomerId { get; set; }
/// <summary>
/// Cliente al que se factura realmente. Permite escenarios obra social / particular u otros terceros pagadores.
/// </summary>
public int BillToCustomerId { get; set; }
/// <summary>
/// Fecha de emision interna del documento comercial.
/// </summary>
public DateTime? IssueDate { get; set; }
/// <summary>
/// Moneda del documento comercial.
/// </summary>
public string Currency { get; set; } = null!;
/// <summary>
/// Cotizacion utilizada para la moneda del documento.
/// </summary>
public decimal ExchangeRate { get; set; }
/// <summary>
/// Importe neto total del documento.
/// </summary>
public decimal NetAmount { get; set; }
/// <summary>
/// Importe total de impuestos del documento.
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// Importe total del documento.
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// Tipo de documento interno asociado opcional, por ejemplo remito, orden de compra o autorizacion. No representa CbtesAsoc fiscal.
/// </summary>
public string? AssociatedDocumentType { get; set; }
/// <summary>
/// Numero del documento interno asociado opcional.
/// </summary>
public string? AssociatedDocumentNumber { get; set; }
/// <summary>
/// Fecha del documento interno asociado opcional.
/// </summary>
public DateTime? AssociatedDocumentDate { get; set; }
/// <summary>
/// Observaciones comerciales del documento.
/// </summary>
public string? Observations { get; set; }
/// <summary>
/// Snapshot JSON con informacion extra contextual del documento.
/// </summary>
public string? ExtraInfoJson { get; set; }
/// <summary>
/// Fecha inicial del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.
/// </summary>
public DateTime? PeriodFrom { get; set; }
/// <summary>
/// Fecha final del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.
/// </summary>
public DateTime? PeriodTo { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<ESalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<ESalesDocumentCoverage>();
public virtual ICollection<ESalesDocumentDetail> PhSSalesDocumentDetails { get; set; } = new List<ESalesDocumentDetail>();
public virtual ICollection<ESalesFiscalDocumentAssociation> PhSSalesFiscalDocumentAssociations { get; set; } = new List<ESalesFiscalDocumentAssociation>();
}
}

View File

@ -0,0 +1,65 @@
namespace Domain.Entities
{
public partial class ESalesDocumentCoverage
{
/// <summary>
/// Identificador interno de la cobertura.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Documento de venta que cubre el presupuesto/caso.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Detalle del documento de venta asociado a esta cobertura, cuando aplique. En capita puede apuntar a la linea agregada mensual.
/// </summary>
public int? SalesdocumentdetailId { get; set; }
/// <summary>
/// Presupuesto/caso cubierto por el documento de venta. Se usa tanto para facturacion directa como para capita.
/// </summary>
public int QuoteId { get; set; }
/// <summary>
/// Detalle de presupuesto cubierto, cuando se requiera trazabilidad granular por item.
/// </summary>
public int? QuoteDetailId { get; set; }
/// <summary>
/// Tipo de cobertura. Valores esperados en Domain: Direct, Capita, Adjustment.
/// </summary>
public int CoverageType { get; set; }
/// <summary>
/// Porcentaje del presupuesto/caso cubierto por el documento. Permite 100% en facturacion directa o particiones 60/40.
/// </summary>
public decimal? CoveragePercentage { get; set; }
/// <summary>
/// Importe de referencia cubierto por el documento, cuando aplique.
/// </summary>
public decimal? CoverageAmount { get; set; }
/// <summary>
/// Fecha inicial del periodo de cobertura.
/// </summary>
public DateTime? PeriodFrom { get; set; }
/// <summary>
/// Fecha final del periodo de cobertura.
/// </summary>
public DateTime? PeriodTo { get; set; }
/// <summary>
/// Notas internas de cobertura.
/// </summary>
public string? Notes { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
}
}

View File

@ -0,0 +1,94 @@
namespace Domain.Entities
{
public partial class ESalesDocumentDetail
{
public int Id { get; set; }
/// <summary>
/// Documento comercial al que pertenece el detalle.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Numero de linea dentro del documento.
/// </summary>
public int LineNumber { get; set; }
/// <summary>
/// Origen logico del item. Persistir como codigo semantico: MANUAL, QUOTE, DELIVERY_NOTE, CAPITA o ADJUSTMENT.
/// </summary>
public string OriginType { get; set; } = null!;
/// <summary>
/// Identificador generico del origen cuando aplique.
/// </summary>
public int? OriginId { get; set; }
/// <summary>
/// Detalle del presupuesto aprobado que origina la linea, cuando exista. Puede ser NULL en ventas manuales.
/// </summary>
public int? QuoteDetailId { get; set; }
/// <summary>
/// Producto asociado a la linea, si aplica.
/// </summary>
public int? ProductId { get; set; }
/// <summary>
/// Descripcion visible de la linea facturada.
/// </summary>
public string Description { get; set; } = null!;
/// <summary>
/// Cantidad facturada.
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Precio unitario autorizado o de referencia proveniente del origen comercial.
/// </summary>
public decimal? AuthorizedUnitPrice { get; set; }
/// <summary>
/// Importe autorizado o de referencia proveniente del origen comercial.
/// </summary>
public decimal? AuthorizedAmount { get; set; }
/// <summary>
/// Porcentaje facturado sobre el origen. Permite facturacion parcial obra social / particular.
/// </summary>
public decimal? BilledPercentage { get; set; }
/// <summary>
/// Precio unitario efectivo de la linea del documento.
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// Importe neto de la linea.
/// </summary>
public decimal NetAmount { get; set; }
/// <summary>
/// Importe de impuestos de la linea.
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// Importe total de la linea.
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// Snapshot JSON del origen de la linea para trazabilidad historica.
/// </summary>
public string? OriginSnapshotJson { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<ESalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<ESalesDocumentCoverage>();
}
}

View File

@ -0,0 +1,108 @@
namespace Domain.Entities
{
public partial class ESalesFiscalDocument
{
public int Id { get; set; }
/// <summary>
/// Documento comercial interno vinculado al documento fiscal.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Estado fiscal independiente del estado comercial. Ejemplos: None, Pending, Authorized, Rejected, Error, PendingReconciliation.
/// </summary>
public int FiscalStatus { get; set; }
/// <summary>
/// Ambiente fiscal usado para autorizacion: homologacion, produccion u otro valor definido por configuracion.
/// </summary>
public string Environment { get; set; } = null!;
/// <summary>
/// Punto de venta fiscal ARCA/AFIP.
/// </summary>
public short? PointOfSale { get; set; }
/// <summary>
/// Tipo de comprobante fiscal ARCA/AFIP utilizado en FECAESolicitar.
/// </summary>
public int? VoucherType { get; set; }
/// <summary>
/// Letra fiscal del comprobante autorizado o a autorizar.
/// </summary>
public string? VoucherLetter { get; set; }
/// <summary>
/// Numero fiscal del comprobante asignado para ARCA. Se mantiene separado del numero interno.
/// </summary>
public int? VoucherNumber { get; set; }
/// <summary>
/// Codigo de autorizacion electronico obtenido desde ARCA/AFIP.
/// </summary>
public string? Cae { get; set; }
/// <summary>
/// Fecha de vencimiento del CAE.
/// </summary>
public DateTime? CaeExpirationDate { get; set; }
/// <summary>
/// Huella de idempotencia fiscal para evitar duplicacion de solicitudes ante ARCA.
/// </summary>
public string? RequestFingerprint { get; set; }
/// <summary>
/// Indica si el resultado fiscal es final y no debe volver a mutar salvo procesos controlados de auditoria.
/// </summary>
public bool IsFinal { get; set; }
/// <summary>
/// Payload JSON enviado a ARCA/AFIP.
/// </summary>
public string? ArcaRequestPayloadJson { get; set; }
/// <summary>
/// Payload JSON recibido desde ARCA/AFIP.
/// </summary>
public string? ArcaResponsePayloadJson { get; set; }
/// <summary>
/// Errores devueltos por ARCA/AFIP serializados como JSON.
/// </summary>
public string? ErrorsJson { get; set; }
/// <summary>
/// Eventos devueltos por ARCA/AFIP serializados como JSON.
/// </summary>
public string? EventsJson { get; set; }
/// <summary>
/// Observaciones devueltas por ARCA/AFIP serializadas como JSON.
/// </summary>
public string? ObservationsJson { get; set; }
/// <summary>
/// Fecha/hora UTC del intento de autorizacion fiscal.
/// </summary>
public DateTime? AttemptedAtUtc { get; set; }
/// <summary>
/// Fecha/hora UTC de finalizacion del flujo fiscal.
/// </summary>
public DateTime? CompletedAtUtc { get; set; }
/// <summary>
/// Indica que el documento fiscal fue resuelto mediante reconciliacion posterior a timeout o resultado ambiguo.
/// </summary>
public bool ReconciledAfterTimeout { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<ESalesFiscalDocumentAssociation> PhSSalesFiscalDocumentAssociations { get; set; } = new List<ESalesFiscalDocumentAssociation>();
}
}

View File

@ -0,0 +1,46 @@
namespace Domain.Entities
{
public partial class ESalesFiscalDocumentAssociation
{
public int Id { get; set; }
/// <summary>
/// Documento fiscal que contiene esta asociacion.
/// </summary>
public int SalesfiscaldocumentId { get; set; }
/// <summary>
/// Documento comercial interno asociado, si existe dentro de PhronCare.
/// </summary>
public int? AssociatedSalesdocumentId { get; set; }
/// <summary>
/// Tipo fiscal ARCA/AFIP del comprobante asociado.
/// </summary>
public int AssociatedVoucherType { get; set; }
/// <summary>
/// Punto de venta fiscal del comprobante asociado.
/// </summary>
public short AssociatedPointOfSale { get; set; }
/// <summary>
/// Numero fiscal del comprobante asociado.
/// </summary>
public int AssociatedVoucherNumber { get; set; }
/// <summary>
/// CUIT emisor del comprobante asociado, cuando sea requerido por ARCA.
/// </summary>
public string? AssociatedVoucherCuit { get; set; }
/// <summary>
/// Fecha del comprobante fiscal asociado.
/// </summary>
public DateTime? AssociatedVoucherDate { get; set; }
public DateTime Createdat { get; set; }
public virtual ESalesDocument? AssociatedSalesdocument { get; set; }
}
}

View File

@ -0,0 +1,14 @@
namespace Domain.Generics
{
public class DeliveryNoteSearchParams : PagedRequest
{
public string? DeliveryNoteNumber { get; set; }
public int? CustomerId { get; set; }
public string? CustomerText { get; set; }
public int? QuoteId { get; set; }
public string? QuoteNumber { get; set; }
public DateTime? IssueDateFrom { get; set; }
public DateTime? IssueDateTo { get; set; }
public string? Status { get; set; }
}
}

View File

@ -7,12 +7,28 @@ namespace Models.Interfaces
// 1.1 Data (Repo) // 1.1 Data (Repo)
public interface IExpeditionRepository public interface IExpeditionRepository
{ {
/// <summary>
/// Verifica si alguno de los stock items indicados ya está asociado
/// a otra expedición activa. Utilizado para prevenir doble traza.
/// </summary>
/// <param name="stockItemIds">Lista de stockitem_id a validar.</param>
/// <param name="ignoreExpeditionId">
/// Expedición a ignorar (usado en edición para no detectar conflicto consigo misma).
/// </param>
/// <returns>Lista de conflictos encontrados.</returns>
Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(IEnumerable<int> stockItemIds, int? ignoreExpeditionId);
/// <summary> /// <summary>
/// Crea la expedición completa (encabezado + detalles) y la deja emitida con numeración de serie. /// Crea la expedición completa (encabezado + detalles) y la deja emitida con numeración de serie.
/// </summary> /// </summary>
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId); Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
Task<ExpeditionDto?> GetDtoByIdAsync(int id); Task<ExpeditionDto?> GetDtoByIdAsync(int id);
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize); Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
/// <summary>
/// Pasa la expedición a En tránsito y crea las reservas de stock asociadas.
/// La operación es transaccional y falla completa si detecta inconsistencias.
/// </summary>
Task MarkInTransitAsync(int expeditionId);
} }
} }

View File

@ -24,5 +24,9 @@ namespace Models.Interfaces
int? locationId, int? locationId,
int page, int page,
int take); int take);
// Obtener disponibilidad por IDs de StockItem
Task<List<StockItemAvailabilityDto>> GetAvailabilityByStockItemIdsAsync(
IEnumerable<int> stockItemIds);
} }
} }

View File

@ -0,0 +1,28 @@
using Domain.Dtos.Sales;
using Domain.Entities;
using Domain.Generics;
namespace Models.Interfaces
{
public interface IPhSDeliveryNoteRepository
{
Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(
int? customerId,
string? customerText,
string? deliveryNoteNumber,
int? quoteId,
string? quoteNumber,
DateTime? issueDateFrom,
DateTime? issueDateTo,
string? status,
int page = 1,
int pageSize = 50);
Task<DeliveryNoteDto?> GetDtoByIdAsync(int id);
Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber);
Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId);
Task<bool> ExistsByDeliveryNoteNumberAsync(string deliveryNoteNumber);
Task<EDeliveryNote> CreateAsync(EDeliveryNote entity);
}
}

View File

@ -0,0 +1,11 @@
using Domain.Dtos.Sales;
using Domain.Entities;
namespace Models.Interfaces
{
public interface IPhSSalesDocumentRepository
{
Task<ESalesDocument> CreateAsync(ESalesDocument entity);
Task<SalesDocumentDto?> GetDtoByIdAsync(int id);
}
}

View File

@ -20,6 +20,11 @@ public partial class PhLsmExpeditionDetail
/// </summary> /// </summary>
public int ProductId { get; set; } public int ProductId { get; set; }
/// <summary>
/// Referencia a StockItem (PhLSM_StockItem)
/// </summary>
public int StockitemId { get; set; }
/// <summary> /// <summary>
/// Cantidad solicitada del producto /// Cantidad solicitada del producto
/// </summary> /// </summary>
@ -68,4 +73,6 @@ public partial class PhLsmExpeditionDetail
public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!; public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!;
public virtual PhLsmProduct Product { get; set; } = null!; public virtual PhLsmProduct Product { get; set; } = null!;
public virtual PhLsmStockItem Stockitem { get; set; } = null!;
} }

View File

@ -67,5 +67,9 @@ public partial class PhLsmStockItem
public virtual PhLsmStockLocation Location { get; set; } = null!; public virtual PhLsmStockLocation Location { get; set; } = null!;
public virtual ICollection<PhLsmExpeditionDetail> PhLsmExpeditionDetails { get; set; } = new List<PhLsmExpeditionDetail>();
public virtual ICollection<PhLsmStockReservation> PhLsmStockReservations { get; set; } = new List<PhLsmStockReservation>();
public virtual PhLsmProduct Product { get; set; } = null!; public virtual PhLsmProduct Product { get; set; } = null!;
} }

View File

@ -1,4 +1,7 @@
namespace Models.Models; using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary> /// <summary>
/// Reservas de stock por origen genérico (source_type/source_id). Cada fila bloquea cantidad sobre un StockItem. No duplica lote/serie/vencimiento; se resuelve por JOIN a PhLSM_StockItem. /// Reservas de stock por origen genérico (source_type/source_id). Cada fila bloquea cantidad sobre un StockItem. No duplica lote/serie/vencimiento; se resuelve por JOIN a PhLSM_StockItem.

View File

@ -29,7 +29,13 @@ public partial class PhSCustomer
public virtual ICollection<PhSCustomerDocument> PhSCustomerDocuments { get; set; } = new List<PhSCustomerDocument>(); public virtual ICollection<PhSCustomerDocument> PhSCustomerDocuments { get; set; } = new List<PhSCustomerDocument>();
public virtual ICollection<PhSDeliveryNote> PhSDeliveryNotes { get; set; } = new List<PhSDeliveryNote>();
public virtual ICollection<PhSPatient> PhSPatients { get; set; } = new List<PhSPatient>(); public virtual ICollection<PhSPatient> PhSPatients { get; set; } = new List<PhSPatient>();
public virtual ICollection<PhSSalesDocument> PhSSalesDocumentBillToCustomers { get; set; } = new List<PhSSalesDocument>();
public virtual ICollection<PhSSalesDocument> PhSSalesDocumentCustomers { get; set; } = new List<PhSSalesDocument>();
public virtual PhOhTaxCondition? TaxCondition { get; set; } public virtual PhOhTaxCondition? TaxCondition { get; set; }
} }

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
public partial class PhSDeliveryNote
{
public int Id { get; set; }
public string Deliverynotenumber { get; set; } = null!;
public int? QuoteId { get; set; }
public int? SalesinvoiceId { get; set; }
public DateTime Issuedate { get; set; }
public int CustomerId { get; set; }
public string Status { get; set; } = null!;
public string? Observations { get; set; }
public string? ExtrainfoJson { get; set; }
public int Printcount { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual PhSCustomer Customer { get; set; } = null!;
public virtual ICollection<PhSDeliveryNoteDetail> PhSDeliveryNoteDetails { get; set; } = new List<PhSDeliveryNoteDetail>();
public virtual PhSQuoteHeader? Quote { get; set; }
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
public partial class PhSDeliveryNoteDetail
{
public int Id { get; set; }
public int DeliverynoteId { get; set; }
public int LineNumber { get; set; }
public byte OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public string? Description { get; set; }
public decimal Quantity { get; set; }
public string Notes { get; set; } = null!;
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual PhSDeliveryNote Deliverynote { get; set; } = null!;
public virtual PhSQuoteDetail? QuoteDetail { get; set; }
}

View File

@ -44,4 +44,6 @@ public partial class PhSFormSeries
public virtual PhSFormType Formtype { get; set; } = null!; public virtual PhSFormType Formtype { get; set; } = null!;
public virtual PhSFormSeriesNextNumber? PhSFormSeriesNextNumber { get; set; } public virtual PhSFormSeriesNextNumber? PhSFormSeriesNextNumber { get; set; }
public virtual ICollection<PhSSalesDocument> PhSSalesDocuments { get; set; } = new List<PhSSalesDocument>();
} }

View File

@ -28,4 +28,6 @@ public partial class PhSProduct
public virtual PhSProductCategory? Category { get; set; } public virtual PhSProductCategory? Category { get; set; }
public virtual ICollection<PhSQuoteDetail> PhSQuoteDetails { get; set; } = new List<PhSQuoteDetail>(); public virtual ICollection<PhSQuoteDetail> PhSQuoteDetails { get; set; } = new List<PhSQuoteDetail>();
public virtual ICollection<PhSSalesDocumentDetail> PhSSalesDocumentDetails { get; set; } = new List<PhSSalesDocumentDetail>();
} }

View File

@ -68,6 +68,12 @@ public partial class PhSQuoteDetail
/// </summary> /// </summary>
public DateTime? Modifiedat { get; set; } public DateTime? Modifiedat { get; set; }
public virtual ICollection<PhSDeliveryNoteDetail> PhSDeliveryNoteDetails { get; set; } = new List<PhSDeliveryNoteDetail>();
public virtual ICollection<PhSSalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<PhSSalesDocumentCoverage>();
public virtual ICollection<PhSSalesDocumentDetail> PhSSalesDocumentDetails { get; set; } = new List<PhSSalesDocumentDetail>();
public virtual PhSProduct Product { get; set; } = null!; public virtual PhSProduct Product { get; set; } = null!;
public virtual PhSQuoteHeader Quoteheader { get; set; } = null!; public virtual PhSQuoteHeader Quoteheader { get; set; } = null!;

View File

@ -130,6 +130,8 @@ public partial class PhSQuoteHeader
public virtual PhSPaymentTerm? Paymentterm { get; set; } public virtual PhSPaymentTerm? Paymentterm { get; set; }
public virtual ICollection<PhSDeliveryNote> PhSDeliveryNotes { get; set; } = new List<PhSDeliveryNote>();
public virtual ICollection<PhSQuoteAdjustment> PhSQuoteAdjustments { get; set; } = new List<PhSQuoteAdjustment>(); public virtual ICollection<PhSQuoteAdjustment> PhSQuoteAdjustments { get; set; } = new List<PhSQuoteAdjustment>();
public virtual ICollection<PhSQuoteDetail> PhSQuoteDetails { get; set; } = new List<PhSQuoteDetail>(); public virtual ICollection<PhSQuoteDetail> PhSQuoteDetails { get; set; } = new List<PhSQuoteDetail>();
@ -137,4 +139,8 @@ public partial class PhSQuoteHeader
public virtual ICollection<PhSQuoteRole> PhSQuoteRoles { get; set; } = new List<PhSQuoteRole>(); public virtual ICollection<PhSQuoteRole> PhSQuoteRoles { get; set; } = new List<PhSQuoteRole>();
public virtual ICollection<PhSQuoteTaxis> PhSQuoteTaxes { get; set; } = new List<PhSQuoteTaxis>(); public virtual ICollection<PhSQuoteTaxis> PhSQuoteTaxes { get; set; } = new List<PhSQuoteTaxis>();
public virtual ICollection<PhSSalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<PhSSalesDocumentCoverage>();
public virtual ICollection<PhSSalesDocument> PhSSalesDocuments { get; set; } = new List<PhSSalesDocument>();
} }

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Documentos comerciales internos de venta: facturas, notas de debito, notas de credito, FCE, NDE y NCE. Mantiene la emision interna separada de la autorizacion fiscal ARCA.
/// </summary>
public partial class PhSSalesDocument
{
/// <summary>
/// Identificador interno del documento comercial.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Talonario/serie interna existente en PhronCare. Reutiliza PhS_FormSeries para numeracion interna.
/// </summary>
public int? FormseriesId { get; set; }
/// <summary>
/// Numero secuencial interno asignado al emitir internamente el documento. No corresponde al numero fiscal ARCA.
/// </summary>
public int? InternalSequenceNumber { get; set; }
/// <summary>
/// Numero visible interno del documento, formado desde la serie/talonario interno. Puede diferir del numero fiscal.
/// </summary>
public string? InternalDocumentNumber { get; set; }
/// <summary>
/// Tipo comercial interno del documento. Ejemplos: Invoice, DebitNote, CreditNote, CreditInvoice, CreditDebitNote, CreditCreditNote.
/// </summary>
public int DocumentType { get; set; }
/// <summary>
/// Tipo de comprobante fiscal AFIP/ARCA previsto para autorizacion futura. Ejemplos: 1, 6, 11, 201, 202, 203.
/// </summary>
public int? FiscalVoucherType { get; set; }
/// <summary>
/// Letra fiscal prevista del comprobante: A, B, C u otras segun configuracion fiscal.
/// </summary>
public string? FiscalVoucherLetter { get; set; }
/// <summary>
/// Estado comercial interno. Ejemplos: Draft, Validated, Issued, Cancelled. Independiente del estado fiscal.
/// </summary>
public int Status { get; set; }
/// <summary>
/// Presupuesto origen opcional. Puede ser NULL para ventas manuales o de escritorio.
/// </summary>
public int? QuoteId { get; set; }
/// <summary>
/// Cliente origen de la operacion comercial.
/// </summary>
public int CustomerId { get; set; }
/// <summary>
/// Cliente al que se factura realmente. Permite escenarios obra social / particular u otros terceros pagadores.
/// </summary>
public int BillToCustomerId { get; set; }
/// <summary>
/// Fecha de emision interna del documento comercial.
/// </summary>
public DateTime? IssueDate { get; set; }
/// <summary>
/// Moneda del documento comercial.
/// </summary>
public string Currency { get; set; } = null!;
/// <summary>
/// Cotizacion utilizada para la moneda del documento.
/// </summary>
public decimal ExchangeRate { get; set; }
/// <summary>
/// Importe neto total del documento.
/// </summary>
public decimal NetAmount { get; set; }
/// <summary>
/// Importe total de impuestos del documento.
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// Importe total del documento.
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// Tipo de documento interno asociado opcional, por ejemplo remito, orden de compra o autorizacion. No representa CbtesAsoc fiscal.
/// </summary>
public string? AssociatedDocumentType { get; set; }
/// <summary>
/// Numero del documento interno asociado opcional.
/// </summary>
public string? AssociatedDocumentNumber { get; set; }
/// <summary>
/// Fecha del documento interno asociado opcional.
/// </summary>
public DateTime? AssociatedDocumentDate { get; set; }
/// <summary>
/// Observaciones comerciales del documento.
/// </summary>
public string? Observations { get; set; }
/// <summary>
/// Snapshot JSON con informacion extra contextual del documento.
/// </summary>
public string? ExtraInfoJson { get; set; }
/// <summary>
/// Fecha inicial del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.
/// </summary>
public DateTime? PeriodFrom { get; set; }
/// <summary>
/// Fecha final del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.
/// </summary>
public DateTime? PeriodTo { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual PhSCustomer BillToCustomer { get; set; } = null!;
public virtual PhSCustomer Customer { get; set; } = null!;
public virtual PhSFormSeries? Formseries { get; set; }
public virtual ICollection<PhSSalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<PhSSalesDocumentCoverage>();
public virtual ICollection<PhSSalesDocumentDetail> PhSSalesDocumentDetails { get; set; } = new List<PhSSalesDocumentDetail>();
public virtual PhSSalesFiscalDocument? PhSSalesFiscalDocument { get; set; }
public virtual ICollection<PhSSalesFiscalDocumentAssociation> PhSSalesFiscalDocumentAssociations { get; set; } = new List<PhSSalesFiscalDocumentAssociation>();
public virtual PhSQuoteHeader? Quote { get; set; }
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Presupuestos/casos cubiertos por un documento de venta. Es la fuente real para determinar si un presupuesto queda pendiente de facturacion o ya fue cubierto, incluyendo facturacion 1 a 1 y capita.
/// </summary>
public partial class PhSSalesDocumentCoverage
{
/// <summary>
/// Identificador interno de la cobertura.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Documento de venta que cubre el presupuesto/caso.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Detalle del documento de venta asociado a esta cobertura, cuando aplique. En capita puede apuntar a la linea agregada mensual.
/// </summary>
public int? SalesdocumentdetailId { get; set; }
/// <summary>
/// Presupuesto/caso cubierto por el documento de venta. Se usa tanto para facturacion directa como para capita.
/// </summary>
public int QuoteId { get; set; }
/// <summary>
/// Detalle de presupuesto cubierto, cuando se requiera trazabilidad granular por item.
/// </summary>
public int? QuoteDetailId { get; set; }
/// <summary>
/// Tipo de cobertura. Valores esperados en Domain: Direct, Capita, Adjustment.
/// </summary>
public int CoverageType { get; set; }
/// <summary>
/// Porcentaje del presupuesto/caso cubierto por el documento. Permite 100% en facturacion directa o particiones 60/40.
/// </summary>
public decimal? CoveragePercentage { get; set; }
/// <summary>
/// Importe de referencia cubierto por el documento, cuando aplique.
/// </summary>
public decimal? CoverageAmount { get; set; }
/// <summary>
/// Fecha inicial del periodo de cobertura.
/// </summary>
public DateTime? PeriodFrom { get; set; }
/// <summary>
/// Fecha final del periodo de cobertura.
/// </summary>
public DateTime? PeriodTo { get; set; }
/// <summary>
/// Notas internas de cobertura.
/// </summary>
public string? Notes { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual PhSQuoteHeader Quote { get; set; } = null!;
public virtual PhSQuoteDetail? QuoteDetail { get; set; }
public virtual PhSSalesDocument Salesdocument { get; set; } = null!;
public virtual PhSSalesDocumentDetail? Salesdocumentdetail { get; set; }
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Detalles valorizados de documentos comerciales de venta.
/// </summary>
public partial class PhSSalesDocumentDetail
{
public int Id { get; set; }
/// <summary>
/// Documento comercial al que pertenece el detalle.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Numero de linea dentro del documento.
/// </summary>
public int LineNumber { get; set; }
/// <summary>
/// Origen logico del item: Manual, QuoteDetail, Adjustment u otro valor definido por Domain/Core.
/// </summary>
public string OriginType { get; set; } = null!;
/// <summary>
/// Identificador generico del origen cuando aplique.
/// </summary>
public int? OriginId { get; set; }
/// <summary>
/// Detalle del presupuesto aprobado que origina la linea, cuando exista. Puede ser NULL en ventas manuales.
/// </summary>
public int? QuoteDetailId { get; set; }
/// <summary>
/// Producto asociado a la linea, si aplica.
/// </summary>
public int? ProductId { get; set; }
/// <summary>
/// Descripcion visible de la linea facturada.
/// </summary>
public string Description { get; set; } = null!;
/// <summary>
/// Cantidad facturada.
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Precio unitario autorizado o de referencia proveniente del origen comercial.
/// </summary>
public decimal? AuthorizedUnitPrice { get; set; }
/// <summary>
/// Importe autorizado o de referencia proveniente del origen comercial.
/// </summary>
public decimal? AuthorizedAmount { get; set; }
/// <summary>
/// Porcentaje facturado sobre el origen. Permite facturacion parcial obra social / particular.
/// </summary>
public decimal? BilledPercentage { get; set; }
/// <summary>
/// Precio unitario efectivo de la linea del documento.
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// Importe neto de la linea.
/// </summary>
public decimal NetAmount { get; set; }
/// <summary>
/// Importe de impuestos de la linea.
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// Importe total de la linea.
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// Snapshot JSON del origen de la linea para trazabilidad historica.
/// </summary>
public string? OriginSnapshotJson { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<PhSSalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; } = new List<PhSSalesDocumentCoverage>();
public virtual PhSProduct? Product { get; set; }
public virtual PhSQuoteDetail? QuoteDetail { get; set; }
public virtual PhSSalesDocument Salesdocument { get; set; } = null!;
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Documento fiscal asociado a un documento comercial de venta. Guarda estado, CAE, numeracion fiscal, request/response e idempotencia ARCA.
/// </summary>
public partial class PhSSalesFiscalDocument
{
public int Id { get; set; }
/// <summary>
/// Documento comercial interno vinculado al documento fiscal.
/// </summary>
public int SalesdocumentId { get; set; }
/// <summary>
/// Estado fiscal independiente del estado comercial. Ejemplos: None, Pending, Authorized, Rejected, Error, PendingReconciliation.
/// </summary>
public int FiscalStatus { get; set; }
/// <summary>
/// Ambiente fiscal usado para autorizacion: homologacion, produccion u otro valor definido por configuracion.
/// </summary>
public string Environment { get; set; } = null!;
/// <summary>
/// Punto de venta fiscal ARCA/AFIP.
/// </summary>
public short? PointOfSale { get; set; }
/// <summary>
/// Tipo de comprobante fiscal ARCA/AFIP utilizado en FECAESolicitar.
/// </summary>
public int? VoucherType { get; set; }
/// <summary>
/// Letra fiscal del comprobante autorizado o a autorizar.
/// </summary>
public string? VoucherLetter { get; set; }
/// <summary>
/// Numero fiscal del comprobante asignado para ARCA. Se mantiene separado del numero interno.
/// </summary>
public int? VoucherNumber { get; set; }
/// <summary>
/// Codigo de autorizacion electronico obtenido desde ARCA/AFIP.
/// </summary>
public string? Cae { get; set; }
/// <summary>
/// Fecha de vencimiento del CAE.
/// </summary>
public DateTime? CaeExpirationDate { get; set; }
/// <summary>
/// Huella de idempotencia fiscal para evitar duplicacion de solicitudes ante ARCA.
/// </summary>
public string? RequestFingerprint { get; set; }
/// <summary>
/// Indica si el resultado fiscal es final y no debe volver a mutar salvo procesos controlados de auditoria.
/// </summary>
public bool IsFinal { get; set; }
/// <summary>
/// Payload JSON enviado a ARCA/AFIP.
/// </summary>
public string? ArcaRequestPayloadJson { get; set; }
/// <summary>
/// Payload JSON recibido desde ARCA/AFIP.
/// </summary>
public string? ArcaResponsePayloadJson { get; set; }
/// <summary>
/// Errores devueltos por ARCA/AFIP serializados como JSON.
/// </summary>
public string? ErrorsJson { get; set; }
/// <summary>
/// Eventos devueltos por ARCA/AFIP serializados como JSON.
/// </summary>
public string? EventsJson { get; set; }
/// <summary>
/// Observaciones devueltas por ARCA/AFIP serializadas como JSON.
/// </summary>
public string? ObservationsJson { get; set; }
/// <summary>
/// Fecha/hora UTC del intento de autorizacion fiscal.
/// </summary>
public DateTime? AttemptedAtUtc { get; set; }
/// <summary>
/// Fecha/hora UTC de finalizacion del flujo fiscal.
/// </summary>
public DateTime? CompletedAtUtc { get; set; }
/// <summary>
/// Indica que el documento fiscal fue resuelto mediante reconciliacion posterior a timeout o resultado ambiguo.
/// </summary>
public bool ReconciledAfterTimeout { get; set; }
public DateTime Createdat { get; set; }
public DateTime? Modifiedat { get; set; }
public virtual ICollection<PhSSalesFiscalDocumentAssociation> PhSSalesFiscalDocumentAssociations { get; set; } = new List<PhSSalesFiscalDocumentAssociation>();
public virtual PhSSalesDocument Salesdocument { get; set; } = null!;
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Comprobantes fiscales asociados enviados como CbtesAsoc. Aplica a notas de credito, notas de debito, NCE/NDE y casos FCE donde corresponda.
/// </summary>
public partial class PhSSalesFiscalDocumentAssociation
{
public int Id { get; set; }
/// <summary>
/// Documento fiscal que contiene esta asociacion.
/// </summary>
public int SalesfiscaldocumentId { get; set; }
/// <summary>
/// Documento comercial interno asociado, si existe dentro de PhronCare.
/// </summary>
public int? AssociatedSalesdocumentId { get; set; }
/// <summary>
/// Tipo fiscal ARCA/AFIP del comprobante asociado.
/// </summary>
public int AssociatedVoucherType { get; set; }
/// <summary>
/// Punto de venta fiscal del comprobante asociado.
/// </summary>
public short AssociatedPointOfSale { get; set; }
/// <summary>
/// Numero fiscal del comprobante asociado.
/// </summary>
public int AssociatedVoucherNumber { get; set; }
/// <summary>
/// CUIT emisor del comprobante asociado, cuando sea requerido por ARCA.
/// </summary>
public string? AssociatedVoucherCuit { get; set; }
/// <summary>
/// Fecha del comprobante fiscal asociado.
/// </summary>
public DateTime? AssociatedVoucherDate { get; set; }
public DateTime Createdat { get; set; }
public virtual PhSSalesDocument? AssociatedSalesdocument { get; set; }
public virtual PhSSalesFiscalDocument Salesfiscaldocument { get; set; } = null!;
}

View File

@ -6,10 +6,6 @@ namespace Models.Models;
public partial class PhronCareOperationsHubContext : DbContext public partial class PhronCareOperationsHubContext : DbContext
{ {
public PhronCareOperationsHubContext()
{
}
public PhronCareOperationsHubContext(DbContextOptions<PhronCareOperationsHubContext> options) public PhronCareOperationsHubContext(DbContextOptions<PhronCareOperationsHubContext> options)
: base(options) : base(options)
{ {
@ -35,6 +31,8 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhLsmStockOut> PhLsmStockOuts { get; set; } public virtual DbSet<PhLsmStockOut> PhLsmStockOuts { get; set; }
public virtual DbSet<PhLsmStockReservation> PhLsmStockReservations { get; set; }
public virtual DbSet<PhLsmUnitOfMeasure> PhLsmUnitOfMeasures { get; set; } public virtual DbSet<PhLsmUnitOfMeasure> PhLsmUnitOfMeasures { get; set; }
public virtual DbSet<PhOhArcadocumentType> PhOhArcadocumentTypes { get; set; } public virtual DbSet<PhOhArcadocumentType> PhOhArcadocumentTypes { get; set; }
@ -59,6 +57,10 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSCustomerDocument> PhSCustomerDocuments { get; set; } public virtual DbSet<PhSCustomerDocument> PhSCustomerDocuments { get; set; }
public virtual DbSet<PhSDeliveryNote> PhSDeliveryNotes { get; set; }
public virtual DbSet<PhSDeliveryNoteDetail> PhSDeliveryNoteDetails { get; set; }
public virtual DbSet<PhSDocumentType> PhSDocumentTypes { get; set; } public virtual DbSet<PhSDocumentType> PhSDocumentTypes { get; set; }
public virtual DbSet<PhSFormSeries> PhSFormSeries { get; set; } public virtual DbSet<PhSFormSeries> PhSFormSeries { get; set; }
@ -95,16 +97,15 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; } public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) public virtual DbSet<PhSSalesDocument> PhSSalesDocuments { get; set; }
#region VERSION DOCKER
{ public virtual DbSet<PhSSalesDocumentCoverage> PhSSalesDocumentCoverages { get; set; }
if (!optionsBuilder.IsConfigured)
{ public virtual DbSet<PhSSalesDocumentDetail> PhSSalesDocumentDetails { get; set; }
// Dejarlo vacío para usar la configuración externa desde Program.cs o Startup.cs
} public virtual DbSet<PhSSalesFiscalDocument> PhSSalesFiscalDocuments { get; set; }
}
#endregion public virtual DbSet<PhSSalesFiscalDocumentAssociation> PhSSalesFiscalDocumentAssociations { get; set; }
//=> optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True");
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -116,6 +117,14 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.ToTable("PhLSM_ExpeditionDetails"); entity.ToTable("PhLSM_ExpeditionDetails");
entity.HasIndex(e => e.ExpeditionId, "IX_PhLSM_ExpeditionDetails_Expedition");
entity.HasIndex(e => new { e.ExpeditionId, e.StockitemId }, "IX_PhLSM_ExpeditionDetails_Expedition_StockItem");
entity.HasIndex(e => e.ProductId, "IX_PhLSM_ExpeditionDetails_Product");
entity.HasIndex(e => e.StockitemId, "IX_PhLSM_ExpeditionDetails_StockItem");
entity.Property(e => e.Id) entity.Property(e => e.Id)
.HasComment("Identificador interno del ítem de expedición") .HasComment("Identificador interno del ítem de expedición")
.HasColumnName("id"); .HasColumnName("id");
@ -162,6 +171,9 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasMaxLength(100) .HasMaxLength(100)
.HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.") .HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.")
.HasColumnName("serial"); .HasColumnName("serial");
entity.Property(e => e.StockitemId)
.HasComment("Referencia a StockItem (PhLSM_StockItem)")
.HasColumnName("stockitem_id");
entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails) entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails)
.HasForeignKey(d => d.ExpeditionId) .HasForeignKey(d => d.ExpeditionId)
@ -172,6 +184,11 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasForeignKey(d => d.ProductId) .HasForeignKey(d => d.ProductId)
.OnDelete(DeleteBehavior.ClientSetNull) .OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_Product"); .HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_Product");
entity.HasOne(d => d.Stockitem).WithMany(p => p.PhLsmExpeditionDetails)
.HasForeignKey(d => d.StockitemId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_StockItem");
}); });
modelBuilder.Entity<PhLsmExpeditionHeader>(entity => modelBuilder.Entity<PhLsmExpeditionHeader>(entity =>
@ -180,6 +197,8 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.ToTable("PhLSM_ExpeditionHeaders"); entity.ToTable("PhLSM_ExpeditionHeaders");
entity.HasIndex(e => e.Expeditionnumber, "UX_PhLSM_ExpeditionHeaders_Number").IsUnique();
entity.Property(e => e.Id) entity.Property(e => e.Id)
.HasComment("Identificador interno de la expedición") .HasComment("Identificador interno de la expedición")
.HasColumnName("id"); .HasColumnName("id");
@ -641,6 +660,64 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasConstraintName("FK_PhLSM_StockOut_PhLSM_Product"); .HasConstraintName("FK_PhLSM_StockOut_PhLSM_Product");
}); });
modelBuilder.Entity<PhLsmStockReservation>(entity =>
{
entity.ToTable("PhLSM_StockReservation", tb => tb.HasComment("Reservas de stock por origen genérico (source_type/source_id). Cada fila bloquea cantidad sobre un StockItem. No duplica lote/serie/vencimiento; se resuelve por JOIN a PhLSM_StockItem."));
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.Status }, "IX_PhLSM_StockReservation_Source_Status");
entity.HasIndex(e => e.StockitemId, "IX_PhLSM_StockReservation_StockItem_Reserved").HasFilter("([status]=(1))");
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.StockitemId }, "UX_PhLSM_StockReservation_Source_StockItem_Consumed")
.IsUnique()
.HasFilter("([status]=(3))");
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.StockitemId }, "UX_PhLSM_StockReservation_Source_StockItem_Reserved")
.IsUnique()
.HasFilter("([status]=(1))");
entity.Property(e => e.Id)
.HasComment("Identificador autoincremental de la reserva.")
.HasColumnName("id");
entity.Property(e => e.Createdat)
.HasPrecision(0)
.HasDefaultValueSql("(sysutcdatetime())")
.HasComment("Fecha/hora de creación (UTC).")
.HasColumnName("createdat");
entity.Property(e => e.Modifiedat)
.HasPrecision(0)
.HasComment("Última modificación (UTC). Puede ser NULL si nunca se actualizó.")
.HasColumnName("modifiedat");
entity.Property(e => e.ReservedQuantity)
.HasComment("Cantidad reservada (bloqueada). No disponible mientras status=1 (Reserved).")
.HasColumnType("decimal(18, 2)")
.HasColumnName("reserved_quantity");
entity.Property(e => e.Rowversion)
.IsRowVersion()
.IsConcurrencyToken()
.HasComment("Token de concurrencia optimista (ROWVERSION) para actualizaciones seguras.")
.HasColumnName("rowversion");
entity.Property(e => e.SourceId)
.HasComment("Identificador del origen. Ej.: expedition_id cuando source_type=1.")
.HasColumnName("source_id");
entity.Property(e => e.SourceType)
.HasDefaultValue((byte)1)
.HasComment("Tipo de origen de la reserva. 1=Expedition (extensible a futuros orígenes).")
.HasColumnName("source_type");
entity.Property(e => e.Status)
.HasDefaultValue(1)
.HasComment("Estado de la reserva: 1=Reserved, 2=Released, 3=Consumed.")
.HasColumnName("status");
entity.Property(e => e.StockitemId)
.HasComment("Referencia al StockItem exacto bloqueado (FK a PhLSM_StockItem). Define producto/ubicación/trazabilidad por JOIN.")
.HasColumnName("stockitem_id");
entity.HasOne(d => d.Stockitem).WithMany(p => p.PhLsmStockReservations)
.HasForeignKey(d => d.StockitemId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_StockReservation_StockItem");
});
modelBuilder.Entity<PhLsmUnitOfMeasure>(entity => modelBuilder.Entity<PhLsmUnitOfMeasure>(entity =>
{ {
entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6"); entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6");
@ -970,6 +1047,96 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasConstraintName("FK_PhS_CustomerDocuments_PhS_DocumentTypes"); .HasConstraintName("FK_PhS_CustomerDocuments_PhS_DocumentTypes");
}); });
modelBuilder.Entity<PhSDeliveryNote>(entity =>
{
entity.ToTable("PhS_DeliveryNotes");
entity.HasIndex(e => e.CustomerId, "IX_PhS_DeliveryNotes_customer_id");
entity.HasIndex(e => e.Issuedate, "IX_PhS_DeliveryNotes_issuedate");
entity.HasIndex(e => e.QuoteId, "IX_PhS_DeliveryNotes_quote_id");
entity.HasIndex(e => e.Status, "IX_PhS_DeliveryNotes_status");
entity.HasIndex(e => e.Deliverynotenumber, "UQ_PhS_DeliveryNotes_deliverynotenumber").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.CustomerId).HasColumnName("customer_id");
entity.Property(e => e.Deliverynotenumber)
.HasMaxLength(20)
.IsUnicode(false)
.HasColumnName("deliverynotenumber");
entity.Property(e => e.ExtrainfoJson).HasColumnName("extrainfo_json");
entity.Property(e => e.Issuedate)
.HasColumnType("datetime")
.HasColumnName("issuedate");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.Observations)
.HasMaxLength(1000)
.HasColumnName("observations");
entity.Property(e => e.Printcount).HasColumnName("printcount");
entity.Property(e => e.QuoteId).HasColumnName("quote_id");
entity.Property(e => e.SalesinvoiceId).HasColumnName("salesinvoice_id");
entity.Property(e => e.Status)
.HasMaxLength(20)
.IsUnicode(false)
.HasColumnName("status");
entity.HasOne(d => d.Customer).WithMany(p => p.PhSDeliveryNotes)
.HasForeignKey(d => d.CustomerId)
.OnDelete(DeleteBehavior.ClientSetNull);
entity.HasOne(d => d.Quote).WithMany(p => p.PhSDeliveryNotes).HasForeignKey(d => d.QuoteId);
});
modelBuilder.Entity<PhSDeliveryNoteDetail>(entity =>
{
entity.ToTable("PhS_DeliveryNoteDetails");
entity.HasIndex(e => e.DeliverynoteId, "IX_PhS_DeliveryNoteDetails_deliverynote_id");
entity.HasIndex(e => new { e.DeliverynoteId, e.LineNumber }, "IX_PhS_DeliveryNoteDetails_deliverynote_id_line_number");
entity.HasIndex(e => new { e.OriginType, e.OriginId }, "IX_PhS_DeliveryNoteDetails_origin_type_origin_id");
entity.HasIndex(e => e.QuoteDetailId, "IX_PhS_DeliveryNoteDetails_quote_detail_id");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.DeliverynoteId).HasColumnName("deliverynote_id");
entity.Property(e => e.Description).HasColumnName("description");
entity.Property(e => e.LineNumber).HasColumnName("line_number");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.Notes)
.HasMaxLength(1000)
.HasDefaultValue("")
.HasColumnName("notes");
entity.Property(e => e.OriginId).HasColumnName("origin_id");
entity.Property(e => e.OriginType).HasColumnName("origin_type");
entity.Property(e => e.Quantity)
.HasColumnType("decimal(18, 2)")
.HasColumnName("quantity");
entity.Property(e => e.QuoteDetailId).HasColumnName("quote_detail_id");
entity.HasOne(d => d.Deliverynote).WithMany(p => p.PhSDeliveryNoteDetails)
.HasForeignKey(d => d.DeliverynoteId)
.OnDelete(DeleteBehavior.ClientSetNull);
entity.HasOne(d => d.QuoteDetail).WithMany(p => p.PhSDeliveryNoteDetails).HasForeignKey(d => d.QuoteDetailId);
});
modelBuilder.Entity<PhSDocumentType>(entity => modelBuilder.Entity<PhSDocumentType>(entity =>
{ {
entity.ToTable("PhS_DocumentTypes"); entity.ToTable("PhS_DocumentTypes");
@ -1705,6 +1872,458 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasConstraintName("FK_PhS_QuoteTaxes_QuoteHeaders"); .HasConstraintName("FK_PhS_QuoteTaxes_QuoteHeaders");
}); });
modelBuilder.Entity<PhSSalesDocument>(entity =>
{
entity.ToTable("PhS_SalesDocuments", tb => tb.HasComment("Documentos comerciales internos de venta: facturas, notas de debito, notas de credito, FCE, NDE y NCE. Mantiene la emision interna separada de la autorizacion fiscal ARCA."));
entity.HasIndex(e => e.BillToCustomerId, "IX_PhS_SalesDocuments_BillToCustomer");
entity.HasIndex(e => e.CustomerId, "IX_PhS_SalesDocuments_Customer");
entity.HasIndex(e => e.FormseriesId, "IX_PhS_SalesDocuments_FormSeries");
entity.HasIndex(e => e.IssueDate, "IX_PhS_SalesDocuments_IssueDate");
entity.HasIndex(e => new { e.PeriodFrom, e.PeriodTo }, "IX_PhS_SalesDocuments_Period");
entity.HasIndex(e => e.QuoteId, "IX_PhS_SalesDocuments_Quote");
entity.HasIndex(e => e.Status, "IX_PhS_SalesDocuments_Status");
entity.HasIndex(e => new { e.FormseriesId, e.InternalDocumentNumber }, "UX_PhS_SalesDocuments_InternalDocumentNumber")
.IsUnique()
.HasFilter("([formseries_id] IS NOT NULL AND [internal_document_number] IS NOT NULL)");
entity.HasIndex(e => new { e.FormseriesId, e.InternalSequenceNumber }, "UX_PhS_SalesDocuments_InternalNumber")
.IsUnique()
.HasFilter("([formseries_id] IS NOT NULL AND [internal_sequence_number] IS NOT NULL)");
entity.Property(e => e.Id)
.HasComment("Identificador interno del documento comercial.")
.HasColumnName("id");
entity.Property(e => e.AssociatedDocumentDate)
.HasComment("Fecha del documento interno asociado opcional.")
.HasColumnType("datetime")
.HasColumnName("associated_document_date");
entity.Property(e => e.AssociatedDocumentNumber)
.HasMaxLength(50)
.HasComment("Numero del documento interno asociado opcional.")
.HasColumnName("associated_document_number");
entity.Property(e => e.AssociatedDocumentType)
.HasMaxLength(50)
.HasComment("Tipo de documento interno asociado opcional, por ejemplo remito, orden de compra o autorizacion. No representa CbtesAsoc fiscal.")
.HasColumnName("associated_document_type");
entity.Property(e => e.BillToCustomerId)
.HasComment("Cliente al que se factura realmente. Permite escenarios obra social / particular u otros terceros pagadores.")
.HasColumnName("bill_to_customer_id");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.Currency)
.HasMaxLength(10)
.HasDefaultValue("ARS")
.HasComment("Moneda del documento comercial.")
.HasColumnName("currency");
entity.Property(e => e.CustomerId)
.HasComment("Cliente origen de la operacion comercial.")
.HasColumnName("customer_id");
entity.Property(e => e.DocumentType)
.HasComment("Tipo comercial interno del documento. Ejemplos: Invoice, DebitNote, CreditNote, CreditInvoice, CreditDebitNote, CreditCreditNote.")
.HasColumnName("document_type");
entity.Property(e => e.ExchangeRate)
.HasDefaultValue(1m)
.HasComment("Cotizacion utilizada para la moneda del documento.")
.HasColumnType("decimal(18, 6)")
.HasColumnName("exchange_rate");
entity.Property(e => e.ExtraInfoJson)
.HasComment("Snapshot JSON con informacion extra contextual del documento.")
.HasColumnName("extra_info_json");
entity.Property(e => e.FiscalVoucherLetter)
.HasMaxLength(5)
.HasComment("Letra fiscal prevista del comprobante: A, B, C u otras segun configuracion fiscal.")
.HasColumnName("fiscal_voucher_letter");
entity.Property(e => e.FiscalVoucherType)
.HasComment("Tipo de comprobante fiscal AFIP/ARCA previsto para autorizacion futura. Ejemplos: 1, 6, 11, 201, 202, 203.")
.HasColumnName("fiscal_voucher_type");
entity.Property(e => e.FormseriesId)
.HasComment("Talonario/serie interna existente en PhronCare. Reutiliza PhS_FormSeries para numeracion interna.")
.HasColumnName("formseries_id");
entity.Property(e => e.InternalDocumentNumber)
.HasMaxLength(50)
.HasComment("Numero visible interno del documento, formado desde la serie/talonario interno. Puede diferir del numero fiscal.")
.HasColumnName("internal_document_number");
entity.Property(e => e.InternalSequenceNumber)
.HasComment("Numero secuencial interno asignado al emitir internamente el documento. No corresponde al numero fiscal ARCA.")
.HasColumnName("internal_sequence_number");
entity.Property(e => e.IssueDate)
.HasComment("Fecha de emision interna del documento comercial.")
.HasColumnType("datetime")
.HasColumnName("issue_date");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.NetAmount)
.HasComment("Importe neto total del documento.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("net_amount");
entity.Property(e => e.Observations)
.HasComment("Observaciones comerciales del documento.")
.HasColumnName("observations");
entity.Property(e => e.PeriodFrom)
.HasComment("Fecha inicial del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.")
.HasColumnType("datetime")
.HasColumnName("period_from");
entity.Property(e => e.PeriodTo)
.HasComment("Fecha final del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.")
.HasColumnType("datetime")
.HasColumnName("period_to");
entity.Property(e => e.QuoteId)
.HasComment("Presupuesto origen opcional. Puede ser NULL para ventas manuales o de escritorio.")
.HasColumnName("quote_id");
entity.Property(e => e.Status)
.HasComment("Estado comercial interno. Ejemplos: Draft, Validated, Issued, Cancelled. Independiente del estado fiscal.")
.HasColumnName("status");
entity.Property(e => e.TaxAmount)
.HasComment("Importe total de impuestos del documento.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("tax_amount");
entity.Property(e => e.TotalAmount)
.HasComment("Importe total del documento.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("total_amount");
entity.HasOne(d => d.BillToCustomer).WithMany(p => p.PhSSalesDocumentBillToCustomers)
.HasForeignKey(d => d.BillToCustomerId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesDocuments_BillToCustomers");
entity.HasOne(d => d.Customer).WithMany(p => p.PhSSalesDocumentCustomers)
.HasForeignKey(d => d.CustomerId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesDocuments_Customers");
entity.HasOne(d => d.Formseries).WithMany(p => p.PhSSalesDocuments)
.HasForeignKey(d => d.FormseriesId)
.HasConstraintName("FK_PhS_SalesDocuments_FormSeries");
entity.HasOne(d => d.Quote).WithMany(p => p.PhSSalesDocuments)
.HasForeignKey(d => d.QuoteId)
.HasConstraintName("FK_PhS_SalesDocuments_QuoteHeaders");
});
modelBuilder.Entity<PhSSalesDocumentCoverage>(entity =>
{
entity.ToTable("PhS_SalesDocumentCoverage", tb => tb.HasComment("Presupuestos/casos cubiertos por un documento de venta. Es la fuente real para determinar si un presupuesto queda pendiente de facturacion o ya fue cubierto, incluyendo facturacion 1 a 1 y capita."));
entity.HasIndex(e => e.SalesdocumentId, "IX_PhS_SalesDocumentCoverage_Document");
entity.HasIndex(e => e.SalesdocumentdetailId, "IX_PhS_SalesDocumentCoverage_DocumentDetail");
entity.HasIndex(e => new { e.PeriodFrom, e.PeriodTo }, "IX_PhS_SalesDocumentCoverage_Period");
entity.HasIndex(e => e.QuoteId, "IX_PhS_SalesDocumentCoverage_Quote");
entity.HasIndex(e => e.QuoteDetailId, "IX_PhS_SalesDocumentCoverage_QuoteDetail");
entity.Property(e => e.Id)
.HasComment("Identificador interno de la cobertura.")
.HasColumnName("id");
entity.Property(e => e.CoverageAmount)
.HasComment("Importe de referencia cubierto por el documento, cuando aplique.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("coverage_amount");
entity.Property(e => e.CoveragePercentage)
.HasComment("Porcentaje del presupuesto/caso cubierto por el documento. Permite 100% en facturacion directa o particiones 60/40.")
.HasColumnType("decimal(9, 4)")
.HasColumnName("coverage_percentage");
entity.Property(e => e.CoverageType)
.HasDefaultValue(1)
.HasComment("Tipo de cobertura. Valores esperados en Domain: Direct, Capita, Adjustment.")
.HasColumnName("coverage_type");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.Notes)
.HasComment("Notas internas de cobertura.")
.HasColumnName("notes");
entity.Property(e => e.PeriodFrom)
.HasComment("Fecha inicial del periodo de cobertura.")
.HasColumnType("datetime")
.HasColumnName("period_from");
entity.Property(e => e.PeriodTo)
.HasComment("Fecha final del periodo de cobertura.")
.HasColumnType("datetime")
.HasColumnName("period_to");
entity.Property(e => e.QuoteDetailId)
.HasComment("Detalle de presupuesto cubierto, cuando se requiera trazabilidad granular por item.")
.HasColumnName("quote_detail_id");
entity.Property(e => e.QuoteId)
.HasComment("Presupuesto/caso cubierto por el documento de venta. Se usa tanto para facturacion directa como para capita.")
.HasColumnName("quote_id");
entity.Property(e => e.SalesdocumentId)
.HasComment("Documento de venta que cubre el presupuesto/caso.")
.HasColumnName("salesdocument_id");
entity.Property(e => e.SalesdocumentdetailId)
.HasComment("Detalle del documento de venta asociado a esta cobertura, cuando aplique. En capita puede apuntar a la linea agregada mensual.")
.HasColumnName("salesdocumentdetail_id");
entity.HasOne(d => d.QuoteDetail).WithMany(p => p.PhSSalesDocumentCoverages)
.HasForeignKey(d => d.QuoteDetailId)
.HasConstraintName("FK_PhS_SalesDocumentCoverage_QuoteDetails");
entity.HasOne(d => d.Quote).WithMany(p => p.PhSSalesDocumentCoverages)
.HasForeignKey(d => d.QuoteId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesDocumentCoverage_QuoteHeaders");
entity.HasOne(d => d.Salesdocument).WithMany(p => p.PhSSalesDocumentCoverages)
.HasForeignKey(d => d.SalesdocumentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesDocumentCoverage_SalesDocuments");
entity.HasOne(d => d.Salesdocumentdetail).WithMany(p => p.PhSSalesDocumentCoverages)
.HasForeignKey(d => d.SalesdocumentdetailId)
.HasConstraintName("FK_PhS_SalesDocumentCoverage_SalesDocumentDetails");
});
modelBuilder.Entity<PhSSalesDocumentDetail>(entity =>
{
entity.ToTable("PhS_SalesDocumentDetails", tb => tb.HasComment("Detalles valorizados de documentos comerciales de venta."));
entity.HasIndex(e => e.SalesdocumentId, "IX_PhS_SalesDocumentDetails_Document");
entity.HasIndex(e => e.QuoteDetailId, "IX_PhS_SalesDocumentDetails_QuoteDetail");
entity.HasIndex(e => new { e.SalesdocumentId, e.LineNumber }, "UX_PhS_SalesDocumentDetails_Line").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.AuthorizedAmount)
.HasComment("Importe autorizado o de referencia proveniente del origen comercial.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("authorized_amount");
entity.Property(e => e.AuthorizedUnitPrice)
.HasComment("Precio unitario autorizado o de referencia proveniente del origen comercial.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("authorized_unit_price");
entity.Property(e => e.BilledPercentage)
.HasComment("Porcentaje facturado sobre el origen. Permite facturacion parcial obra social / particular.")
.HasColumnType("decimal(9, 4)")
.HasColumnName("billed_percentage");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.Description)
.HasMaxLength(255)
.HasComment("Descripcion visible de la linea facturada.")
.HasColumnName("description");
entity.Property(e => e.LineNumber)
.HasComment("Numero de linea dentro del documento.")
.HasColumnName("line_number");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.NetAmount)
.HasComment("Importe neto de la linea.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("net_amount");
entity.Property(e => e.OriginId)
.HasComment("Identificador generico del origen cuando aplique.")
.HasColumnName("origin_id");
entity.Property(e => e.OriginSnapshotJson)
.HasComment("Snapshot JSON del origen de la linea para trazabilidad historica.")
.HasColumnName("origin_snapshot_json");
entity.Property(e => e.OriginType)
.HasMaxLength(50)
.HasComment("Origen logico del item: Manual, QuoteDetail, Adjustment u otro valor definido por Domain/Core.")
.HasColumnName("origin_type");
entity.Property(e => e.ProductId)
.HasComment("Producto asociado a la linea, si aplica.")
.HasColumnName("product_id");
entity.Property(e => e.Quantity)
.HasComment("Cantidad facturada.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("quantity");
entity.Property(e => e.QuoteDetailId)
.HasComment("Detalle del presupuesto aprobado que origina la linea, cuando exista. Puede ser NULL en ventas manuales.")
.HasColumnName("quote_detail_id");
entity.Property(e => e.SalesdocumentId)
.HasComment("Documento comercial al que pertenece el detalle.")
.HasColumnName("salesdocument_id");
entity.Property(e => e.TaxAmount)
.HasComment("Importe de impuestos de la linea.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("tax_amount");
entity.Property(e => e.TotalAmount)
.HasComment("Importe total de la linea.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("total_amount");
entity.Property(e => e.UnitPrice)
.HasComment("Precio unitario efectivo de la linea del documento.")
.HasColumnType("decimal(18, 2)")
.HasColumnName("unit_price");
entity.HasOne(d => d.Product).WithMany(p => p.PhSSalesDocumentDetails)
.HasForeignKey(d => d.ProductId)
.HasConstraintName("FK_PhS_SalesDocumentDetails_Products");
entity.HasOne(d => d.QuoteDetail).WithMany(p => p.PhSSalesDocumentDetails)
.HasForeignKey(d => d.QuoteDetailId)
.HasConstraintName("FK_PhS_SalesDocumentDetails_QuoteDetails");
entity.HasOne(d => d.Salesdocument).WithMany(p => p.PhSSalesDocumentDetails)
.HasForeignKey(d => d.SalesdocumentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesDocumentDetails_SalesDocuments");
});
modelBuilder.Entity<PhSSalesFiscalDocument>(entity =>
{
entity.ToTable("PhS_SalesFiscalDocuments", tb => tb.HasComment("Documento fiscal asociado a un documento comercial de venta. Guarda estado, CAE, numeracion fiscal, request/response e idempotencia ARCA."));
entity.HasIndex(e => e.SalesdocumentId, "IX_PhS_SalesFiscalDocuments_Document");
entity.HasIndex(e => e.SalesdocumentId, "UX_PhS_SalesFiscalDocuments_Document").IsUnique();
entity.HasIndex(e => new { e.Environment, e.PointOfSale, e.VoucherType, e.VoucherNumber }, "UX_PhS_SalesFiscalDocuments_FiscalNumber")
.IsUnique()
.HasFilter("([point_of_sale] IS NOT NULL AND [voucher_type] IS NOT NULL AND [voucher_number] IS NOT NULL)");
entity.HasIndex(e => new { e.Environment, e.PointOfSale, e.VoucherType, e.RequestFingerprint }, "UX_PhS_SalesFiscalDocuments_Idempotency")
.IsUnique()
.HasFilter("([point_of_sale] IS NOT NULL AND [voucher_type] IS NOT NULL AND [request_fingerprint] IS NOT NULL)");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.ArcaRequestPayloadJson)
.HasComment("Payload JSON enviado a ARCA/AFIP.")
.HasColumnName("arca_request_payload_json");
entity.Property(e => e.ArcaResponsePayloadJson)
.HasComment("Payload JSON recibido desde ARCA/AFIP.")
.HasColumnName("arca_response_payload_json");
entity.Property(e => e.AttemptedAtUtc)
.HasComment("Fecha/hora UTC del intento de autorizacion fiscal.")
.HasColumnType("datetime")
.HasColumnName("attempted_at_utc");
entity.Property(e => e.Cae)
.HasMaxLength(20)
.HasComment("Codigo de autorizacion electronico obtenido desde ARCA/AFIP.")
.HasColumnName("cae");
entity.Property(e => e.CaeExpirationDate)
.HasComment("Fecha de vencimiento del CAE.")
.HasColumnType("datetime")
.HasColumnName("cae_expiration_date");
entity.Property(e => e.CompletedAtUtc)
.HasComment("Fecha/hora UTC de finalizacion del flujo fiscal.")
.HasColumnType("datetime")
.HasColumnName("completed_at_utc");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.Environment)
.HasMaxLength(20)
.HasComment("Ambiente fiscal usado para autorizacion: homologacion, produccion u otro valor definido por configuracion.")
.HasColumnName("environment");
entity.Property(e => e.ErrorsJson)
.HasComment("Errores devueltos por ARCA/AFIP serializados como JSON.")
.HasColumnName("errors_json");
entity.Property(e => e.EventsJson)
.HasComment("Eventos devueltos por ARCA/AFIP serializados como JSON.")
.HasColumnName("events_json");
entity.Property(e => e.FiscalStatus)
.HasComment("Estado fiscal independiente del estado comercial. Ejemplos: None, Pending, Authorized, Rejected, Error, PendingReconciliation.")
.HasColumnName("fiscal_status");
entity.Property(e => e.IsFinal)
.HasComment("Indica si el resultado fiscal es final y no debe volver a mutar salvo procesos controlados de auditoria.")
.HasColumnName("is_final");
entity.Property(e => e.Modifiedat)
.HasColumnType("datetime")
.HasColumnName("modifiedat");
entity.Property(e => e.ObservationsJson)
.HasComment("Observaciones devueltas por ARCA/AFIP serializadas como JSON.")
.HasColumnName("observations_json");
entity.Property(e => e.PointOfSale)
.HasComment("Punto de venta fiscal ARCA/AFIP.")
.HasColumnName("point_of_sale");
entity.Property(e => e.ReconciledAfterTimeout)
.HasComment("Indica que el documento fiscal fue resuelto mediante reconciliacion posterior a timeout o resultado ambiguo.")
.HasColumnName("reconciled_after_timeout");
entity.Property(e => e.RequestFingerprint)
.HasMaxLength(255)
.HasComment("Huella de idempotencia fiscal para evitar duplicacion de solicitudes ante ARCA.")
.HasColumnName("request_fingerprint");
entity.Property(e => e.SalesdocumentId)
.HasComment("Documento comercial interno vinculado al documento fiscal.")
.HasColumnName("salesdocument_id");
entity.Property(e => e.VoucherLetter)
.HasMaxLength(5)
.HasComment("Letra fiscal del comprobante autorizado o a autorizar.")
.HasColumnName("voucher_letter");
entity.Property(e => e.VoucherNumber)
.HasComment("Numero fiscal del comprobante asignado para ARCA. Se mantiene separado del numero interno.")
.HasColumnName("voucher_number");
entity.Property(e => e.VoucherType)
.HasComment("Tipo de comprobante fiscal ARCA/AFIP utilizado en FECAESolicitar.")
.HasColumnName("voucher_type");
entity.HasOne(d => d.Salesdocument).WithOne(p => p.PhSSalesFiscalDocument)
.HasForeignKey<PhSSalesFiscalDocument>(d => d.SalesdocumentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesFiscalDocuments_SalesDocuments");
});
modelBuilder.Entity<PhSSalesFiscalDocumentAssociation>(entity =>
{
entity.ToTable("PhS_SalesFiscalDocumentAssociations", tb => tb.HasComment("Comprobantes fiscales asociados enviados como CbtesAsoc. Aplica a notas de credito, notas de debito, NCE/NDE y casos FCE donde corresponda."));
entity.HasIndex(e => e.AssociatedSalesdocumentId, "IX_PhS_SalesFiscalDocumentAssociations_AssociatedDocument");
entity.HasIndex(e => e.SalesfiscaldocumentId, "IX_PhS_SalesFiscalDocumentAssociations_FiscalDocument");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.AssociatedPointOfSale)
.HasComment("Punto de venta fiscal del comprobante asociado.")
.HasColumnName("associated_point_of_sale");
entity.Property(e => e.AssociatedSalesdocumentId)
.HasComment("Documento comercial interno asociado, si existe dentro de PhronCare.")
.HasColumnName("associated_salesdocument_id");
entity.Property(e => e.AssociatedVoucherCuit)
.HasMaxLength(20)
.HasComment("CUIT emisor del comprobante asociado, cuando sea requerido por ARCA.")
.HasColumnName("associated_voucher_cuit");
entity.Property(e => e.AssociatedVoucherDate)
.HasComment("Fecha del comprobante fiscal asociado.")
.HasColumnType("datetime")
.HasColumnName("associated_voucher_date");
entity.Property(e => e.AssociatedVoucherNumber)
.HasComment("Numero fiscal del comprobante asociado.")
.HasColumnName("associated_voucher_number");
entity.Property(e => e.AssociatedVoucherType)
.HasComment("Tipo fiscal ARCA/AFIP del comprobante asociado.")
.HasColumnName("associated_voucher_type");
entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("createdat");
entity.Property(e => e.SalesfiscaldocumentId)
.HasComment("Documento fiscal que contiene esta asociacion.")
.HasColumnName("salesfiscaldocument_id");
entity.HasOne(d => d.AssociatedSalesdocument).WithMany(p => p.PhSSalesFiscalDocumentAssociations)
.HasForeignKey(d => d.AssociatedSalesdocumentId)
.HasConstraintName("FK_PhS_SalesFiscalDocumentAssociations_SalesDocuments");
entity.HasOne(d => d.Salesfiscaldocument).WithMany(p => p.PhSSalesFiscalDocumentAssociations)
.HasForeignKey(d => d.SalesfiscaldocumentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhS_SalesFiscalDocumentAssociations_FiscalDocuments");
});
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);
} }

View File

@ -0,0 +1,187 @@
using Domain.Dtos.Sales;
using Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories
{
public class PhSDeliveryNoteRepository(PhronCareOperationsHubContext context) : IPhSDeliveryNoteRepository
{
private readonly PhronCareOperationsHubContext _context = context;
public async Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(
int? customerId,
string? customerText,
string? deliveryNoteNumber,
int? quoteId,
string? quoteNumber,
DateTime? issueDateFrom,
DateTime? issueDateTo,
string? status,
int page = 1,
int pageSize = 50)
{
var query = _context.PhSDeliveryNotes
.AsNoTracking()
.Include(x => x.Customer)
.Include(x => x.Quote)
.AsQueryable();
if (customerId.HasValue)
query = query.Where(x => x.CustomerId == customerId.Value);
else if (!string.IsNullOrWhiteSpace(customerText))
query = query.Where(x => x.Customer.Name.Contains(customerText));
if (!string.IsNullOrWhiteSpace(deliveryNoteNumber))
query = query.Where(x => x.Deliverynotenumber.Contains(deliveryNoteNumber));
if (quoteId.HasValue)
query = query.Where(x => x.QuoteId == quoteId.Value);
else if (!string.IsNullOrWhiteSpace(quoteNumber))
query = query.Where(x => x.Quote != null && x.Quote.Quotenumber.Contains(quoteNumber));
if (issueDateFrom.HasValue)
query = query.Where(x => x.Issuedate >= issueDateFrom.Value);
if (issueDateTo.HasValue)
query = query.Where(x => x.Issuedate <= issueDateTo.Value);
if (!string.IsNullOrWhiteSpace(status))
query = query.Where(x => x.Status == status);
query = query
.OrderByDescending(x => x.Issuedate)
.ThenByDescending(x => x.Id);
var pagedEntities = await query.ToPagedResultAsync(page, pageSize);
var dtos = pagedEntities.Items.Select(x => new DeliveryNoteSummaryDto
{
Id = x.Id,
DeliveryNoteNumber = x.Deliverynotenumber,
QuoteId = x.QuoteId,
QuoteNumber = x.Quote?.Quotenumber,
IssueDate = x.Issuedate,
CustomerId = x.CustomerId,
CustomerName = x.Customer?.Name ?? string.Empty,
Status = x.Status,
Observations = x.Observations,
PrintCount = x.Printcount,
CreatedAt = x.Createdat,
ModifiedAt = x.Modifiedat
}).ToList();
return new PagedResult<DeliveryNoteSummaryDto>
{
Items = dtos,
TotalItems = pagedEntities.TotalItems,
Page = pagedEntities.Page,
PageSize = pagedEntities.PageSize
};
}
public async Task<DeliveryNoteDto?> GetDtoByIdAsync(int id)
{
var entity = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
return entity == null ? null : MapDeliveryNoteDto(entity);
}
public async Task<DeliveryNoteDto?> GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber)
{
var entity = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber);
return entity == null ? null : MapDeliveryNoteDto(entity);
}
public async Task<IEnumerable<DeliveryNoteDto>> GetDtosByQuoteIdAsync(int quoteId)
{
var entities = await _context.PhSDeliveryNotes
.Include(x => x.Customer)
.Include(x => x.Quote)
.Include(x => x.PhSDeliveryNoteDetails)
.AsNoTracking()
.Where(x => x.QuoteId == quoteId)
.OrderByDescending(x => x.Issuedate)
.ThenByDescending(x => x.Id)
.ToListAsync();
return entities.Select(MapDeliveryNoteDto);
}
public async Task<bool> ExistsByDeliveryNoteNumberAsync(string deliveryNoteNumber)
{
return await _context.PhSDeliveryNotes
.AsNoTracking()
.AnyAsync(x => x.Deliverynotenumber == deliveryNoteNumber);
}
public async Task<EDeliveryNote> CreateAsync(EDeliveryNote entity)
{
var mapped = EntityMapper.MapEntity<EDeliveryNote, PhSDeliveryNote>(entity);
await _context.PhSDeliveryNotes.AddAsync(mapped);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSDeliveryNote, EDeliveryNote>(mapped);
}
private static DeliveryNoteDto MapDeliveryNoteDto(PhSDeliveryNote source)
{
return new DeliveryNoteDto
{
Id = source.Id,
DeliveryNoteNumber = source.Deliverynotenumber,
CustomerName = source.Customer?.Name ?? string.Empty,
QuoteId = source.QuoteId,
QuoteNumber = source.Quote?.Quotenumber,
SalesInvoiceId = source.SalesinvoiceId,
IssueDate = source.Issuedate,
CustomerId = source.CustomerId,
Status = source.Status,
Observations = source.Observations,
ExtraInfoJson = source.ExtrainfoJson,
PrintCount = source.Printcount,
LogoBase64 = null,
CreatedAt = source.Createdat,
ModifiedAt = source.Modifiedat,
Items = source.PhSDeliveryNoteDetails
.OrderBy(d => d.LineNumber)
.ThenBy(d => d.Id)
.Select(MapDeliveryNoteItemDto)
.ToList()
};
}
private static DeliveryNoteItemDto MapDeliveryNoteItemDto(PhSDeliveryNoteDetail source)
{
return new DeliveryNoteItemDto
{
Id = source.Id,
DeliverynoteId = source.DeliverynoteId,
LineNumber = source.LineNumber,
OriginType = source.OriginType,
OriginId = source.OriginId,
QuoteDetailId = source.QuoteDetailId,
Description = source.Description??string.Empty,
Quantity = source.Quantity,
Notes = source.Notes,
Createdat = source.Createdat,
Modifiedat = source.Modifiedat
};
}
}
}

View File

@ -97,7 +97,7 @@ namespace Models.Repositories
return await ( return await (
from q in _context.PhSQuoteHeaders from q in _context.PhSQuoteHeaders
join c in _context.PhSCustomers on q.CustomerId equals c.Id join c in _context.PhSCustomers on q.CustomerId equals c.Id
where q.Status == "Emitido" && where q.Status == "Aprobado" &&
(q.Quotenumber.Contains(filter) || c.Name.Contains(filter)) (q.Quotenumber.Contains(filter) || c.Name.Contains(filter))
orderby q.Issuedate descending orderby q.Issuedate descending
select new ELookUpItem select new ELookUpItem

View File

@ -175,12 +175,17 @@ namespace Models.Repositories
var itemTax = totalTaxAmount * itemBase / netBase; var itemTax = totalTaxAmount * itemBase / netBase;
return new QuoteItemDto return new QuoteItemDto
{ {
Id = d.Id,
Description = d.ProductDescription, Description = d.ProductDescription,
Quantity = d.Quantity, Quantity = d.Quantity,
UnitPrice = d.Unitprice, UnitPrice = d.Unitprice,
Subtotal = itemBase, Subtotal = itemBase,
TaxAmount = itemTax, TaxAmount = itemTax,
Total = itemBase + itemTax Total = itemBase + itemTax,
Approved = d.Approved,
ApprovedQuantity = d.Approvedquantity,
ApprovedUnitPrice = d.Approvedunitprice,
ApprovedAmount = d.Approvedamount
}; };
}).ToList(), }).ToList(),
@ -303,7 +308,11 @@ namespace Models.Repositories
UnitPrice = d.Unitprice, UnitPrice = d.Unitprice,
Subtotal = itemBase, Subtotal = itemBase,
TaxAmount = itemTax, TaxAmount = itemTax,
Total = itemBase + itemTax Total = itemBase + itemTax,
Approved = d.Approved,
ApprovedQuantity = d.Approvedquantity,
ApprovedUnitPrice = d.Approvedunitprice,
ApprovedAmount = d.Approvedamount
}; };
}).ToList(), }).ToList(),

View File

@ -0,0 +1,105 @@
using Domain.Dtos.Sales;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories
{
public class PhSSalesDocumentRepository(PhronCareOperationsHubContext context) : IPhSSalesDocumentRepository
{
private readonly PhronCareOperationsHubContext _context = context;
public async Task<ESalesDocument> CreateAsync(ESalesDocument entity)
{
var mapped = EntityMapper.MapEntity<ESalesDocument, PhSSalesDocument>(entity);
await _context.PhSSalesDocuments.AddAsync(mapped);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSSalesDocument, ESalesDocument>(mapped);
}
public async Task<SalesDocumentDto?> GetDtoByIdAsync(int id)
{
var entity = await _context.PhSSalesDocuments
.Include(x => x.Customer)
.Include(x => x.BillToCustomer)
.Include(x => x.PhSSalesDocumentDetails)
.Include(x => x.PhSSalesDocumentCoverages)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
return null;
return new SalesDocumentDto
{
Id = entity.Id,
FormseriesId = entity.FormseriesId,
InternalSequenceNumber = entity.InternalSequenceNumber,
InternalDocumentNumber = entity.InternalDocumentNumber,
DocumentType = entity.DocumentType,
FiscalVoucherType = entity.FiscalVoucherType,
FiscalVoucherLetter = entity.FiscalVoucherLetter,
Status = entity.Status,
QuoteId = entity.QuoteId,
CustomerId = entity.CustomerId,
CustomerName = entity.Customer?.Name ?? string.Empty,
BillToCustomerId = entity.BillToCustomerId,
BillToCustomerName = entity.BillToCustomer?.Name ?? string.Empty,
IssueDate = entity.IssueDate,
Currency = entity.Currency,
ExchangeRate = entity.ExchangeRate,
NetAmount = entity.NetAmount,
TaxAmount = entity.TaxAmount,
TotalAmount = entity.TotalAmount,
Observations = entity.Observations,
ExtraInfoJson = entity.ExtraInfoJson,
PeriodFrom = entity.PeriodFrom,
PeriodTo = entity.PeriodTo,
Createdat = entity.Createdat,
Modifiedat = entity.Modifiedat,
Details = entity.PhSSalesDocumentDetails.Select(x => new SalesDocumentDetailDto
{
Id = x.Id,
SalesDocumentId = x.SalesdocumentId,
LineNumber = x.LineNumber,
OriginType = x.OriginType,
OriginId = x.OriginId,
QuoteDetailId = x.QuoteDetailId,
ProductId = x.ProductId,
Description = x.Description,
Quantity = x.Quantity,
AuthorizedUnitPrice = x.AuthorizedUnitPrice,
AuthorizedAmount = x.AuthorizedAmount,
BilledPercentage = x.BilledPercentage,
UnitPrice = x.UnitPrice,
NetAmount = x.NetAmount,
TaxAmount = x.TaxAmount,
TotalAmount = x.TotalAmount,
OriginSnapshotJson = x.OriginSnapshotJson,
Createdat = x.Createdat,
Modifiedat = x.Modifiedat
}).ToList(),
Coverage = entity.PhSSalesDocumentCoverages.Select(x => new SalesDocumentCoverageDto
{
Id = x.Id,
SalesDocumentId = x.SalesdocumentId,
SalesDocumentDetailId = x.SalesdocumentdetailId,
QuoteId = x.QuoteId,
QuoteDetailId = x.QuoteDetailId,
CoverageType = x.CoverageType,
CoveragePercentage = x.CoveragePercentage,
CoverageAmount = x.CoverageAmount,
PeriodFrom = x.PeriodFrom,
PeriodTo = x.PeriodTo,
Notes = x.Notes,
Createdat = x.Createdat,
Modifiedat = x.Modifiedat
}).ToList()
};
}
}
}

View File

@ -2,10 +2,12 @@
using Domain.Dtos.Stock; using Domain.Dtos.Stock;
using Domain.Entities; using Domain.Entities;
using Domain.Generics; using Domain.Generics;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Models.Helpers; using Models.Helpers;
using Models.Interfaces; using Models.Interfaces;
using Models.Models; using Models.Models;
using System.Data;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
@ -56,7 +58,6 @@ namespace Models.Repositories.Stock
throw; throw;
} }
} }
/// <summary> /// <summary>
/// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión. /// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
/// </summary> /// </summary>
@ -164,9 +165,7 @@ namespace Models.Repositories.Stock
return dto; return dto;
} }
// ----- helpers ----- // ----- helpers -----
/// <summary> /// <summary>
/// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6). /// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
/// </summary> /// </summary>
@ -310,6 +309,73 @@ namespace Models.Repositories.Stock
PageSize = pageSize PageSize = pageSize
}; };
} }
public async Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(
IEnumerable<int> stockItemIds,
int? ignoreExpeditionId)
{
// Normalización defensiva
var ids = (stockItemIds ?? Enumerable.Empty<int>())
.Where(x => x > 0)
.Distinct()
.ToList();
if (ids.Count == 0)
return new List<StockItemExpeditionConflictDto>();
// TVP: dbo.PhLSM_StockItemIdList(stockitem_id int not null)
var tvp = new DataTable();
tvp.Columns.Add("stockitem_id", typeof(int));
foreach (var id in ids)
tvp.Rows.Add(id);
var results = new List<StockItemExpeditionConflictDto>();
// Usamos la conexión del DbContext (no creamos otra)
var conn = _context.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
await _context.Database.OpenConnectionAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "dbo.PhLSM_Expedition_CheckStockItemConflicts";
cmd.CommandType = CommandType.StoredProcedure;
// Param TVP
var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
{
TypeName = "dbo.PhLSM_StockItemIdList",
Value = tvp
};
cmd.Parameters.Add(pIds);
// Param opcional para edición
var pIgnore = new SqlParameter("@IgnoreExpeditionId", SqlDbType.Int)
{
Value = ignoreExpeditionId.HasValue ? ignoreExpeditionId.Value : DBNull.Value
};
cmd.Parameters.Add(pIgnore);
await using var reader = await cmd.ExecuteReaderAsync();
// Ordinals por nombre (más robusto ante cambios de orden)
var ordStockItemId = reader.GetOrdinal("StockitemId");
var ordExpId = reader.GetOrdinal("ExpeditionId");
var ordExpNum = reader.GetOrdinal("Expeditionnumber");
var ordStatus = reader.GetOrdinal("Status");
while (await reader.ReadAsync())
{
results.Add(new StockItemExpeditionConflictDto
{
StockitemId = reader.GetInt32(ordStockItemId),
ExpeditionId = reader.GetInt32(ordExpId),
Expeditionnumber = reader.IsDBNull(ordExpNum) ? string.Empty : reader.GetString(ordExpNum),
Status = reader.GetInt32(ordStatus)
});
}
return results;
}
private static int? MapStatusLabelToInt(string labelOrNumber) private static int? MapStatusLabelToInt(string labelOrNumber)
{ {
if (string.IsNullOrWhiteSpace(labelOrNumber)) return null; if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;
@ -329,7 +395,6 @@ namespace Models.Repositories.Stock
_ => (int?)null _ => (int?)null
}; };
} }
private static string NormalizeKey(string s) private static string NormalizeKey(string s)
{ {
var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD); var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
@ -339,7 +404,165 @@ namespace Models.Repositories.Stock
sb.Append(ch); sb.Append(ch);
return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", ""); return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", "");
} }
public async Task MarkInTransitAsync(int expeditionId)
{
const byte expeditionReservationSourceType = 1;
const int reservedStatus = 1;
var header = await _context.PhLsmExpeditionHeaders
.Include(x => x.PhLsmExpeditionDetails)
.FirstOrDefaultAsync(x => x.Id == expeditionId);
if (header == null)
throw new KeyNotFoundException($"No se encontró la expedición con ID {expeditionId}.");
if (header.Status != (int)ExpeditionStatus.Emitida)
throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");
var details = header.PhLsmExpeditionDetails?.ToList() ?? new List<PhLsmExpeditionDetail>();
if (details.Count == 0)
throw new InvalidOperationException("No se puede pasar la expedición a 'En tránsito' porque no tiene ítems para reservar.");
var invalidStockItems = details
.Where(d => d.StockitemId <= 0)
.Select(d => d.Id)
.OrderBy(x => x)
.ToList();
if (invalidStockItems.Count > 0)
{
throw new InvalidOperationException(
"No se puede pasar la expedición a 'En tránsito' porque existen detalles sin stockitem_id válido. " +
$"Detalle(s): {string.Join(", ", invalidStockItems)}");
}
var duplicateStockItems = details
.GroupBy(d => d.StockitemId)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.OrderBy(x => x)
.ToList();
if (duplicateStockItems.Count > 0)
{
throw new InvalidOperationException(
"No se puede pasar la expedición a 'En tránsito' porque el mismo StockItem aparece más de una vez en la expedición: " +
string.Join(", ", duplicateStockItems));
}
var detailByStockItem = details
.Select(d => new
{
DetailId = d.Id,
StockitemId = d.StockitemId,
Quantity = d.Quantity
})
.ToList();
var stockItemIds = detailByStockItem
.Select(x => x.StockitemId)
.Distinct()
.ToList();
var duplicatedReservations = await _context.PhLsmStockReservations
.AsNoTracking()
.Where(r =>
r.SourceType == expeditionReservationSourceType &&
r.SourceId == expeditionId &&
r.Status == reservedStatus &&
stockItemIds.Contains(r.StockitemId))
.Select(r => r.StockitemId)
.Distinct()
.OrderBy(x => x)
.ToListAsync();
if (duplicatedReservations.Count > 0)
{
throw new InvalidOperationException(
"La expedición ya posee reservas activas para los siguientes StockItem: " +
string.Join(", ", duplicatedReservations));
}
var stockItems = await _context.PhLsmStockItems
.Where(x => stockItemIds.Contains(x.Id))
.ToListAsync();
var stockItemsById = stockItems.ToDictionary(x => x.Id);
var missingStockItems = stockItemIds
.Where(id => !stockItemsById.ContainsKey(id))
.OrderBy(x => x)
.ToList();
if (missingStockItems.Count > 0)
{
throw new InvalidOperationException(
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no existen: " +
string.Join(", ", missingStockItems));
}
var insufficientAvailability = new List<string>();
foreach (var item in detailByStockItem.OrderBy(x => x.StockitemId))
{
var stockItem = stockItemsById[item.StockitemId];
var availableQuantity = stockItem.Quantity - stockItem.ReservedQuantity;
if (item.Quantity > availableQuantity)
{
insufficientAvailability.Add(
$"• StockItem {item.StockitemId} → solicitado: {item.Quantity}, disponible: {availableQuantity}.");
}
}
if (insufficientAvailability.Count > 0)
{
var lines = new List<string>
{
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no tienen cantidad disponible suficiente para reservar."
};
lines.AddRange(insufficientAvailability);
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
}
using var tx = await _context.Database.BeginTransactionAsync();
try
{
var now = DateTime.Now;
var reservations = detailByStockItem.Select(item => new PhLsmStockReservation
{
SourceType = expeditionReservationSourceType,
SourceId = expeditionId,
StockitemId = item.StockitemId,
ReservedQuantity = item.Quantity,
Status = reservedStatus,
Createdat = now
}).ToList();
_context.PhLsmStockReservations.AddRange(reservations);
foreach (var item in detailByStockItem)
{
var stockItem = stockItemsById[item.StockitemId];
stockItem.ReservedQuantity += item.Quantity;
stockItem.Modifiedat = now;
}
header.Status = (int)ExpeditionStatus.EnTransito;
header.Modifiedat = now;
await _context.SaveChangesAsync();
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
}
} }
} }

View File

@ -1,9 +1,11 @@
using Domain.Dtos.Stock; // StockItemScanResultDto using Microsoft.EntityFrameworkCore;
using Microsoft.Data.SqlClient;
using Domain.Dtos.Stock; // StockItemScanResultDto
using Domain.Generics; // PagedResult<T> using Domain.Generics; // PagedResult<T>
using Microsoft.EntityFrameworkCore;
using Models.Helpers; // ToPagedResultAsync using Models.Helpers; // ToPagedResultAsync
using Models.Interfaces; // IPhLSMStockItemRepository using Models.Interfaces; // IPhLSMStockItemRepository
using Models.Models; // PhronCareOperationsHubContext using Models.Models;
using System.Data; // PhronCareOperationsHubContext
namespace Models.Repositories.Stock namespace Models.Repositories.Stock
{ {
@ -324,5 +326,74 @@ namespace Models.Repositories.Stock
PageSize = paged.PageSize PageSize = paged.PageSize
}; };
} }
public async Task<List<StockItemAvailabilityDto>> GetAvailabilityByStockItemIdsAsync(
IEnumerable<int> stockItemIds)
{
try
{
var ids = stockItemIds?
.Where(x => x > 0)
.Distinct()
.ToList() ?? new List<int>();
if (ids.Count == 0)
return new List<StockItemAvailabilityDto>();
// TVP = Table Valued Parameter
var tvp = new DataTable();
tvp.Columns.Add("stockitem_id", typeof(int));
foreach (var id in ids)
tvp.Rows.Add(id);
var result = new List<StockItemAvailabilityDto>();
var conn = _context.Database.GetDbConnection();
var cs = _context.Database.GetConnectionString();
if (conn.State != ConnectionState.Open)
await _context.Database.OpenConnectionAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "dbo.PhLSM_Stock_GetAvailabilityByStockItemIds";
cmd.CommandType = CommandType.StoredProcedure;
var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
{
TypeName = "dbo.PhLSM_StockItemIdList",
Value = tvp
};
cmd.Parameters.Add(pIds);
await using var reader = await cmd.ExecuteReaderAsync();
var ordStockItemId = reader.GetOrdinal("StockitemId");
var ordQuantity = reader.GetOrdinal("Quantity");
var ordReserved = reader.GetOrdinal("ReservedQuantity");
var ordAvailable = reader.GetOrdinal("AvailableQuantity");
var ordSerial = reader.GetOrdinal("Serial");
while (await reader.ReadAsync())
{
result.Add(new StockItemAvailabilityDto
{
StockitemId = reader.GetInt32(ordStockItemId),
Quantity = reader.GetDecimal(ordQuantity),
ReservedQuantity = reader.GetDecimal(ordReserved),
AvailableQuantity = reader.GetDecimal(ordAvailable),
Serial = reader.IsDBNull(ordSerial)
? null
: reader.GetString(ordSerial)
});
}
return result;
}
catch (Exception ex)
{
throw new Exception(ex.ToString());
}
}
} }
} }

View File

@ -1,7 +0,0 @@

namespace Models.Repositories.Stock
{
internal class PhLSMStockReservationRepository
{
}
}

165
README.md
View File

@ -1,93 +1,140 @@
# phronCare # PhronCare
## 📌 Descripción
PhronCare es una plataforma back-office desarrollada en .NET 8 para la gestión de operaciones en ortopedia y logística médica.
Incluye módulos principales como:
- Presupuestos (Quotes)
- Expediciones (Expeditions)
- Stock médico con trazabilidad GS1
- Generación de documentos (PDF / Excel)
## Getting started ---
To make it easy for you to get started with GitLab, here's a list of recommended next steps. ## 🧱 Arquitectura
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! El sistema sigue una arquitectura estricta por capas:
## Add your files Data → Domain → Core → API → UI (Blazor)
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files Reglas clave:
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - NO modificar modelos EF generados por scaffold (Models/Data)
- La lógica de negocio vive en Domain/Core
- Las entidades de dominio (E*) replican estructura de DB
- Core orquesta casos de uso y transacciones
``` ---
cd existing_repo
git remote add origin https://gitlab.com/maskinc00/phronCare.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools ## 📦 Estructura del proyecto
- [ ] [Set up project integrations](https://gitlab.com/maskinc00/phronCare/-/settings/integrations) /src
├── 1.1 Data → EF Models + Repositories
├── 1.2 Domain → Entidades E*, contratos, enums
├── 1.3 Core → Servicios de negocio
├── API → Controllers / Endpoints
├── UIBlazor → Frontend Blazor
├── Documents → Templates PDF / Excel
## Collaborate with your team ---
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) ## ⚙️ Requisitos
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy - .NET 8 SDK
- SQL Server
- Docker (opcional)
- Node.js (solo si se utiliza en contenedor o build específico)
Use the built-in continuous integration in GitLab. ---
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) ## 🚀 Ejecución local
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
*** 1. Configurar `appsettings.json` con la conexión a base de datos
2. Verificar que la base de datos esté disponible
3. Ejecutar la API:
dotnet run (en proyecto API)
4. Ejecutar la UI Blazor:
dotnet run (en proyecto UIBlazor)
# Editing this README Opcional:
- Ejecutar con Docker Compose si está configurado
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. ---
## Suggestions for a good README ## 🔌 Endpoints principales
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - /api/quotes
- /api/expeditions
- /api/stock
- /api/lsstockscan
## Name Swagger disponible en:
Choose a self-explaining name for your project. - /swagger
## Description ---
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges ## 📄 Generación de documentos
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals El sistema soporta generación de:
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation - PDF (Quotes, Expeditions, Delivery Notes)
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - Excel (exportaciones)
## Usage Implementación basada en:
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - DocumentTemplateService
- Templates Razor
- PuppeteerSharp
## Support ---
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap ## 🧠 Convenciones del proyecto
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing - Tablas: PascalCase
State if you are open to contributions and what your requirements are for accepting them. - Campos: snake_case
- Foreign Keys: *_id
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. Prefijos por módulo:
- PhS_ → Sales
- PhLSM_ → Logística y Stock Médico
- PhOH_ → Operations Hub
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. Otros:
- Entidades Domain: prefijo E (EQuoteHeader, etc.)
- DTOs separados de entidades
- No usar AutoMapper (usar EntityMapper)
## Authors and acknowledgment ---
Show your appreciation to those who have contributed to the project.
## License ## 🔄 Flujo de desarrollo
For open source projects, say how it is licensed.
## Project status - API-First
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. - Stories pequeñas y atómicas
- Branching:
- feature/{issue}-{desc}
- fix/{issue}-{desc}
Commits:
- Conventional Commits
- Uso de “Closes #issue” en PR
---
## 📋 Stories y documentación
Las stories del proyecto siguen una plantilla estándar ubicada en:
/docs/story-template.md
Incluye:
- Objetivo
- Contexto
- Alcance
- Criterios de aceptación
- Decisiones de diseño
---
## ⚠️ Notas importantes
- No romper contratos existentes de la API
- Mantener consistencia con módulos existentes
- Priorizar claridad sobre complejidad

View File

@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EPPlus" Version="7.5.2" /> <PackageReference Include="EPPlus" Version="7.5.2" />
<PackageReference Include="MimeKit" Version="4.8.0" /> <PackageReference Include="MimeKit" Version="4.15.1" />
<PackageReference Include="NETCore.MailKit" Version="2.1.0" /> <PackageReference Include="NETCore.MailKit" Version="2.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,275 @@
-- ============================================================
-- Story #55 - Sales Document Coverage incremental update
-- ============================================================
-- Objetivo:
-- Ajustar el modelo de documentos de venta ya creado para soportar
-- facturacion por periodo/capita y trazabilidad de presupuestos cubiertos.
--
-- Contexto:
-- El scaffold actualizado ya incluye:
-- - PhS_SalesDocuments
-- - PhS_SalesDocumentDetails
-- - PhS_SalesFiscalDocuments
-- - PhS_SalesFiscalDocumentAssociations
--
-- Decisiones:
-- - No se recrean tablas existentes.
-- - No se modifican modelos EF manualmente.
-- - Se agrega periodo a la cabecera comercial.
-- - Se agrega PhS_SalesDocumentCoverage como verdad de cobertura.
-- - Coverage se usa tanto para facturacion 1 a 1 como para capita.
-- - quote_id en PhS_SalesDocuments queda como referencia principal/rapida,
-- no como verdad completa de facturacion.
-- ============================================================
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- ============================================================
-- PhS_SalesDocuments: periodo comercial opcional
-- ============================================================
IF COL_LENGTH('dbo.PhS_SalesDocuments', 'period_from') IS NULL
BEGIN
ALTER TABLE dbo.PhS_SalesDocuments
ADD period_from DATETIME NULL;
END;
IF COL_LENGTH('dbo.PhS_SalesDocuments', 'period_to') IS NULL
BEGIN
ALTER TABLE dbo.PhS_SalesDocuments
ADD period_to DATETIME NULL;
END;
IF OBJECT_ID('dbo.CK_PhS_SalesDocuments_Period', 'C') IS NULL
BEGIN
ALTER TABLE dbo.PhS_SalesDocuments
ADD CONSTRAINT CK_PhS_SalesDocuments_Period
CHECK (period_from IS NULL OR period_to IS NULL OR period_to >= period_from);
END;
-- ============================================================
-- PhS_SalesDocumentCoverage
-- ============================================================
-- Representa que presupuestos/casos quedan cubiertos por un documento
-- de venta. Es la fuente real para determinar si un presupuesto queda
-- pendiente de facturacion o ya fue cubierto/facturado.
-- ============================================================
IF OBJECT_ID('dbo.PhS_SalesDocumentCoverage', 'U') IS NULL
BEGIN
CREATE TABLE dbo.PhS_SalesDocumentCoverage
(
id INT IDENTITY(1,1) NOT NULL,
salesdocument_id INT NOT NULL,
salesdocumentdetail_id INT NULL,
quote_id INT NOT NULL,
quote_detail_id INT NULL,
-- Tipo de cobertura.
-- Valores esperados en Domain:
-- 1 = Direct
-- 2 = Capita
-- 3 = Adjustment
coverage_type INT NOT NULL CONSTRAINT DF_PhS_SalesDocumentCoverage_coverage_type DEFAULT (1),
-- Porcentaje/importe cubierto respecto del presupuesto/caso.
-- En facturacion 1 a 1 normalmente sera 100.
-- En obra social / particular puede ser 60/40.
-- En capita puede ser 100 aunque la linea facturada sea agregada.
coverage_percentage DECIMAL(9,4) NULL,
coverage_amount DECIMAL(18,2) NULL,
period_from DATETIME NULL,
period_to DATETIME NULL,
notes NVARCHAR(MAX) NULL,
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesDocumentCoverage_createdat DEFAULT (GETDATE()),
modifiedat DATETIME NULL,
CONSTRAINT PK_PhS_SalesDocumentCoverage PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_SalesDocuments
FOREIGN KEY (salesdocument_id)
REFERENCES dbo.PhS_SalesDocuments(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_SalesDocumentDetails
FOREIGN KEY (salesdocumentdetail_id)
REFERENCES dbo.PhS_SalesDocumentDetails(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_QuoteHeaders
FOREIGN KEY (quote_id)
REFERENCES dbo.PhS_QuoteHeaders(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_QuoteDetails
FOREIGN KEY (quote_detail_id)
REFERENCES dbo.PhS_QuoteDetails(id),
CONSTRAINT CK_PhS_SalesDocumentCoverage_Period
CHECK (period_from IS NULL OR period_to IS NULL OR period_to >= period_from),
CONSTRAINT CK_PhS_SalesDocumentCoverage_Percentage
CHECK (coverage_percentage IS NULL OR (coverage_percentage > 0 AND coverage_percentage <= 100))
);
END;
-- ============================================================
-- Indices
-- ============================================================
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Document' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Document
ON dbo.PhS_SalesDocumentCoverage(salesdocument_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_DocumentDetail' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_DocumentDetail
ON dbo.PhS_SalesDocumentCoverage(salesdocumentdetail_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Quote' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Quote
ON dbo.PhS_SalesDocumentCoverage(quote_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_QuoteDetail' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_QuoteDetail
ON dbo.PhS_SalesDocumentCoverage(quote_detail_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Period' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Period
ON dbo.PhS_SalesDocumentCoverage(period_from, period_to);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocuments_Period' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocuments'))
BEGIN
CREATE INDEX IX_PhS_SalesDocuments_Period
ON dbo.PhS_SalesDocuments(period_from, period_to);
END;
-- ============================================================
-- Extended properties / MS_Description
-- ============================================================
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocuments')
AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocuments'), N'period_from', 'ColumnId')
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Fecha inicial del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocuments',
@level2type = N'COLUMN', @level2name = N'period_from';
END;
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocuments')
AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocuments'), N'period_to', 'ColumnId')
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Fecha final del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocuments',
@level2type = N'COLUMN', @level2name = N'period_to';
END;
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage')
AND minor_id = 0
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Presupuestos/casos cubiertos por un documento de venta. Es la fuente real para determinar si un presupuesto queda pendiente de facturacion o ya fue cubierto, incluyendo facturacion 1 a 1 y capita.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocumentCoverage';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Identificador interno de la cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'salesdocument_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento de venta que cubre el presupuesto/caso.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'salesdocument_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'salesdocumentdetail_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalle del documento de venta asociado a esta cobertura, cuando aplique. En capita puede apuntar a la linea agregada mensual.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'salesdocumentdetail_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'quote_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Presupuesto/caso cubierto por el documento de venta. Se usa tanto para facturacion directa como para capita.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'quote_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'quote_detail_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalle de presupuesto cubierto, cuando se requiera trazabilidad granular por item.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'quote_detail_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_type', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo de cobertura. Valores esperados en Domain: Direct, Capita, Adjustment.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_type';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_percentage', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Porcentaje del presupuesto/caso cubierto por el documento. Permite 100% en facturacion directa o particiones 60/40.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_percentage';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_amount', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe de referencia cubierto por el documento, cuando aplique.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_amount';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'period_from', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha inicial del periodo de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'period_from';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'period_to', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha final del periodo de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'period_to';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'notes', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Notas internas de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'notes';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;

View File

@ -0,0 +1,279 @@
-- ============================================================
-- Story #55 - Sales Document Coverage incremental update
-- ============================================================
-- Objetivo:
-- Ajustar el modelo de documentos de venta ya creado para soportar
-- facturacion por periodo/capita y trazabilidad de presupuestos cubiertos.
--
-- Contexto:
-- El scaffold actualizado ya incluye:
-- - PhS_SalesDocuments
-- - PhS_SalesDocumentDetails
-- - PhS_SalesFiscalDocuments
-- - PhS_SalesFiscalDocumentAssociations
--
-- Decisiones:
-- - No se recrean tablas existentes.
-- - No se modifican modelos EF manualmente.
-- - Se agrega periodo a la cabecera comercial.
-- - Se agrega PhS_SalesDocumentCoverage como verdad de cobertura.
-- - Coverage se usa tanto para facturacion 1 a 1 como para capita.
-- - quote_id en PhS_SalesDocuments queda como referencia principal/rapida,
-- no como verdad completa de facturacion.
-- ============================================================
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- ============================================================
-- PhS_SalesDocuments: periodo comercial opcional
-- ============================================================
IF COL_LENGTH('dbo.PhS_SalesDocuments', 'period_from') IS NULL
BEGIN
ALTER TABLE dbo.PhS_SalesDocuments
ADD period_from DATETIME NULL;
END;
IF COL_LENGTH('dbo.PhS_SalesDocuments', 'period_to') IS NULL
BEGIN
ALTER TABLE dbo.PhS_SalesDocuments
ADD period_to DATETIME NULL;
END;
IF OBJECT_ID('dbo.CK_PhS_SalesDocuments_Period', 'C') IS NULL
BEGIN
EXEC sys.sp_executesql N'
ALTER TABLE dbo.PhS_SalesDocuments
ADD CONSTRAINT CK_PhS_SalesDocuments_Period
CHECK (period_from IS NULL OR period_to IS NULL OR period_to >= period_from);
';
END;
-- ============================================================
-- PhS_SalesDocumentCoverage
-- ============================================================
-- Representa que presupuestos/casos quedan cubiertos por un documento
-- de venta. Es la fuente real para determinar si un presupuesto queda
-- pendiente de facturacion o ya fue cubierto/facturado.
-- ============================================================
IF OBJECT_ID('dbo.PhS_SalesDocumentCoverage', 'U') IS NULL
BEGIN
CREATE TABLE dbo.PhS_SalesDocumentCoverage
(
id INT IDENTITY(1,1) NOT NULL,
salesdocument_id INT NOT NULL,
salesdocumentdetail_id INT NULL,
quote_id INT NOT NULL,
quote_detail_id INT NULL,
-- Tipo de cobertura.
-- Valores esperados en Domain:
-- 1 = Direct
-- 2 = Capita
-- 3 = Adjustment
coverage_type INT NOT NULL CONSTRAINT DF_PhS_SalesDocumentCoverage_coverage_type DEFAULT (1),
-- Porcentaje/importe cubierto respecto del presupuesto/caso.
-- En facturacion 1 a 1 normalmente sera 100.
-- En obra social / particular puede ser 60/40.
-- En capita puede ser 100 aunque la linea facturada sea agregada.
coverage_percentage DECIMAL(9,4) NULL,
coverage_amount DECIMAL(18,2) NULL,
period_from DATETIME NULL,
period_to DATETIME NULL,
notes NVARCHAR(MAX) NULL,
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesDocumentCoverage_createdat DEFAULT (GETDATE()),
modifiedat DATETIME NULL,
CONSTRAINT PK_PhS_SalesDocumentCoverage PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_SalesDocuments
FOREIGN KEY (salesdocument_id)
REFERENCES dbo.PhS_SalesDocuments(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_SalesDocumentDetails
FOREIGN KEY (salesdocumentdetail_id)
REFERENCES dbo.PhS_SalesDocumentDetails(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_QuoteHeaders
FOREIGN KEY (quote_id)
REFERENCES dbo.PhS_QuoteHeaders(id),
CONSTRAINT FK_PhS_SalesDocumentCoverage_QuoteDetails
FOREIGN KEY (quote_detail_id)
REFERENCES dbo.PhS_QuoteDetails(id),
CONSTRAINT CK_PhS_SalesDocumentCoverage_Period
CHECK (period_from IS NULL OR period_to IS NULL OR period_to >= period_from),
CONSTRAINT CK_PhS_SalesDocumentCoverage_Percentage
CHECK (coverage_percentage IS NULL OR (coverage_percentage > 0 AND coverage_percentage <= 100))
);
END;
-- ============================================================
-- Indices
-- ============================================================
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Document' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Document
ON dbo.PhS_SalesDocumentCoverage(salesdocument_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_DocumentDetail' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_DocumentDetail
ON dbo.PhS_SalesDocumentCoverage(salesdocumentdetail_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Quote' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Quote
ON dbo.PhS_SalesDocumentCoverage(quote_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_QuoteDetail' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_QuoteDetail
ON dbo.PhS_SalesDocumentCoverage(quote_detail_id);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocumentCoverage_Period' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'))
BEGIN
CREATE INDEX IX_PhS_SalesDocumentCoverage_Period
ON dbo.PhS_SalesDocumentCoverage(period_from, period_to);
END;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PhS_SalesDocuments_Period' AND object_id = OBJECT_ID(N'dbo.PhS_SalesDocuments'))
BEGIN
EXEC sys.sp_executesql N'
CREATE INDEX IX_PhS_SalesDocuments_Period
ON dbo.PhS_SalesDocuments(period_from, period_to);
';
END;
-- ============================================================
-- Extended properties / MS_Description
-- ============================================================
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocuments')
AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocuments'), N'period_from', 'ColumnId')
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Fecha inicial del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocuments',
@level2type = N'COLUMN', @level2name = N'period_from';
END;
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocuments')
AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocuments'), N'period_to', 'ColumnId')
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Fecha final del periodo comercial facturado. Aplica especialmente a facturacion por capita o periodos mensuales.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocuments',
@level2type = N'COLUMN', @level2name = N'period_to';
END;
IF NOT EXISTS (
SELECT 1
FROM sys.extended_properties
WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage')
AND minor_id = 0
AND name = N'MS_Description'
)
BEGIN
EXEC sys.sp_addextendedproperty
@name = N'MS_Description',
@value = N'Presupuestos/casos cubiertos por un documento de venta. Es la fuente real para determinar si un presupuesto queda pendiente de facturacion o ya fue cubierto, incluyendo facturacion 1 a 1 y capita.',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'PhS_SalesDocumentCoverage';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Identificador interno de la cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'salesdocument_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento de venta que cubre el presupuesto/caso.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'salesdocument_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'salesdocumentdetail_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalle del documento de venta asociado a esta cobertura, cuando aplique. En capita puede apuntar a la linea agregada mensual.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'salesdocumentdetail_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'quote_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Presupuesto/caso cubierto por el documento de venta. Se usa tanto para facturacion directa como para capita.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'quote_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'quote_detail_id', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalle de presupuesto cubierto, cuando se requiera trazabilidad granular por item.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'quote_detail_id';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_type', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo de cobertura. Valores esperados en Domain: Direct, Capita, Adjustment.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_type';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_percentage', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Porcentaje del presupuesto/caso cubierto por el documento. Permite 100% en facturacion directa o particiones 60/40.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_percentage';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'coverage_amount', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe de referencia cubierto por el documento, cuando aplique.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'coverage_amount';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'period_from', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha inicial del periodo de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'period_from';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'period_to', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha final del periodo de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'period_to';
END;
IF NOT EXISTS (SELECT 1 FROM sys.extended_properties WHERE major_id = OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage') AND minor_id = COLUMNPROPERTY(OBJECT_ID(N'dbo.PhS_SalesDocumentCoverage'), N'notes', 'ColumnId') AND name = N'MS_Description')
BEGIN
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Notas internas de cobertura.', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'PhS_SalesDocumentCoverage', @level2type=N'COLUMN', @level2name=N'notes';
END;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;

View File

@ -0,0 +1,620 @@
-- ============================================================
-- Story #55 - Sales Document persistence model
-- ============================================================
-- Objetivo:
-- Crear la base de persistencia para comprobantes de venta:
-- - Facturas internas
-- - Notas de debito / credito
-- - FCE / NDE / NCE futuras
-- - Autorizacion fiscal ARCA futura
--
-- Decisiones:
-- - No se crean nuevos talonarios.
-- - Se reutiliza PhS_FormSeries / PhS_FormSeriesNextNumber.
-- - La numeracion interna queda separada de la numeracion fiscal.
-- - El documento comercial queda separado del documento fiscal ARCA.
-- - El remito NO es obligatorio ni se modela como dependencia directa.
-- - El vinculo principal con origen comercial se mantiene por quote_detail_id.
-- ============================================================
IF OBJECT_ID('dbo.PhS_SalesFiscalDocumentAssociations', 'U') IS NOT NULL
DROP TABLE dbo.PhS_SalesFiscalDocumentAssociations;
IF OBJECT_ID('dbo.PhS_SalesFiscalDocuments', 'U') IS NOT NULL
DROP TABLE dbo.PhS_SalesFiscalDocuments;
IF OBJECT_ID('dbo.PhS_SalesDocumentDetails', 'U') IS NOT NULL
DROP TABLE dbo.PhS_SalesDocumentDetails;
IF OBJECT_ID('dbo.PhS_SalesDocuments', 'U') IS NOT NULL
DROP TABLE dbo.PhS_SalesDocuments;
-- ============================================================
-- Documento comercial interno
-- ============================================================
CREATE TABLE dbo.PhS_SalesDocuments
(
id INT IDENTITY(1,1) NOT NULL,
formseries_id INT NULL,
internal_sequence_number INT NULL,
internal_document_number NVARCHAR(50) NULL,
document_type INT NOT NULL,
fiscal_voucher_type INT NULL,
fiscal_voucher_letter NVARCHAR(5) NULL,
status INT NOT NULL CONSTRAINT DF_PhS_SalesDocuments_status DEFAULT (0),
quote_id INT NULL,
customer_id INT NOT NULL,
bill_to_customer_id INT NOT NULL,
issue_date DATETIME NULL,
currency NVARCHAR(10) NOT NULL CONSTRAINT DF_PhS_SalesDocuments_currency DEFAULT ('ARS'),
exchange_rate DECIMAL(18,6) NOT NULL CONSTRAINT DF_PhS_SalesDocuments_exchange_rate DEFAULT (1),
net_amount DECIMAL(18,2) NOT NULL CONSTRAINT DF_PhS_SalesDocuments_net_amount DEFAULT (0),
tax_amount DECIMAL(18,2) NOT NULL CONSTRAINT DF_PhS_SalesDocuments_tax_amount DEFAULT (0),
total_amount DECIMAL(18,2) NOT NULL CONSTRAINT DF_PhS_SalesDocuments_total_amount DEFAULT (0),
associated_document_type NVARCHAR(50) NULL,
associated_document_number NVARCHAR(50) NULL,
associated_document_date DATETIME NULL,
observations NVARCHAR(MAX) NULL,
extra_info_json NVARCHAR(MAX) NULL,
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesDocuments_createdat DEFAULT (GETDATE()),
modifiedat DATETIME NULL,
CONSTRAINT PK_PhS_SalesDocuments PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesDocuments_FormSeries
FOREIGN KEY (formseries_id)
REFERENCES dbo.PhS_FormSeries(id),
CONSTRAINT FK_PhS_SalesDocuments_QuoteHeaders
FOREIGN KEY (quote_id)
REFERENCES dbo.PhS_QuoteHeaders(id),
CONSTRAINT FK_PhS_SalesDocuments_Customers
FOREIGN KEY (customer_id)
REFERENCES dbo.PhS_Customers(id),
CONSTRAINT FK_PhS_SalesDocuments_BillToCustomers
FOREIGN KEY (bill_to_customer_id)
REFERENCES dbo.PhS_Customers(id)
);
-- ============================================================
-- Detalle comercial
-- ============================================================
CREATE TABLE dbo.PhS_SalesDocumentDetails
(
id INT IDENTITY(1,1) NOT NULL,
salesdocument_id INT NOT NULL,
line_number INT NOT NULL,
origin_type NVARCHAR(50) NOT NULL,
origin_id INT NULL,
quote_detail_id INT NULL,
product_id INT NULL,
description NVARCHAR(255) NOT NULL,
quantity DECIMAL(18,2) NOT NULL,
authorized_unit_price DECIMAL(18,2) NULL,
authorized_amount DECIMAL(18,2) NULL,
billed_percentage DECIMAL(9,4) NULL,
unit_price DECIMAL(18,2) NOT NULL,
net_amount DECIMAL(18,2) NOT NULL,
tax_amount DECIMAL(18,2) NOT NULL,
total_amount DECIMAL(18,2) NOT NULL,
origin_snapshot_json NVARCHAR(MAX) NULL,
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesDocumentDetails_createdat DEFAULT (GETDATE()),
modifiedat DATETIME NULL,
CONSTRAINT PK_PhS_SalesDocumentDetails PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesDocumentDetails_SalesDocuments
FOREIGN KEY (salesdocument_id)
REFERENCES dbo.PhS_SalesDocuments(id),
CONSTRAINT FK_PhS_SalesDocumentDetails_QuoteDetails
FOREIGN KEY (quote_detail_id)
REFERENCES dbo.PhS_QuoteDetails(id),
CONSTRAINT FK_PhS_SalesDocumentDetails_Products
FOREIGN KEY (product_id)
REFERENCES dbo.PhS_Products(id)
);
-- ============================================================
-- Documento fiscal ARCA / AFIP
-- ============================================================
CREATE TABLE dbo.PhS_SalesFiscalDocuments
(
id INT IDENTITY(1,1) NOT NULL,
salesdocument_id INT NOT NULL,
fiscal_status INT NOT NULL CONSTRAINT DF_PhS_SalesFiscalDocuments_fiscal_status DEFAULT (0),
environment NVARCHAR(20) NOT NULL,
point_of_sale SMALLINT NULL,
voucher_type INT NULL,
voucher_letter NVARCHAR(5) NULL,
voucher_number INT NULL,
cae NVARCHAR(20) NULL,
cae_expiration_date DATETIME NULL,
request_fingerprint NVARCHAR(255) NULL,
is_final BIT NOT NULL CONSTRAINT DF_PhS_SalesFiscalDocuments_is_final DEFAULT (0),
arca_request_payload_json NVARCHAR(MAX) NULL,
arca_response_payload_json NVARCHAR(MAX) NULL,
errors_json NVARCHAR(MAX) NULL,
events_json NVARCHAR(MAX) NULL,
observations_json NVARCHAR(MAX) NULL,
attempted_at_utc DATETIME NULL,
completed_at_utc DATETIME NULL,
reconciled_after_timeout BIT NOT NULL CONSTRAINT DF_PhS_SalesFiscalDocuments_reconciled DEFAULT (0),
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesFiscalDocuments_createdat DEFAULT (GETDATE()),
modifiedat DATETIME NULL,
CONSTRAINT PK_PhS_SalesFiscalDocuments PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesFiscalDocuments_SalesDocuments
FOREIGN KEY (salesdocument_id)
REFERENCES dbo.PhS_SalesDocuments(id)
);
-- ============================================================
-- Asociaciones fiscales reales: CbtesAsoc
-- ============================================================
CREATE TABLE dbo.PhS_SalesFiscalDocumentAssociations
(
id INT IDENTITY(1,1) NOT NULL,
salesfiscaldocument_id INT NOT NULL,
associated_salesdocument_id INT NULL,
associated_voucher_type INT NOT NULL,
associated_point_of_sale SMALLINT NOT NULL,
associated_voucher_number INT NOT NULL,
associated_voucher_cuit NVARCHAR(20) NULL,
associated_voucher_date DATETIME NULL,
createdat DATETIME NOT NULL CONSTRAINT DF_PhS_SalesFiscalDocumentAssociations_createdat DEFAULT (GETDATE()),
CONSTRAINT PK_PhS_SalesFiscalDocumentAssociations PRIMARY KEY (id),
CONSTRAINT FK_PhS_SalesFiscalDocumentAssociations_FiscalDocuments
FOREIGN KEY (salesfiscaldocument_id)
REFERENCES dbo.PhS_SalesFiscalDocuments(id),
CONSTRAINT FK_PhS_SalesFiscalDocumentAssociations_SalesDocuments
FOREIGN KEY (associated_salesdocument_id)
REFERENCES dbo.PhS_SalesDocuments(id)
);
-- ============================================================
-- Indices
-- ============================================================
CREATE INDEX IX_PhS_SalesDocuments_FormSeries
ON dbo.PhS_SalesDocuments(formseries_id);
CREATE INDEX IX_PhS_SalesDocuments_Quote
ON dbo.PhS_SalesDocuments(quote_id);
CREATE INDEX IX_PhS_SalesDocuments_Customer
ON dbo.PhS_SalesDocuments(customer_id);
CREATE INDEX IX_PhS_SalesDocuments_BillToCustomer
ON dbo.PhS_SalesDocuments(bill_to_customer_id);
CREATE INDEX IX_PhS_SalesDocuments_Status
ON dbo.PhS_SalesDocuments(status);
CREATE INDEX IX_PhS_SalesDocuments_IssueDate
ON dbo.PhS_SalesDocuments(issue_date);
CREATE UNIQUE INDEX UX_PhS_SalesDocuments_InternalNumber
ON dbo.PhS_SalesDocuments(formseries_id, internal_sequence_number)
WHERE formseries_id IS NOT NULL
AND internal_sequence_number IS NOT NULL;
CREATE UNIQUE INDEX UX_PhS_SalesDocuments_InternalDocumentNumber
ON dbo.PhS_SalesDocuments(formseries_id, internal_document_number)
WHERE formseries_id IS NOT NULL
AND internal_document_number IS NOT NULL;
CREATE INDEX IX_PhS_SalesDocumentDetails_Document
ON dbo.PhS_SalesDocumentDetails(salesdocument_id);
CREATE UNIQUE INDEX UX_PhS_SalesDocumentDetails_Line
ON dbo.PhS_SalesDocumentDetails(salesdocument_id, line_number);
CREATE INDEX IX_PhS_SalesDocumentDetails_QuoteDetail
ON dbo.PhS_SalesDocumentDetails(quote_detail_id);
CREATE INDEX IX_PhS_SalesFiscalDocuments_Document
ON dbo.PhS_SalesFiscalDocuments(salesdocument_id);
CREATE UNIQUE INDEX UX_PhS_SalesFiscalDocuments_Document
ON dbo.PhS_SalesFiscalDocuments(salesdocument_id);
CREATE UNIQUE INDEX UX_PhS_SalesFiscalDocuments_FiscalNumber
ON dbo.PhS_SalesFiscalDocuments(environment, point_of_sale, voucher_type, voucher_number)
WHERE point_of_sale IS NOT NULL
AND voucher_type IS NOT NULL
AND voucher_number IS NOT NULL;
CREATE UNIQUE INDEX UX_PhS_SalesFiscalDocuments_Idempotency
ON dbo.PhS_SalesFiscalDocuments(environment, point_of_sale, voucher_type, request_fingerprint)
WHERE point_of_sale IS NOT NULL
AND voucher_type IS NOT NULL
AND request_fingerprint IS NOT NULL;
CREATE INDEX IX_PhS_SalesFiscalDocumentAssociations_FiscalDocument
ON dbo.PhS_SalesFiscalDocumentAssociations(salesfiscaldocument_id);
CREATE INDEX IX_PhS_SalesFiscalDocumentAssociations_AssociatedDocument
ON dbo.PhS_SalesFiscalDocumentAssociations(associated_salesdocument_id);
-- ============================================================
-- Extended properties / MS_Description
-- ============================================================
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documentos comerciales internos de venta: facturas, notas de debito, notas de credito, FCE, NDE y NCE. Mantiene la emision interna separada de la autorizacion fiscal ARCA.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Identificador interno del documento comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Talonario/serie interna existente en PhronCare. Reutiliza PhS_FormSeries para numeracion interna.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'formseries_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero secuencial interno asignado al emitir internamente el documento. No corresponde al numero fiscal ARCA.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'internal_sequence_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero visible interno del documento, formado desde la serie/talonario interno. Puede diferir del numero fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'internal_document_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo comercial interno del documento. Ejemplos: Invoice, DebitNote, CreditNote, CreditInvoice, CreditDebitNote, CreditCreditNote.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'document_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo de comprobante fiscal AFIP/ARCA previsto para autorizacion futura. Ejemplos: 1, 6, 11, 201, 202, 203.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'fiscal_voucher_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Letra fiscal prevista del comprobante: A, B, C u otras segun configuracion fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'fiscal_voucher_letter';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Estado comercial interno. Ejemplos: Draft, Validated, Issued, Cancelled. Independiente del estado fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'status';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Presupuesto origen opcional. Puede ser NULL para ventas manuales o de escritorio.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'quote_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Cliente origen de la operacion comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'customer_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Cliente al que se factura realmente. Permite escenarios obra social / particular u otros terceros pagadores.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'bill_to_customer_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha de emision interna del documento comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'issue_date';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Moneda del documento comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'currency';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Cotizacion utilizada para la moneda del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'exchange_rate';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe neto total del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'net_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe total de impuestos del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'tax_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe total del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'total_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo de documento interno asociado opcional, por ejemplo remito, orden de compra o autorizacion. No representa CbtesAsoc fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'associated_document_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero del documento interno asociado opcional.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'associated_document_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha del documento interno asociado opcional.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'associated_document_date';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Observaciones comerciales del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'observations';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Snapshot JSON con informacion extra contextual del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocuments',
@level2type=N'COLUMN', @level2name=N'extra_info_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalles valorizados de documentos comerciales de venta.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento comercial al que pertenece el detalle.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'salesdocument_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero de linea dentro del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'line_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Origen logico del item: Manual, QuoteDetail, Adjustment u otro valor definido por Domain/Core.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'origin_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Identificador generico del origen cuando aplique.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'origin_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Detalle del presupuesto aprobado que origina la linea, cuando exista. Puede ser NULL en ventas manuales.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'quote_detail_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Producto asociado a la linea, si aplica.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'product_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Descripcion visible de la linea facturada.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'description';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Cantidad facturada.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'quantity';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Precio unitario autorizado o de referencia proveniente del origen comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'authorized_unit_price';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe autorizado o de referencia proveniente del origen comercial.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'authorized_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Porcentaje facturado sobre el origen. Permite facturacion parcial obra social / particular.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'billed_percentage';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Precio unitario efectivo de la linea del documento.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'unit_price';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe neto de la linea.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'net_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe de impuestos de la linea.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'tax_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Importe total de la linea.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'total_amount';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Snapshot JSON del origen de la linea para trazabilidad historica.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesDocumentDetails',
@level2type=N'COLUMN', @level2name=N'origin_snapshot_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento fiscal asociado a un documento comercial de venta. Guarda estado, CAE, numeracion fiscal, request/response e idempotencia ARCA.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento comercial interno vinculado al documento fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'salesdocument_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Estado fiscal independiente del estado comercial. Ejemplos: None, Pending, Authorized, Rejected, Error, PendingReconciliation.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'fiscal_status';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Ambiente fiscal usado para autorizacion: homologacion, produccion u otro valor definido por configuracion.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'environment';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Punto de venta fiscal ARCA/AFIP.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'point_of_sale';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo de comprobante fiscal ARCA/AFIP utilizado en FECAESolicitar.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'voucher_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Letra fiscal del comprobante autorizado o a autorizar.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'voucher_letter';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero fiscal del comprobante asignado para ARCA. Se mantiene separado del numero interno.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'voucher_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Codigo de autorizacion electronico obtenido desde ARCA/AFIP.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'cae';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha de vencimiento del CAE.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'cae_expiration_date';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Huella de idempotencia fiscal para evitar duplicacion de solicitudes ante ARCA.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'request_fingerprint';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Indica si el resultado fiscal es final y no debe volver a mutar salvo procesos controlados de auditoria.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'is_final';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Payload JSON enviado a ARCA/AFIP.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'arca_request_payload_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Payload JSON recibido desde ARCA/AFIP.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'arca_response_payload_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Errores devueltos por ARCA/AFIP serializados como JSON.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'errors_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Eventos devueltos por ARCA/AFIP serializados como JSON.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'events_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Observaciones devueltas por ARCA/AFIP serializadas como JSON.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'observations_json';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha/hora UTC del intento de autorizacion fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'attempted_at_utc';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha/hora UTC de finalizacion del flujo fiscal.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'completed_at_utc';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Indica que el documento fiscal fue resuelto mediante reconciliacion posterior a timeout o resultado ambiguo.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocuments',
@level2type=N'COLUMN', @level2name=N'reconciled_after_timeout';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Comprobantes fiscales asociados enviados como CbtesAsoc. Aplica a notas de credito, notas de debito, NCE/NDE y casos FCE donde corresponda.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento fiscal que contiene esta asociacion.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'salesfiscaldocument_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Documento comercial interno asociado, si existe dentro de PhronCare.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_salesdocument_id';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Tipo fiscal ARCA/AFIP del comprobante asociado.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_voucher_type';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Punto de venta fiscal del comprobante asociado.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_point_of_sale';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Numero fiscal del comprobante asociado.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_voucher_number';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'CUIT emisor del comprobante asociado, cuando sea requerido por ARCA.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_voucher_cuit';
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'Fecha del comprobante fiscal asociado.',
@level0type=N'SCHEMA', @level0name=N'dbo',
@level1type=N'TABLE', @level1name=N'PhS_SalesFiscalDocumentAssociations',
@level2type=N'COLUMN', @level2name=N'associated_voucher_date';

View File

@ -0,0 +1,180 @@
using Core.Interfaces;
using Documents.Interfaces;
using Documents.Models;
using Domain.Dtos.Sales;
using Domain.Generics;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
namespace phronCare.API.Controllers.Sales
{
[Route("api/[controller]")]
[ApiController]
public class DeliveryNoteController : ControllerBase
{
private readonly IDocumentTemplateService _documentTemplateService;
private readonly IDeliveryNoteDom _deliveryNoteService;
public DeliveryNoteController(
IDocumentTemplateService documentTemplateService,
IDeliveryNoteDom deliveryNoteService)
{
_documentTemplateService = documentTemplateService ?? throw new ArgumentNullException(nameof(documentTemplateService));
_deliveryNoteService = deliveryNoteService ?? throw new ArgumentNullException(nameof(deliveryNoteService));
}
[HttpGet("search")]
public async Task<ActionResult<PagedResult<DeliveryNoteSummaryDto>>> Search(
[FromQuery] int? customerId,
[FromQuery] string? customerText,
[FromQuery] string? deliveryNoteNumber,
[FromQuery] int? quoteId,
[FromQuery] string? quoteNumber,
[FromQuery] DateTime? issueDateFrom,
[FromQuery] DateTime? issueDateTo,
[FromQuery] string? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50)
{
try
{
var result = await _deliveryNoteService.SearchAsync(
customerId,
customerText,
deliveryNoteNumber,
quoteId,
quoteNumber,
issueDateFrom,
issueDateTo,
status,
page,
pageSize);
return Ok(result);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<DeliveryNoteDto>> GetById(int id)
{
try
{
var deliveryNote = await _deliveryNoteService.GetDtoByIdAsync(id);
if (deliveryNote == null)
return NotFound($"Remito con ID {id} no encontrado.");
return Ok(deliveryNote);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("number/{deliveryNoteNumber}")]
public async Task<ActionResult<DeliveryNoteDto>> GetByDeliveryNoteNumber(string deliveryNoteNumber)
{
try
{
var deliveryNote = await _deliveryNoteService.GetDtoByDeliveryNoteNumberAsync(deliveryNoteNumber);
if (deliveryNote == null)
return NotFound($"Remito con número {deliveryNoteNumber} no encontrado.");
return Ok(deliveryNote);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("{id:int}/pdf")]
public async Task<IActionResult> GetDeliveryNotePdf(int id)
{
var deliveryNote = await _deliveryNoteService.GetDtoByIdAsync(id);
if (deliveryNote == null)
return NotFound($"Remito con ID {id} no encontrado.");
var pdfBytes = await _documentTemplateService.GenerateDocumentAsync(new DocumentGenerationRequest
{
Model = deliveryNote,
DocumentType = DocumentType.DeliveryNote
});
return File(pdfBytes, "application/pdf", $"Remito_{deliveryNote.DeliveryNoteNumber}.pdf");
}
[HttpGet("by-quote/{quoteId:int}")]
public async Task<ActionResult<IEnumerable<DeliveryNoteDto>>> GetByQuoteId(int quoteId)
{
try
{
var deliveryNotes = await _deliveryNoteService.GetDtosByQuoteIdAsync(quoteId);
return Ok(deliveryNotes);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("issue")]
public async Task<ActionResult<DeliveryNoteCreateResponse>> Issue([FromBody] DeliveryNoteCreateRequest request)
{
try
{
var result = await _deliveryNoteService.CreateAndIssueDeliveryNoteAsync(request);
return Ok(result);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("exportfiltered")]
public async Task<IActionResult> ExportFiltered([FromBody] DeliveryNoteSearchParams searchParams)
{
try
{
var file = await _deliveryNoteService.ExportFilteredToExcelAsync(searchParams);
return File(
file,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Remitos.xlsx");
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return BadRequest($"{methodName} Message: {ex.Message}");
}
}
}
}

View File

@ -144,6 +144,32 @@ namespace phronCare.API.Controllers.Stock
return BadRequest(ex.Message); return BadRequest(ex.Message);
} }
} }
[HttpPost("{id:int}/mark-in-transit")]
public async Task<IActionResult> MarkInTransit(int id)
{
try
{
await _expeditionService.MarkInTransitAsync(id);
return Ok(new { Success = true, Message = "La expedición pasó a estado 'En tránsito' correctamente." });
}
catch (KeyNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
return StatusCode(500, $"Ocurrió un error interno: {ex.Message}");
}
}
} }
public class CreateFullExpeditionRequest public class CreateFullExpeditionRequest
{ {

View File

@ -238,6 +238,9 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
builder.Services.AddScoped<IAdjustmentReasonDom, AdjustmentReasonService>(); builder.Services.AddScoped<IAdjustmentReasonDom, AdjustmentReasonService>();
builder.Services.AddScoped<IPhSAdjustmentReasonRepository, PhSAdjustmentReasonRepository>(); builder.Services.AddScoped<IPhSAdjustmentReasonRepository, PhSAdjustmentReasonRepository>();
builder.Services.AddScoped<IDeliveryNoteDom, DeliveryNoteService>();
builder.Services.AddScoped<IPhSDeliveryNoteRepository, PhSDeliveryNoteRepository>();
builder.Services.AddScoped<ITaxTypeDom, TaxTypeService>(); builder.Services.AddScoped<ITaxTypeDom, TaxTypeService>();
builder.Services.AddScoped<IPhOhArcataxTypeRepository, PhOhArcataxTypeRepository>(); builder.Services.AddScoped<IPhOhArcataxTypeRepository, PhOhArcataxTypeRepository>();

View File

@ -37,6 +37,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.Bcl.Memory" Version="9.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -1,142 +0,0 @@
using Transversal.Services;
using Transversal.Models;
namespace phronCare.Test
{
[TestFixture]
public class PdfGeneratorServiceTests
{
private PuppeteerPdfGeneratorService _pdfService;
[OneTimeSetUp]
public void Setup()
{
// Instancia real del servicio (recomendado para test manual/local)
_pdfService = new PuppeteerPdfGeneratorService();
}
[OneTimeTearDown]
public async Task TearDown()
{
// Liberar Chromium al finalizar los tests
if (_pdfService != null)
await _pdfService.DisposeAsync();
}
[Test]
public async Task GeneratePdfFromHtml_ShouldCreatePdfFile()
{
// Arrange
string html = @"
<html>
<head>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; margin: 20px; }
.header { text-align: center; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; margin-bottom: 20px; }
.header h1 { color: #4CAF50; }
.info { margin-bottom: 20px; }
.info table { width: 100%; }
.info td { padding: 5px; }
.details table { width: 100%; border-collapse: collapse; }
.details th, .details td { border: 1px solid #dddddd; text-align: center; padding: 8px; }
.details th { background-color: #4CAF50; color: white; }
.totals { margin-top: 20px; float: right; width: 300px; }
.totals table { width: 100%; border-collapse: collapse; }
.totals th, .totals td { border: 1px solid #dddddd; text-align: right; padding: 8px; }
.totals th { background-color: #4CAF50; color: white; }
.footer { position: fixed; bottom: 20px; left: 0; right: 0; text-align: center; font-size: 10px; color: gray; }
</style>
</head>
<body>
<div class='header'>
<h1>PhronCare Ortopedia</h1>
<p>Presupuesto Médico</p>
</div>
<div class='info'>
<table>
<tr>
<td><strong>Cliente:</strong> Juan Pérez</td>
<td><strong>Presupuesto N°:</strong> 000123</td>
</tr>
<tr>
<td><strong>Fecha:</strong> 13/05/2025</td>
<td><strong>Profesional:</strong> Dr. Carlos López</td>
</tr>
</table>
</div>
<div class='details'>
<table>
<tr>
<th>Cantidad</th>
<th>Producto</th>
<th>Precio Unitario</th>
<th>Subtotal</th>
</tr>
<tr>
<td>2</td>
<td>Rodillera ortopédica</td>
<td>$5.000</td>
<td>$10.000</td>
</tr>
<tr>
<td>1</td>
<td>Férula de inmovilización</td>
<td>$8.000</td>
<td>$8.000</td>
</tr>
</table>
</div>
<div class='totals'>
<table>
<tr>
<th>Subtotal</th>
<td>$18.000</td>
</tr>
<tr>
<th>IVA (21%)</th>
<td>$3.780</td>
</tr>
<tr>
<th>Total</th>
<td><strong>$21.780</strong></td>
</tr>
</table>
</div>
<div class='footer'>
Presupuesto generado automáticamente por PhronCare - No válido como factura.
</div>
</body>
</html>";
string outputFolder = @"C:\temp";
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
string outputPath = Path.Combine(outputFolder, "DemoTest_Puppeteer.pdf");
// Opcional: podés probar pasando o no opciones
var options = new PdfGenerationOptions
{
Format = PuppeteerSharp.Media.PaperFormat.A4,
Landscape = false,
PrintBackground = true,
Scale = 1.0m,
HeaderTemplate = "<div style='font-size:10px; text-align:center;'>Presupuesto</div>",
FooterTemplate = "<div style='font-size:10px; text-align:center;'>Página <span class='pageNumber'></span> de <span class='totalPages'></span></div>"
};
// Act
byte[] pdfBytes = await _pdfService.GeneratePdfFromHtmlAsync(html, options);
await File.WriteAllBytesAsync(outputPath, pdfBytes);
// Assert
Assert.IsTrue(File.Exists(outputPath));
Assert.IsTrue(new FileInfo(outputPath).Length > 0);
TestContext.WriteLine($"PDF generado correctamente en: {outputPath}");
}
}
}

View File

@ -129,6 +129,11 @@
<i class="bi bi-receipt me-2 text-white"></i> Presupuesto <i class="bi bi-receipt me-2 text-white"></i> Presupuesto
</NavLink> </NavLink>
</div> </div>
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="deliverynotes" activeClass="bg-secondary text-white fw-semibold">
<i class="bi bi-truck me-2 text-warning"></i> Remitos
</NavLink>
</div>
<div class="nav-item ps-4 py-0 border-start border-2 border-white"> <div class="nav-item ps-4 py-0 border-start border-2 border-white">
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="sales/institutions/" activeClass="bg-secondary text-white fw-semibold"> <NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="sales/institutions/" activeClass="bg-secondary text-white fw-semibold">
<i class="bi bi-building me-2 text-info"></i> Instituciones <i class="bi bi-building me-2 text-info"></i> Instituciones

View File

@ -0,0 +1,424 @@
@page "/deliverynotes/create"
@using System.ComponentModel.DataAnnotations
@using System.Text.Json
@using Blazored.Typeahead
@using Domain.Constants
@using Domain.Dtos
@using Domain.Dtos.Sales
@using phronCare.UIBlazor.Services.Lookups
@using phronCare.UIBlazor.Services.Sales.DeliveryNotes
@using phronCare.UIBlazor.Services.Sales.Quotes
@using phronCare.UIBlazor.Shared.Modals
@inject NavigationManager Navigation
@inject IDeliveryNoteService DeliveryNoteService
@inject ISalesLookupService SalesLookupService
@inject IQuoteService QuoteService
@inject IToastService toastService
@inject IModalService Modal
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="container mt-4" style="zoom:.8;">
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="mb-0">Emisión de Remito</h3>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-2">
<label for="deliveryNoteNumber" class="form-label">Número de remito</label>
<InputText id="deliveryNoteNumber" class="form-control" @bind-Value="Model.DeliveryNoteNumber" />
<ValidationMessage For="@(() => Model.DeliveryNoteNumber)" />
</div>
<div class="col-md-2">
<label for="issueDate" class="form-label">Fecha de emisión</label>
<InputDate id="issueDate" class="form-control" @bind-Value="Model.IssueDate" />
<ValidationMessage For="@(() => Model.IssueDate)" />
</div>
<div class="col-md-4">
<label for="quoteLookup" class="form-label">Presupuesto aprobado (opcional)</label>
<BlazoredTypeahead id="quoteLookup" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchApprovedQuotesAsync"
Value="SelectedQuote"
ValueChanged="OnQuoteSelected"
ValueExpression="@(() => SelectedQuote)"
MaximumSuggestions="10"
Placeholder="Buscar presupuesto aprobado..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
</div>
<div class="col-md-4">
<label for="customerLookup" class="form-label">Cliente</label>
<BlazoredTypeahead id="customerLookup" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchCustomersAsync"
Value="SelectedCustomer"
ValueChanged="OnCustomerSelected"
ValueExpression="@(() => SelectedCustomer)"
MaximumSuggestions="10"
Placeholder="Buscar cliente..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<ValidationMessage For="@(() => Model.CustomerId)" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="observations" class="form-label">Observaciones</label>
<InputTextArea id="observations" class="form-control" rows="3" @bind-Value="Model.Observations" />
</div>
<div class="col-md-6">
@if (SelectedQuote is not null)
{
<label for="observations" class="form-label">Vinculado</label>
<div class="alert alert-dark border mb-3">
<strong rows="3">Presupuesto vinculado:</strong> @SelectedQuote.Nombre
</div>
}
</div>
</div>
@if (SelectedQuote is not null)
{
<div class="card border-1 bg-light-subtle">
<div class="card-body py-3">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Profesional</label>
<div class="form-control bg-white">@(string.IsNullOrWhiteSpace(ExtraInfo.Professional) ? "No informado" : ExtraInfo.Professional)</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Institución</label>
<div class="form-control bg-white">@(string.IsNullOrWhiteSpace(ExtraInfo.Institution) ? "No informada" : ExtraInfo.Institution)</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Paciente</label>
<div class="form-control bg-white">@(string.IsNullOrWhiteSpace(ExtraInfo.Patient) ? "No informado" : ExtraInfo.Patient)</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Fecha estimada de cirugía</label>
<div class="form-control bg-white">@(ExtraInfo.SurgeryDate.HasValue ? ExtraInfo.SurgeryDate.Value.ToString("dd/MM/yyyy") : "No informada")</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Ítems del remito</h5>
<button type="button" class="btn btn-outline-success btn-sm rounded-pill" @onclick="AddItem">
<i class="fas fa-plus me-1"></i> Agregar ítem
</button>
</div>
<div class="card-body p-2">
@if (Items.Any())
{
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0 deliverynote-items-table">
<thead class="table-light">
<tr>
<th style="width: 60px;" class="text-center">#</th>
<th>Descripción</th>
<th style="width: 120px;" class="text-center">Cantidad</th>
<th style="width: 160px;" class="text-center">Origen</th>
<th>Notas</th>
<th style="width: 60px;"></th>
</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
<tr>
<td class="text-center line-number-cell">@item.LineNumber</td>
<td>
<InputTextArea class="form-control form-control-sm item-description" rows="3" @bind-Value="item.Description" />
</td>
<td>
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.Quantity" />
</td>
<td>
<InputSelect class="form-select form-select-sm" @bind-Value="item.OriginType">
<option value="@((byte)DeliveryNoteItemOriginType.Manual)">Manual</option>
<option value="@((byte)DeliveryNoteItemOriginType.QuoteDetail)">Presupuesto</option>
<option value="@((byte)DeliveryNoteItemOriginType.SalesProduct)">Producto venta</option>
<option value="@((byte)DeliveryNoteItemOriginType.StockProduct)">Producto stock</option>
</InputSelect>
</td>
<td>
<InputTextArea class="form-control form-control-sm item-notes" rows="3" @bind-Value="item.Notes" />
</td>
<td class="text-center actions-cell">
<button type="button" class="btn btn-link p-0 text-danger" title="Eliminar" @onclick="() => RemoveItem(item)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="text-center text-muted py-4">
No hay ítems cargados.
</div>
}
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary rounded-pill" @onclick="BackToList" disabled="@IsSaving">
<i class="fas fa-arrow-left me-1"></i> Volver
</button>
<button type="submit" class="btn btn-primary rounded-pill" disabled="@IsSaving">
@if (IsSaving)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
<i class="fas fa-save me-1"></i> Emitir remito
</button>
</div>
</div>
</EditForm>
@code {
private DeliveryNoteCreatePageModel Model = new()
{
IssueDate = DateTime.Today
};
private ELookUpItem? SelectedCustomer;
private ELookUpItem? SelectedQuote;
private DeliveryNoteExtraInfoModel ExtraInfo = new();
private List<DeliveryNoteItemRow> Items = new();
private bool IsSaving;
private void AddItem()
{
Items.Add(new DeliveryNoteItemRow
{
LineNumber = Items.Count + 1,
OriginType = (byte)DeliveryNoteItemOriginType.Manual,
Quantity = 1
});
}
private void RemoveItem(DeliveryNoteItemRow item)
{
if (Items.Remove(item))
{
ReindexItems();
}
}
private void ReindexItems()
{
for (var i = 0; i < Items.Count; i++)
{
Items[i].LineNumber = i + 1;
}
}
private Task OnCustomerSelected(ELookUpItem? customer)
{
SelectedCustomer = customer;
Model.CustomerId = customer?.Id;
return Task.CompletedTask;
}
private async Task OnQuoteSelected(ELookUpItem? quote)
{
SelectedQuote = quote;
Model.QuoteId = quote?.Id;
if (quote is null)
{
ExtraInfo = new();
Model.ExtraInfoJson = null;
return;
}
var quoteDto = await QuoteService.GetDtoByIdAsync(quote.Id);
if (quoteDto is null)
{
ExtraInfo = new();
Model.ExtraInfoJson = null;
toastService.ShowError("No se pudo cargar el presupuesto seleccionado.");
return;
}
ExtraInfo = BuildExtraInfoModel(quoteDto);
var mappedItems = BuildItemsFromApprovedQuote(quoteDto);
if (mappedItems.Count == 0)
{
Model.ExtraInfoJson = JsonSerializer.Serialize(ExtraInfo);
toastService.ShowWarning("El presupuesto seleccionado no tiene ítems aprobados para precargar.");
return;
}
if (Items.Any())
{
var parameters = new ModalParameters();
parameters.Add(nameof(ConfirmModal.Title), "Reemplazar ítems");
parameters.Add(nameof(ConfirmModal.Message), "Ya hay ítems cargados. ¿Desea reemplazarlos por los ítems aprobados del presupuesto?");
var modal = Modal.Show<ConfirmModal>("Confirmación", parameters);
var result = await modal.Result;
if (result.Cancelled)
return;
}
Items = mappedItems;
Model.ExtraInfoJson = JsonSerializer.Serialize(ExtraInfo);
ReindexItems();
StateHasChanged();
}
private static DeliveryNoteExtraInfoModel BuildExtraInfoModel(QuoteDto quote)
{
return new DeliveryNoteExtraInfoModel
{
Professional = quote.ProfessionalName,
Institution = quote.InstitutionName,
Patient = quote.PatientName,
SurgeryDate = quote.EstimatedDate
};
}
private List<DeliveryNoteItemRow> BuildItemsFromApprovedQuote(QuoteDto quote)
{
return quote.Items
.Where(item => item.Approved)
.Select(item => new
{
Item = item,
Quantity = item.ApprovedQuantity.HasValue && item.ApprovedQuantity.Value > 0
? item.ApprovedQuantity.Value
: item.Quantity
})
.Where(x => x.Quantity > 0)
.Select((x, index) => new DeliveryNoteItemRow
{
LineNumber = index + 1,
OriginType = (byte)DeliveryNoteItemOriginType.QuoteDetail,
QuoteDetailId = x.Item.Id,
Description = x.Item.Description,
Quantity = x.Quantity
})
.ToList();
}
private string? ValidateBeforeSave()
{
if (Items.Count == 0)
return "Debe incluir al menos un ítem.";
if (Items.Any(x => string.IsNullOrWhiteSpace(x.Description)))
return "Todos los ítems deben tener descripción.";
if (Items.Any(x => x.Quantity <= 0))
return "Todos los ítems deben tener cantidad mayor a cero.";
return null;
}
private async Task HandleValidSubmit()
{
var validationError = ValidateBeforeSave();
if (!string.IsNullOrWhiteSpace(validationError))
{
toastService.ShowError(validationError);
return;
}
try
{
IsSaving = true;
var request = new DeliveryNoteCreateRequest
{
DeliveryNoteNumber = Model.DeliveryNoteNumber.Trim(),
IssueDate = Model.IssueDate!.Value,
CustomerId = Model.CustomerId!.Value,
QuoteId = Model.QuoteId,
Observations = Model.Observations,
ExtraInfoJson = Model.ExtraInfoJson,
Items = Items.Select(x => new DeliveryNoteCreateItemRequest
{
OriginType = x.OriginType,
OriginId = x.OriginId,
QuoteDetailId = x.QuoteDetailId,
Description = x.Description.Trim(),
Quantity = x.Quantity,
Notes = string.IsNullOrWhiteSpace(x.Notes) ? null : x.Notes.Trim()
}).ToList()
};
var response = await DeliveryNoteService.CreateAndIssueAsync(request);
toastService.ShowSuccess($"Remito {response.DeliveryNoteNumber} emitido correctamente.");
await DeliveryNoteService.ExportPdfAsync(response.Id, response.DeliveryNoteNumber);
Navigation.NavigateTo("/deliverynotes");
}
catch (Exception ex)
{
toastService.ShowError(ex.Message);
}
finally
{
IsSaving = false;
}
}
private void BackToList()
{
Navigation.NavigateTo("/deliverynotes");
}
private sealed class DeliveryNoteCreatePageModel
{
[Required(ErrorMessage = "El número de remito es obligatorio.")]
public string DeliveryNoteNumber { get; set; } = string.Empty;
[Required(ErrorMessage = "La fecha de emisión es obligatoria.")]
public DateTime? IssueDate { get; set; }
[Required(ErrorMessage = "El cliente es obligatorio.")]
public int? CustomerId { get; set; }
public int? QuoteId { get; set; }
public string? Observations { get; set; }
public string? ExtraInfoJson { get; set; }
}
private sealed class DeliveryNoteExtraInfoModel
{
public string? Professional { get; set; }
public string? Institution { get; set; }
public string? Patient { get; set; }
public DateTime? SurgeryDate { get; set; }
}
private sealed class DeliveryNoteItemRow
{
public int LineNumber { get; set; }
public byte OriginType { get; set; }
public int? OriginId { get; set; }
public int? QuoteDetailId { get; set; }
public string Description { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public string? Notes { get; set; }
}
}

View File

@ -0,0 +1,33 @@
.deliverynote-items-table td {
vertical-align: top;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.deliverynote-items-table .line-number-cell,
.deliverynote-items-table .actions-cell {
vertical-align: middle;
}
.deliverynote-items-table .form-control,
.deliverynote-items-table .form-select {
min-height: 38px;
}
.deliverynote-items-table .item-description,
.deliverynote-items-table .item-notes {
min-height: calc(1.5em * 3 + 1rem + 2px);
resize: vertical;
}
.deliverynote-items-table textarea.form-control {
line-height: 1.35;
}
.deliverynote-items-table .text-end {
min-width: 90px;
}
.deliverynote-items-table .actions-cell .btn {
margin-top: 0.35rem;
}

View File

@ -0,0 +1,172 @@
@using Domain.Dtos.Sales
@if (Visible && Summary != null)
{
<div class="position-fixed top-0 start-0 w-100 h-100"
style="background: rgba(0,0,0,0.4); z-index: 1040;"
@onclick="Close">
</div>
<div class="position-fixed top-0 end-0 h-100 bg-white shadow"
style="width: 45%; z-index: 1050; overflow-y: auto;">
<div class="d-flex justify-content-between align-items-center border-bottom px-3 py-2">
<div>
<h5 class="m-0">Detalle Remito @HeaderNumber</h5>
<div class="mt-1">
<span class="badge @GetStatusBadge(HeaderStatus)">@HeaderStatus</span>
</div>
</div>
<button type="button" class="btn-close" @onclick="Close"></button>
</div>
<div class="p-3" style="zoom: 0.8;">
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item">
<button type="button"
class="nav-link @((activeTab == "Datos") ? "active bg-primary text-white" : "")"
@onclick='() => activeTab = "Datos"'
title="Datos generales">
<i class="fas fa-info-circle me-1"></i> Datos
</button>
</li>
<li class="nav-item">
<button type="button"
class="nav-link @((activeTab == "Items") ? "active bg-success text-white" : "")"
@onclick='() => activeTab = "Items"'
title="Detalle de líneas">
<i class="fas fa-boxes-stacked me-1"></i> Items
</button>
</li>
</ul>
@if (Loading)
{
<div class="text-center text-muted py-4">Cargando detalle...</div>
}
else if (activeTab == "Datos")
{
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Remito</label>
<div class="form-control form-control-sm bg-light">@HeaderNumber</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Fecha de emisión</label>
<div class="form-control form-control-sm bg-light">@HeaderIssueDate.ToString("dd/MM/yyyy")</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Cliente</label>
<div class="form-control form-control-sm bg-light">@HeaderCustomerName</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Presupuesto relacionado</label>
<div class="form-control form-control-sm bg-light">@HeaderQuoteNumber</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Estado</label>
<div><span class="badge @GetStatusBadge(HeaderStatus)">@HeaderStatus</span></div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Reimpresiones</label>
<div class="form-control form-control-sm bg-light">@HeaderPrintCount</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Factura asociada</label>
<div class="form-control form-control-sm bg-light">@(Detail?.SalesInvoiceId?.ToString() ?? "—")</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold mb-1">Última modificación</label>
<div class="form-control form-control-sm bg-light">@(Detail?.ModifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "—")</div>
</div>
<div class="col-12">
<label class="form-label fw-semibold mb-1">Observaciones</label>
<div class="form-control form-control-sm bg-light" style="min-height: 90px; white-space: pre-wrap;">@(string.IsNullOrWhiteSpace(Detail?.Observations) ? "—" : Detail!.Observations)</div>
</div>
</div>
}
else
{
@if (Detail?.Items == null || !Detail.Items.Any())
{
<p class="text-muted mb-0">No hay ítems para este remito.</p>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 80px;">Línea</th>
<th>Descripción</th>
<th style="width: 100px;">Cantidad</th>
<th style="width: 120px;">Origen</th>
<th>Notas</th>
</tr>
</thead>
<tbody>
@foreach (var item in Detail.Items.OrderBy(i => i.LineNumber))
{
<tr>
<td class="text-center">@item.LineNumber</td>
<td>@item.Description</td>
<td class="text-center">@item.Quantity</td>
<td>@GetOriginLabel(item.OriginType)</td>
<td>@(string.IsNullOrWhiteSpace(item.Notes) ? "—" : item.Notes)</td>
</tr>
}
</tbody>
</table>
</div>
}
}
</div>
</div>
}
@code {
[Parameter] public bool Visible { get; set; }
[Parameter] public EventCallback<bool> VisibleChanged { get; set; }
[Parameter] public DeliveryNoteSummaryDto? Summary { get; set; }
[Parameter] public DeliveryNoteDto? Detail { get; set; }
[Parameter] public bool Loading { get; set; }
private string activeTab = "Datos";
private int? lastDeliveryNoteId;
private string HeaderNumber => Detail?.DeliveryNoteNumber ?? Summary?.DeliveryNoteNumber ?? "—";
private string HeaderStatus => Detail?.Status ?? Summary?.Status ?? "—";
private DateTime HeaderIssueDate => Detail?.IssueDate ?? Summary?.IssueDate ?? DateTime.MinValue;
private string HeaderCustomerName => string.IsNullOrWhiteSpace(Summary?.CustomerName) ? "—" : Summary!.CustomerName;
private string HeaderQuoteNumber => string.IsNullOrWhiteSpace(Summary?.QuoteNumber) ? "—" : Summary!.QuoteNumber!;
private int HeaderPrintCount => Detail?.PrintCount ?? Summary?.PrintCount ?? 0;
protected override void OnParametersSet()
{
if (Summary?.Id != lastDeliveryNoteId)
{
activeTab = "Datos";
lastDeliveryNoteId = Summary?.Id;
}
}
private async Task Close()
{
await VisibleChanged.InvokeAsync(false);
}
private string GetStatusBadge(string status) => status switch
{
"Anulado" => "bg-danger text-white",
"Emitido" => "bg-primary text-white",
"Aprobado" => "bg-success",
"Cerrado" => "bg-dark text-white",
_ => "bg-light text-dark"
};
private string GetOriginLabel(byte originType) => originType switch
{
1 => "Presupuesto",
2 => "Manual",
_ => originType.ToString()
};
}

Some files were not shown because too many files have changed in this diff Show More