brynq-sdk-meta4 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ from pydantic import BaseModel, Field, field_validator, model_validator
2
+ from typing import Optional, Literal
3
+ from datetime import datetime
4
+ from .reference_enums import (
5
+ MovementType,
6
+ DocumentType,
7
+ GenderType,
8
+ MaritalStatusType,
9
+ StreetType,
10
+ HoursType,
11
+ PartTimeType,
12
+ SSNumberType,
13
+ YesNoType,
14
+ SalaryType,
15
+ AdjustmentType,
16
+ PaymentType,
17
+ IRPFType,
18
+ IRPFStatusType,
19
+ DisabilityHRType,
20
+ ManagementType,
21
+ PerceptionKeyType,
22
+ ReductionReasonType,
23
+ TerminationReasonType,
24
+ UnemploymentCauseType,
25
+ ContractType,
26
+ LaborRelationshipType,
27
+ SubstitutionCauseType
28
+ )
29
+
30
+
31
+ class EmployeeSchema(BaseModel):
32
+ """
33
+ Pydantic schema for Meta4 Employee data.
34
+ Based on EQU_Interfaz_Empleados v.0 specification.
35
+ """
36
+ # Movement and Dates
37
+ movement_type: MovementType = Field(description="Tipo de movimiento", example=MovementType.ALTA, alias="TIPO DE MOVIMIENTO")
38
+ effective_date: datetime = Field(description="Fecha efectividad", example="2024-01-01", alias="FECHA EFECTIVIDAD")
39
+ termination_date: Optional[datetime] = Field(None, description="Fecha de baja", example="2024-12-31", alias="FECHA DE BAJA")
40
+ termination_reason: Optional[TerminationReasonType] = Field(None, description="Motivo de baja", example=TerminationReasonType.MOTIVOS_ECONOMICOS, alias="MOTIVO DE BAJA")
41
+ unemployment_cause: Optional[UnemploymentCauseType] = Field(None, description="Causa desempleo", example=UnemploymentCauseType.WORKER_DISMISSAL, alias="CAUSA DESEMPLEO")
42
+ vacation_end_date: Optional[datetime] = Field(None, description="Fecha fin vacaciones", example="2024-08-31", alias="FECHA FIN VACACIONES")
43
+
44
+ # Personal Information
45
+ person_id: Optional[str] = Field(None, description="ID Persona", example="EMP001", alias="ID PERSONA")
46
+ first_surname: Optional[str] = Field(None, description="Apellido primero", example="García", alias="APELLIDO PRIMERO")
47
+ second_surname: Optional[str] = Field(None, description="Apellido segundo", example="López", alias="APELLIDO SEGUNDO")
48
+ employee_name: Optional[str] = Field(None, description="Nombre empleado", example="Juan Carlos", alias="NOMBRE EMPLEADO")
49
+ document_type: Optional[DocumentType] = Field(None, description="Tipo documento", example=DocumentType.DNI, alias="TIPO DOCUMENTO")
50
+ document_number: Optional[str] = Field(None, description="Número documento", example="12345678A", alias="NÚM. DOCUMENTO")
51
+ document_country: Optional[str] = Field(None, description="País emisor documento", example="724", alias="PAÍS EMISOR DOCUMENTO")
52
+ birth_date: Optional[datetime] = Field(None, description="Fecha de nacimiento", example="1985-03-15", alias="FECHA DE NACIMIENTO")
53
+ nationality: Optional[str] = Field(None, description="Nacionalidad", example="724", alias="NACIONALIDAD")
54
+ birth_country: Optional[str] = Field(None, description="País nacimiento", example="724", alias="PAÍS NACIMIENTO")
55
+ birth_province: Optional[str] = Field(None, description="Provincia nacimiento", example="", alias="PROVINCIA NACIMIENTO")
56
+ birth_community: Optional[str] = Field(None, description="Comunidad nacimiento", example="", alias="COMUNIDAD NACIMIENTO")
57
+ gender: Optional[GenderType] = Field(None, description="Sexo", example=GenderType.MALE, alias="SEXO")
58
+ marital_status: Optional[MaritalStatusType] = Field(None, description="Estado civil", example=MaritalStatusType.SINGLE, alias="ESTADO CIVIL")
59
+ phone: Optional[str] = Field(None, description="Teléfono", example="912345678", alias="TELÉFONO")
60
+ mobile_phone: Optional[str] = Field(None, description="Teléfono móvil", example="612345678", alias="TELEF. MÓVIL")
61
+ email: Optional[str] = Field(None, description="Correo electrónico", example="juan.garcia@empresa.com", alias="CORREO ELECTRÓNICO")
62
+
63
+ # Address Information
64
+ location_type: Optional[Literal["1"]] = Field(None, description="Tipo localización", example="1", alias="TIPO LOCALIZACIÓN")
65
+ street_type: Optional[StreetType] = Field(None, description="Tipo vía", example=StreetType.CALLE, alias="TIPO VÍA")
66
+ send_mail: Optional[Literal["0"]] = Field(None, description="Enviar correo", example="0", alias="ENVIAR CORREO")
67
+ employee_address: Optional[str] = Field(None, description="Dirección del empleado", example="Calle Mayor", alias="DIRECCIÓN DEL EMPLEADO")
68
+ street_number: Optional[int] = Field(None, description="Número", example=123, alias="NÚMERO")
69
+ building: Optional[str] = Field(None, description="Bloque", example="A", alias="BLOQUE")
70
+ staircase: Optional[str] = Field(None, description="Escalera", example="1", alias="ESCALERA")
71
+ floor: Optional[str] = Field(None, description="Piso", example="2", alias="PISO")
72
+ door: Optional[str] = Field(None, description="Puerta", example="A", alias="PUERTA")
73
+ country: Optional[str] = Field(None, description="País", example="", alias="PAÍS")
74
+ city: Optional[str] = Field(None, description="Población", example="", alias="POBLACIÓN")
75
+ province: Optional[str] = Field(None, description="Provincia", example="", alias="PROVINCIA")
76
+ community: Optional[str] = Field(None, description="Comunidad", example="", alias="COMUNIDAD")
77
+ postal_code: Optional[str] = Field(None, description="Código postal", example="28001", alias="CÓDIGO POSTAL")
78
+
79
+ # Employment Information
80
+ company: Optional[str] = Field(None, description="Empresa", example="EMPRESA001", alias="EMPRESA")
81
+ job: Optional[str] = Field(None, description="Puesto", example="ANALISTA", alias="PUESTO")
82
+ position: Optional[str] = Field(None, description="Posición", example="", alias="POSICION")
83
+ organizational_unit: Optional[str] = Field(None, description="Unidad organizativa", example="IT", alias="UNIDAD ORGANIZATIVA")
84
+ work_location: Optional[str] = Field(None, description="Lugar trabajo", example="MADRID", alias="LUGAR TRABAJO")
85
+ cost_center: Optional[str] = Field(None, description="Centro de costo", example="CC001", alias="CENTRO DE COSTO")
86
+ start_reason: Optional[Literal["001"]] = Field(None, description="Motivo inicio", example="001", alias="MOTIVO INICIO")
87
+ employee_type: Optional[str] = Field(None, description="Tipo empleado", example="FIJO", alias="TIPO EMPLEADO")
88
+ probation_end_date: Optional[datetime] = Field(None, description="Fin periodo prueba", example="2024-03-31", alias="FIN PERIODO PRUEBA")
89
+ expected_end_date: Optional[datetime] = Field(None, description="Finalización prevista", example="2024-12-31", alias="FINALIZACIÓN PREVISTA")
90
+ duration: Optional[int] = Field(None, description="Duración", example=12, alias="DURACIÓN")
91
+
92
+ # Social Security Information
93
+ has_ss_number: Optional[SSNumberType] = Field(None, description="Con/sin num. S.S.", example=SSNumberType.WITH_SS, alias="CON/SIN NUM. S.S.")
94
+ ss_province: Optional[str] = Field(None, description="Núm S.S. (provincia)", example="28", alias="NÚM S.S. (PROVINCIA)")
95
+ ss_number: Optional[str] = Field(None, description="Núm S.S. (número)", example="12345678", alias="NÚM S.S. (NÚMERO)")
96
+ ss_check_digit: Optional[str] = Field(None, description="Núm S.S. (D.C.)", example="12", alias="NÚM S.S. (D.C.)")
97
+ ss_header: Optional[str] = Field(None, description="Cabecera TC1", example="TC1", alias="CABECERA TC1")
98
+ tariff_group: Optional[str] = Field(None, description="Grupo de tarifa", example="01", alias="GRUPO DE TARIFA")
99
+ occupation: Optional[str] = Field(None, description="Ocupación", example="", alias="OCUPACIÓN")
100
+ ss_agreement: Optional[str] = Field(None, description="Convenio S.S.", example="", alias="CONVENIO S.S.")
101
+ multi_employee_number: Optional[str] = Field(None, description="Número pluriempleado", example="", alias="NÚMERO PLURIEMPLEADO")
102
+ legal_contract: Optional[str] = Field(None, description="Contrato legal", example="", alias="CONTRATO LEGAL")
103
+ internal_contract: Optional[ContractType] = Field(None, description="Contrato interno", example=ContractType.REGULAR_INDEFINITE_FULL_TIME, alias="CONTRATO INTERNO")
104
+ contract_end_date: Optional[datetime] = Field(None, description="Fecha fin contrato", example="2024-12-31", alias="FECHA FIN CONTRATO")
105
+ labor_relationship: Optional[LaborRelationshipType] = Field(None, description="Relación laboral", example=LaborRelationshipType.HIGH_MANAGEMENT_PERSONNEL, alias="RELACIÓN LABORAL")
106
+ part_time_percentage: Optional[float] = Field(None, ge=0, le=100, description="% Jornada parcial", example=50.0, alias="% JORNADA PARCIAL")
107
+ hours_type: Optional[HoursType] = Field(None, description="Tipo horas", example=HoursType.WEEKLY, alias="TIPO HORAS")
108
+ hours_number: Optional[int] = Field(None, description="Número horas", example=40, alias="NÚMERO HORAS")
109
+ part_time_type: Optional[PartTimeType] = Field(None, description="Tipo jornada parcial", example=PartTimeType.REGULAR, alias="TIPO JORNADA PARCIAL")
110
+ work_days_weekly: Optional[str] = Field(None, description="Días trabajo semanales", example="5", alias="DÍAS TRABAJO SEMANALES")
111
+
112
+ # Reduction Information
113
+ reduction_percentage: Optional[str] = Field(None, description="% Reducción", example="25", alias="% REDUCCIÓN")
114
+ reduction_reason: Optional[ReductionReasonType] = Field(None, description="Motivo reducción", example=ReductionReasonType.CARE_OF_MINOR, alias="MOTIVO REDUCCIÓN")
115
+ substitution_cause: Optional[SubstitutionCauseType] = Field(None, description="Causa sustitución", example=SubstitutionCauseType.SUBSTITUTION_LEAVE_FAMILY_CARE, alias="CAUSA SUSTITUCIÓN")
116
+ substituted_ss_province: Optional[str] = Field(None, description="Núm S.S. del sustituido (provincia)", example="", alias="NÚM S.S. DEL SUSTITUIDO (PROVINCIA)")
117
+ substituted_ss_number: Optional[str] = Field(None, description="Núm S.S. del sustituido (número)", example="", alias="NÚM S.S. DEL SUSTITUIDO (NÚMERO)")
118
+ substituted_ss_check_digit: Optional[str] = Field(None, description="Núm S.S. del sustituido (D.C.)", example="", alias="NÚM S.S. DEL SUSTITUIDO (D.C.)")
119
+ disability_percentage: Optional[float] = Field(None, ge=0, le=100, description="% Minusvalía", example=33.0, alias="% MINUSVALÍA")
120
+ old_contract_start: Optional[datetime] = Field(None, description="Inicio antiguo contrato", example="2020-01-01", alias="INICIO ANTIGUO CONTRATO")
121
+ woman_maternity_24_months: Optional[YesNoType] = Field(None, description="Mujer mater. 24 meses", example=YesNoType.NO, alias="MUJER MATER. 24 MESES")
122
+ woman_underrepresented: Optional[YesNoType] = Field(None, description="Mujer subrepresentada", example=YesNoType.NO, alias="MUJER SUBREPRESENTADA")
123
+ probation_days: Optional[int] = Field(None, description="Días de prueba", example=30, alias="DÍAS DE PRUEBA")
124
+ additional_clause: Optional[str] = Field(None, description="Cláusula adicional", example="SC", alias="CLÁUSULA ADICIONAL")
125
+ adjustment_type: Optional[AdjustmentType] = Field(None, description="Tipo de ajuste", example=AdjustmentType.WITH_ADJUSTMENT, alias="TIPO DE AJUSTE")
126
+ agreement: Optional[str] = Field(None, description="Convenio", example="CONV001", alias="CONVENIO")
127
+ category: Optional[str] = Field(None, description="Categoría", example="TECNICO", alias="CATEGORÍA")
128
+ annual_gross: Optional[float] = Field(None, ge=0, description="Bruto anual", example=30000.0, alias="BRUTO ANUAL")
129
+ salary_type: Optional[SalaryType] = Field(None, description="Tipo salario", example=SalaryType.MONTHLY, alias="TIPO SALARIO")
130
+ seniority_date: Optional[datetime] = Field(None, description="Fecha antigüedad", example="2020-01-01", alias="FECHA ANTIGÜEDAD")
131
+ extras_date: Optional[datetime] = Field(None, description="Fecha extras", example="2024-01-01", alias="FECHA EXTRAS")
132
+ union: Optional[str] = Field(None, description="Sindicato", example="", alias="SINDICATO")
133
+ irpf_type: Optional[IRPFType] = Field(None, description="Tipo IRPF", example=IRPFType.NATIONAL, alias="TIPO IRPF")
134
+ irpf_status: Optional[IRPFStatusType] = Field(None, description="Estado IRPF", example=IRPFStatusType.SINGLE_WIDOWED_DIVORCED_SEPARATED_WITH_MINORS, alias="ESTADO IRPF")
135
+ disability_hr: Optional[DisabilityHRType] = Field(None, description="Minusvalía RH", example="", alias="MINUSVALÍA RH")
136
+ payment_type: Optional[PaymentType] = Field(None, description="Tipo pago", example=PaymentType.BANK_TRANSFER, alias="TIPO PAGO")
137
+ company_bank: Optional[str] = Field(None, description="Banco empresa", example="BANK001", alias="BANCO EMPRESA")
138
+ bank_branch: Optional[str] = Field(None, description="Banco + sucursal", example="BRANCH001", alias="BANCO + SUCURSAL")
139
+ account_number: Optional[str] = Field(None, description="Núm. cuenta", example="12345678901234567890", alias="NÚM. CUENTA")
140
+ check_digit: Optional[str] = Field(None, description="D.C.", example="12", alias="D.C.")
141
+ activity_type: Optional[str] = Field(None, description="Tipo actividad", example="ACT001", alias="TIPO ACTIVIDAD")
142
+ producer_type: Optional[str] = Field(None, description="Tipo productor", example="", alias="TIPO PRODUCTOR")
143
+ management_type: Optional[ManagementType] = Field(None, description="Tipo gestión", example=ManagementType.UNICA, alias="TIPO GESTION")
144
+ professional_group: Optional[str] = Field(None, description="Grupo profesional", example="", alias="GRUPO PROFESIONAL")
145
+ professional_level: Optional[str] = Field(None, description="Nivel profesional", example="", alias="NIVEL PROFESIONAL")
146
+ project: Optional[str] = Field(None, description="Proyecto", example="", alias="PROYECTO")
147
+ team: Optional[str] = Field(None, description="Equipo", example="", alias="EQUIPO")
148
+ product: Optional[str] = Field(None, description="Producto", example="", alias="PRODUCTO")
149
+ function: Optional[str] = Field(None, description="Función", example="", alias="FUNCION")
150
+ organizational_subgroup: Optional[str] = Field(None, description="Subgrupo organizativo", example="", alias="SUBGRUPO ORGANIZATIVO")
151
+ department: Optional[str] = Field(None, description="Departamento", example="", alias="DEPARTAMENTO")
152
+ region: Optional[str] = Field(None, description="Región", example="", alias="REGION")
153
+ country_region: Optional[str] = Field(None, description="País", example="", alias="PAIS")
154
+ territory: Optional[str] = Field(None, description="Territorio", example="", alias="TERRITORIO")
155
+ perception_key: Optional[PerceptionKeyType] = Field(None, description="Clave percepción", example=PerceptionKeyType.A, alias="CLAVE PERCEPCIÓN")
156
+ external_employee_id: Optional[str] = Field(None, description="ID empleado externo", example="", alias="ID EMPLEADO EXTERNO")
157
+
158
+ # -------- DATETIME PARSE --------
159
+ @field_validator(
160
+ "effective_date", "termination_date", "vacation_end_date",
161
+ "birth_date", "probation_end_date", "expected_end_date",
162
+ "old_contract_start", "seniority_date", "extras_date",
163
+ mode="before"
164
+ )
165
+ def parse_dd_mm_yyyy(cls, v):
166
+ if v in (None, "") or isinstance(v, datetime):
167
+ return v
168
+ return datetime.strptime(v, "%d/%m/%Y")
169
+
170
+ # -------- MOVEMENT TYPE VALIDATION --------
171
+ @model_validator(mode='after')
172
+ def validate_movement_requirements(self):
173
+ """
174
+ Validate mandatory fields based on movement type.
175
+ """
176
+ if self.movement_type == MovementType.MODIFICACION:
177
+ # For MODIFICACION (3), only effective_date and person_id are mandatory
178
+ mandatory_fields_modificacion = [
179
+ 'effective_date', 'person_id'
180
+ ]
181
+
182
+ for field_name in mandatory_fields_modificacion:
183
+ field_value = getattr(self, field_name, None)
184
+ if field_value is None or field_value == "":
185
+ raise ValueError(f"{field_name} is mandatory for MODIFICACION movement")
186
+
187
+ elif self.movement_type == MovementType.BAJA:
188
+ # For BAJA (2), termination_date and termination_reason are mandatory
189
+ mandatory_fields_baja = [
190
+ 'effective_date', 'termination_date', 'termination_reason',
191
+ 'unemployment_cause', 'person_id'
192
+ ]
193
+
194
+ for field_name in mandatory_fields_baja:
195
+ field_value = getattr(self, field_name, None)
196
+ if field_value is None or field_value == "":
197
+ raise ValueError(f"{field_name} is mandatory for BAJA movement")
198
+
199
+ elif self.movement_type == MovementType.ALTA:
200
+ # For ALTA (1), all basic employee information is mandatory
201
+ mandatory_fields_alta = [
202
+ 'effective_date', 'person_id', 'first_surname', 'second_surname',
203
+ 'employee_name', 'document_type', 'document_number', 'birth_date',
204
+ 'nationality', 'birth_country', 'gender', 'location_type', 'street_type',
205
+ 'employee_address', 'street_number', 'postal_code', 'company', 'job',
206
+ 'organizational_unit', 'work_location', 'cost_center', 'start_reason',
207
+ 'employee_type', 'has_ss_number', 'ss_province', 'ss_number',
208
+ 'ss_check_digit', 'internal_contract', 'reduction_percentage',
209
+ 'reduction_reason', 'additional_clause', 'adjustment_type', 'agreement',
210
+ 'category', 'salary_type', 'irpf_type', 'irpf_status', 'payment_type',
211
+ 'company_bank', 'bank_branch', 'account_number', 'check_digit',
212
+ 'management_type', 'perception_key'
213
+ ]
214
+
215
+ for field_name in mandatory_fields_alta:
216
+ field_value = getattr(self, field_name, None)
217
+ if field_value is None or field_value == "":
218
+ raise ValueError(f"{field_name} is mandatory for ALTA movement")
219
+
220
+ return self
221
+
222
+ class Config:
223
+ json_encoders = {datetime: lambda d: d.strftime("%d/%m/%Y")}
224
+ allow_population_by_field_name = True
225
+ populate_by_name = True
@@ -0,0 +1,59 @@
1
+ """
2
+ Meta4 Job schema based on EQU_Interfaz_Puestos specification.
3
+
4
+ This schema defines the structure for job data that will be exported to Meta4 HR system.
5
+ The schema includes movement type, job information, and date fields with proper formatting.
6
+
7
+ """
8
+
9
+ from pydantic import BaseModel, Field, field_validator, model_validator
10
+ from typing import Optional, Literal
11
+ from datetime import datetime
12
+
13
+
14
+ class JobSchema(BaseModel):
15
+ """
16
+ Pydantic schema for Meta4 Job data.
17
+ Based on EQU_Interfaz_Puestos specification.
18
+ """
19
+
20
+ # Movement Information
21
+ movement_type: Literal["-28", "-29"] = Field(description="Tipo de movimiento", example="-28", alias="Tipo de movimiento", max_length=3)
22
+
23
+ # Job Information
24
+ job_id: Optional[str] = Field(None, description="ID Puesto", example="100", alias="ID Puesto", max_length=8)
25
+ job_name: Optional[str] = Field(None, description="Nombre Puesto", example="Puesto 1", alias="Nombre Puesto", max_length=62)
26
+ start_date: Optional[datetime] = Field(None, description="Fecha inicio", example="01/01/1800", alias="Fecha Inicio", max_length=10)
27
+ end_date: Optional[datetime] = Field(None, description="Fecha fin", example="01/01/180", alias="Fecha Fin ", max_length=10)
28
+ cno_subcode: Optional[str] = Field(None, description="ID Subcódigo CNO del Puesto", example="1120", alias="CNO", max_length=4)
29
+
30
+ @model_validator(mode='after')
31
+ def validate_movement_requirements(self):
32
+ """Validate mandatory fields based on movement_type"""
33
+ if self.movement_type == "-28":
34
+ # All fields are mandatory for CREATE (-28)
35
+ mandatory_fields_create = ['job_id', 'job_name', 'start_date', 'end_date', 'cno_subcode']
36
+ for field_name in mandatory_fields_create:
37
+ field_value = getattr(self, field_name, None)
38
+ if field_value is None or field_value == "":
39
+ raise ValueError(f"{field_name} is mandatory for CREATE (-28) movement")
40
+ elif self.movement_type == "-29":
41
+ # Only basic fields are mandatory for UPDATE (-29)
42
+ mandatory_fields_update = ['job_id', 'job_name']
43
+ for field_name in mandatory_fields_update:
44
+ field_value = getattr(self, field_name, None)
45
+ if field_value is None or field_value == "":
46
+ raise ValueError(f"{field_name} is mandatory for UPDATE (-29) movement")
47
+ return self
48
+
49
+ # --- DATETIME PARSE ---
50
+ @field_validator("start_date", "end_date", mode="before")
51
+ def parse_dd_mm_yyyy(cls, v):
52
+ if v in (None, "") or isinstance(v, datetime):
53
+ return v
54
+ return datetime.strptime(v, "%d/%m/%Y")
55
+
56
+ class Config:
57
+ json_encoders = {datetime: lambda d: d.strftime("%d/%m/%Y")}
58
+ allow_population_by_field_name = True
59
+ populate_by_name = True