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()