diff --git a/g2p_registry_base/models/reg_id.py b/g2p_registry_base/models/reg_id.py
index df7844c0..8512e7a0 100644
--- a/g2p_registry_base/models/reg_id.py
+++ b/g2p_registry_base/models/reg_id.py
@@ -23,6 +23,10 @@ class G2PRegistrantID(models.Model):
expiry_date = fields.Date()
id_type_as_str = fields.Char(related="id_type.name")
+ status = fields.Selection([("invalid", "Invalid"), ("valid", "Valid")], required=True)
+
+ description = fields.Char()
+
def _compute_display_name(self):
res = super()._compute_display_name()
for rec in self:
diff --git a/g2p_registry_base/tests/test_reg_id.py b/g2p_registry_base/tests/test_reg_id.py
index 1a1e059a..3a0032f8 100644
--- a/g2p_registry_base/tests/test_reg_id.py
+++ b/g2p_registry_base/tests/test_reg_id.py
@@ -16,7 +16,13 @@ def test_01_display_name(self):
id_type = self.id_type_model.create({"name": "Test ID Type", "id_validation": "[0-9]+"})
reg_id = self.reg_id_model.create(
- {"partner_id": partner.id, "id_type": id_type.id, "value": "123456"}
+ {
+ "partner_id": partner.id,
+ "id_type": id_type.id,
+ "value": "123456",
+ "status": "valid",
+ "description": "Due to API",
+ }
)
# Call the _compute_display_name method
@@ -35,7 +41,13 @@ def test_02_create_registrant_id(self):
id_type = self.id_type_model.create({"name": "Test ID Type", "id_validation": "[0-9]+"})
reg_id = self.reg_id_model.create(
- {"partner_id": partner.id, "id_type": id_type.id, "value": "123456"}
+ {
+ "partner_id": partner.id,
+ "id_type": id_type.id,
+ "value": "123456",
+ "status": "valid",
+ "description": "Due to API",
+ }
)
self.assertEqual(reg_id.value, "123456", "Registrant ID value is not as expected.")
@@ -44,7 +56,15 @@ def test_03_invalid_id_value(self):
id_type = self.id_type_model.create({"name": "Test ID Type", "id_validation": "[0-9]+"})
with self.assertRaises(ValidationError) as context:
- self.reg_id_model.create({"partner_id": partner.id, "id_type": id_type.id, "value": "abc"})
+ self.reg_id_model.create(
+ {
+ "partner_id": partner.id,
+ "id_type": id_type.id,
+ "value": "abc",
+ "status": "valid",
+ "description": "Due to API",
+ }
+ )
self.assertIn("The provided Test ID Type ID 'abc' is invalid.", str(context.exception))
@@ -52,7 +72,13 @@ def test_04_name_search(self):
partner = self.partner_model.create({"name": "Test Partner", "is_registrant": True})
id_type = self.id_type_model.create({"name": "Test ID Type"})
reg_id = self.reg_id_model.create(
- {"partner_id": partner.id, "id_type": id_type.id, "value": "Test Value"}
+ {
+ "partner_id": partner.id,
+ "id_type": id_type.id,
+ "value": "Test Value",
+ "status": "valid",
+ "description": "Due to API",
+ }
)
search_result = self.reg_id_model._name_search("Test Partner")
self.assertIn(reg_id.id, search_result, "Expected record not found in search result")
diff --git a/g2p_registry_base/views/reg_id_view.xml b/g2p_registry_base/views/reg_id_view.xml
index 650ebebf..10cddea3 100644
--- a/g2p_registry_base/views/reg_id_view.xml
+++ b/g2p_registry_base/views/reg_id_view.xml
@@ -12,6 +12,8 @@
+
+
@@ -24,6 +26,8 @@
+
+
diff --git a/g2p_registry_group/tests/test_groups.py b/g2p_registry_group/tests/test_groups.py
index e631c97b..a9f74751 100644
--- a/g2p_registry_group/tests/test_groups.py
+++ b/g2p_registry_group/tests/test_groups.py
@@ -115,7 +115,12 @@ def test_02_add_id(self):
"name": "Testing ID Type",
}
)
- vals = {"id_type": id_type.id, "value": "112233445566778899"}
+ vals = {
+ "id_type": id_type.id,
+ "value": "112233445566778899",
+ "status": "valid",
+ "description": "Due to API",
+ }
self.group_1.write({"reg_ids": [(0, 0, vals)]})
expected_value = "112233445566778899"
diff --git a/g2p_registry_individual/tests/test_individuals.py b/g2p_registry_individual/tests/test_individuals.py
index bbe4ac0f..1bfa0ca1 100644
--- a/g2p_registry_individual/tests/test_individuals.py
+++ b/g2p_registry_individual/tests/test_individuals.py
@@ -139,7 +139,12 @@ def test_04_add_id(self):
"name": "Testing ID Type",
}
)
- vals = {"id_type": id_type.id, "value": "112233445566778899"}
+ vals = {
+ "id_type": id_type.id,
+ "value": "112233445566778899",
+ "status": "valid",
+ "description": "Due to API",
+ }
self.registrant_1.write({"reg_ids": [(0, 0, vals)]})
expected_value = "112233445566778899"
diff --git a/g2p_registry_individual/views/individuals_view.xml b/g2p_registry_individual/views/individuals_view.xml
index 4da96a61..866be7d5 100644
--- a/g2p_registry_individual/views/individuals_view.xml
+++ b/g2p_registry_individual/views/individuals_view.xml
@@ -199,6 +199,13 @@
/>
+
+
diff --git a/g2p_registry_rest_api/routers/individual.py b/g2p_registry_rest_api/routers/individual.py
index 017ee631..e1965cd1 100644
--- a/g2p_registry_rest_api/routers/individual.py
+++ b/g2p_registry_rest_api/routers/individual.py
@@ -3,13 +3,19 @@
from fastapi import APIRouter, Depends
+from odoo import fields
from odoo.api import Environment
from odoo.addons.fastapi.dependencies import odoo_env
from ..exceptions.base_exception import G2PApiValidationError
from ..exceptions.error_codes import G2PErrorCodes
-from ..schemas.individual import IndividualInfoRequest, IndividualInfoResponse, UpdateIndividualInfoRequest
+from ..schemas.individual import (
+ IndividualInfoRequest,
+ IndividualInfoResponse,
+ UpdateIndividualInfoRequest,
+ UpdateIndividualInfoResponse,
+)
individual_router = APIRouter(tags=["individual"])
@@ -92,7 +98,7 @@ async def get_ids(
if not include_id_type:
_handle_error(G2PErrorCodes.G2P_REQ_010, "Record is not present in the database.")
- domain = [("is_registrant", "=", True), ("is_group", "=", False)]
+ domain = [("is_registrant", "=", True), ("is_group", "=", False), ("active", "=", True)]
if include_id_type:
domain.append(("reg_ids.id_type", "=", include_id_type))
@@ -115,12 +121,12 @@ async def get_ids(
_handle_error(G2PErrorCodes.G2P_REQ_010, "An error occurred while getting IDs.")
-@individual_router.put("/update_individual", responses={200: {"model": IndividualInfoResponse}})
+@individual_router.put("/update_individual", responses={200: {"model": UpdateIndividualInfoResponse}})
async def update_individual(
requests: list[UpdateIndividualInfoRequest],
env: Annotated[Environment, Depends(odoo_env)],
id_type: str | None = "",
-) -> list[IndividualInfoResponse]:
+) -> list[UpdateIndividualInfoResponse]:
"""
Update an individual
"""
@@ -134,7 +140,14 @@ async def update_individual(
partner_rec = (
env["res.partner"]
.sudo()
- .search([("reg_ids.value", "=", _id), ("reg_ids.id_type", "=", id_type)], limit=1)
+ .search(
+ [
+ ("reg_ids.value", "=", _id),
+ ("reg_ids.id_type", "=", id_type),
+ ("active", "=", True),
+ ],
+ limit=1,
+ )
)
if not partner_rec:
_handle_error(
@@ -144,12 +157,65 @@ async def update_individual(
# Update the individual
indv_rec = env["process_individual.rest.mixin"]._process_individual(request)
-
logging.info("Individual Api: Updating Individual Record", indv_rec)
+ for reg_id in indv_rec["reg_ids"]:
+ id_type = reg_id[2].get("id_type")
+ value = reg_id[2].get("value")
+ status = reg_id[2].get("status")
+ description = reg_id[2].get("description")
+
+ id_rec = (
+ env["g2p.reg.id"]
+ .sudo()
+ .search(
+ [
+ ("value", "=", value),
+ ("id_type", "=", id_type),
+ ],
+ limit=1,
+ )
+ )
+ if id_rec:
+ # Search for the partner
+ partner_rec = (
+ env["res.partner"]
+ .sudo()
+ .search(
+ [
+ ("reg_ids.value", "=", value),
+ ("reg_ids.id_type", "=", id_type),
+ ("active", "=", True),
+ ],
+ limit=1,
+ )
+ )
+
+ indv_rec.pop("reg_ids")
+
+ if partner_rec:
+ partner_rec.update(
+ {
+ "reg_ids": [
+ (
+ fields.Command.update(
+ id_rec.id,
+ {
+ "partner_id": partner_rec.id,
+ "id_type": id_type,
+ "value": value,
+ "status": status,
+ "description": description,
+ },
+ )
+ )
+ ]
+ }
+ )
+
partner_rec.write(indv_rec)
- results.append(IndividualInfoResponse.model_validate(partner_rec))
+ results.append(UpdateIndividualInfoResponse.model_validate(partner_rec))
else:
logging.error("ID & ID type is required for update individual")
diff --git a/g2p_registry_rest_api/routers/process_individual_mixin.py b/g2p_registry_rest_api/routers/process_individual_mixin.py
index 8f3c1de8..4096ccf1 100644
--- a/g2p_registry_rest_api/routers/process_individual_mixin.py
+++ b/g2p_registry_rest_api/routers/process_individual_mixin.py
@@ -12,20 +12,24 @@ class ProcessIndividualMixin(models.AbstractModel):
def _process_individual(self, individual):
indv_rec = {
- "name": individual.name,
- "registration_date": individual.registration_date,
+ "name": individual.name if individual.name else None,
+ "registration_date": individual.registration_date if individual.registration_date else None,
"is_registrant": True,
"is_group": False,
- "email": individual.email,
- "given_name": individual.given_name,
- "family_name": individual.family_name,
- "addl_name": individual.addl_name,
- "birthdate": individual.birthdate or False,
- "birth_place": individual.birth_place or False,
+ "email": individual.email if individual.email else None,
+ "given_name": individual.given_name if individual.given_name else None,
+ "family_name": individual.family_name if individual.family_name else None,
+ "addl_name": individual.addl_name if individual.addl_name else None,
+ "birthdate": individual.birthdate if individual.birthdate else None,
+ "birth_place": individual.birth_place if individual.birth_place else None,
"address": individual.address if individual.address else None,
"image_1920": individual.image_1920 if individual.image_1920 else None,
}
+ filtered_none = {key: value for key, value in indv_rec.items() if value is not None}
+ indv_rec.clear()
+ indv_rec.update(filtered_none)
+
ids = []
ids_info = individual
ids = ProcessIndividualMixin._process_ids(self, ids_info)
@@ -61,6 +65,8 @@ def _process_ids(self, ids_info):
"id_type": id_type_id[0].id,
"value": rec.value,
"expiry_date": rec.expiry_date,
+ "status": rec.status if rec.status else None,
+ "description": rec.description if rec.description else None,
},
)
)
diff --git a/g2p_registry_rest_api/schemas/individual.py b/g2p_registry_rest_api/schemas/individual.py
index 9dbe9b4a..f8c17f24 100644
--- a/g2p_registry_rest_api/schemas/individual.py
+++ b/g2p_registry_rest_api/schemas/individual.py
@@ -1,6 +1,6 @@
-from datetime import date
+from datetime import date, datetime
-from pydantic import field_validator
+from pydantic import Field, field_validator
from .registrant import RegistrantInfoRequest, RegistrantInfoResponse
@@ -27,11 +27,29 @@ class IndividualInfoRequest(RegistrantInfoRequest):
addl_name: str | None = None
family_name: str | None = None
gender: str | None
- birthdate: date | None
+ birthdate: str = Field(
+ None, description="Date of birth in YYYY-MM-DD format", json_schema_extra={"examples": ["2000-01-01"]}
+ )
birth_place: str | None
is_group: bool = False
+ @field_validator("birthdate")
+ @classmethod
+ def parse_dob(cls, v):
+ if v is None or v == "":
+ return None
+ return datetime.strptime(v, "%Y-%m-%d").date()
+
class UpdateIndividualInfoRequest(IndividualInfoRequest):
updateId: str
image_1920: bytes | None = None
+ given_name: str | None
+ name: str | None
+
+
+class UpdateIndividualInfoResponse(IndividualInfoResponse):
+ id: int | None = None
+ given_name: str | None = None
+ name: str | None = None
+ family_name: str | None = None
diff --git a/g2p_registry_rest_api/schemas/registrant.py b/g2p_registry_rest_api/schemas/registrant.py
index 28ed04ae..60d1d545 100644
--- a/g2p_registry_rest_api/schemas/registrant.py
+++ b/g2p_registry_rest_api/schemas/registrant.py
@@ -21,22 +21,33 @@ class PhoneNumberResponse(NaiveOrmModel):
id: int
phone_no: str
phone_sanitized: str
- date_collected: date = None
+ date_collected: date | bool | None = None
# disabled: date | None = None
class PhoneNumberRequest(NaiveOrmModel):
phone_no: str
- date_collected: date = None
+ date_collected: str = Field(
+ None,
+ description="Date of Collected in YYYY-MM-DD format",
+ json_schema_extra={"examples": ["2000-01-01"]},
+ )
+
+ @field_validator("date_collected")
+ @classmethod
+ def parse_dob(cls, v):
+ if v is None or v == "":
+ return None
+ return datetime.strptime(v, "%Y-%m-%d").date()
class RegistrantInfoResponse(NaiveOrmModel):
id: int
name: str
- ids: list[RegistrantIDResponse] | None = pydantic.Field([], alias="reg_ids")
+ ids: list[RegistrantIDResponse] | None = Field([], alias="reg_ids")
is_group: bool
registration_date: date = None
- phone_numbers: list[PhoneNumberResponse] | None = pydantic.Field([], alias="phone_number_ids")
+ phone_numbers: list[PhoneNumberResponse] | None = Field([], alias="phone_number_ids")
email: str | bool | None = None
address: str | bool | None = None
create_date: datetime = None
@@ -52,13 +63,42 @@ def validate_email(cls, v):
class RegistrantIDRequest(NaiveOrmModel):
id_type: str
value: str
- expiry_date: date | None = None
+ expiry_date: str = Field(
+ None, description="Expiry date in YYYY-MM-DD format", json_schema_extra={"examples": ["2000-01-01"]}
+ )
+ status: str = None
+ description: str = None
+
+ @field_validator("id_type", "value")
+ @classmethod
+ def check_not_empty(cls, v, field):
+ if not v:
+ raise ValueError(f"{field} cannot be empty")
+ return v
+
+ @field_validator("expiry_date")
+ @classmethod
+ def parse_dob(cls, v):
+ if v is None or v == "":
+ return None
+ return datetime.strptime(v, "%Y-%m-%d").date()
class RegistrantInfoRequest(NaiveOrmModel):
name: str = Field(..., description="Mandatory field")
ids: list[RegistrantIDRequest]
- registration_date: date = None
+ registration_date: str = Field(
+ None,
+ description="Registration date in YYYY-MM-DD format",
+ json_schema_extra={"examples": ["2000-01-01"]},
+ )
phone_numbers: list[PhoneNumberRequest] | None = None
email: str | None = None
address: str | None = None
+
+ @field_validator("registration_date")
+ @classmethod
+ def parse_dob(cls, v):
+ if v is None or v == "":
+ return None
+ return datetime.strptime(v, "%Y-%m-%d").date()