diff --git a/addons_extensions/document_parser/models/document_parser_service.py b/addons_extensions/document_parser/models/document_parser_service.py
index ff5d8da16..f59727146 100644
--- a/addons_extensions/document_parser/models/document_parser_service.py
+++ b/addons_extensions/document_parser/models/document_parser_service.py
@@ -269,6 +269,9 @@ RULES:
- Scan the entire document carefully before answering.
- Extract ONLY what exists in text.
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
+- For list fields, always return an array. Return [] when no explicit entries exist.
+- For list fields that describe objects, return one object per real document entry, not a sentence blob.
+- Do not skip education, employer, project, certification, or family sections if they appear later in the document.
FIELD RULES:
- If "skills" exists, extract only explicit technical skills written in the document.
@@ -283,6 +286,9 @@ FIELD RULES:
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
- If "phone" exists, return the most complete phone number found.
- If "experience" exists, return only clearly supported numeric values.
+- If "education_history" exists, extract every education row/section with education_type, specialization, university, start_year, end_year, and marks_or_grade.
+- If "employer_history" exists, extract every employer/project/work-experience entry with company_name, designation, date_of_joining, last_working_day, ctc, and work_description.
+- If "family_details" exists, extract only explicitly written family members with relation_type, name, contact_no, dob, and location.
Schema:
{schema_text}
@@ -301,7 +307,7 @@ Document:
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
- "max_tokens": 1500,
+ "max_tokens": 4000,
}
try:
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
diff --git a/addons_extensions/hr_recruitment_auto_doc/security/ir.model.access.csv b/addons_extensions/hr_recruitment_auto_doc/security/ir.model.access.csv
index 50393c7aa..4f95c0ff8 100644
--- a/addons_extensions/hr_recruitment_auto_doc/security/ir.model.access.csv
+++ b/addons_extensions/hr_recruitment_auto_doc/security/ir.model.access.csv
@@ -1,3 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_recruitment_auto_doc_wizard,hr.recruitment.auto.doc.wizard,model_hr_recruitment_auto_doc_wizard,base.group_user,1,1,1,1
access_hr_recruitment_auto_doc_wizard_line,hr.recruitment.auto.doc.wizard.line,model_hr_recruitment_auto_doc_wizard_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_education_line,hr.recruitment.auto.doc.education.line,model_hr_recruitment_auto_doc_education_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_employer_line,hr.recruitment.auto.doc.employer.line,model_hr_recruitment_auto_doc_employer_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_family_line,hr.recruitment.auto.doc.family.line,model_hr_recruitment_auto_doc_family_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_certification_line,hr.recruitment.auto.doc.certification.line,model_hr_recruitment_auto_doc_certification_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_project_line,hr.recruitment.auto.doc.project.line,model_hr_recruitment_auto_doc_project_line,base.group_user,1,1,1,1
+access_hr_recruitment_auto_doc_other_line,hr.recruitment.auto.doc.other.line,model_hr_recruitment_auto_doc_other_line,base.group_user,1,1,1,1
diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
index b780b8ad9..edde98818 100644
--- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
+++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
@@ -484,6 +484,17 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
),
},
+ "certifications": {
+ "type": "list",
+ "description": "List of explicit certifications, courses, training, or licenses if present.",
+ },
+ "projects": {
+ "type": "list",
+ "description": (
+ "List of explicit resume projects if present. Each item should include project_name, role, "
+ "technologies, duration, and description when available."
+ ),
+ },
}
def _get_jd_required_fields(self):
@@ -514,6 +525,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"Normalize skills into clean individual names. "
"For experience values, return numeric years when clearly inferable. "
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
+ "Read the complete resume including tables, side columns, and later pages before returning JSON. "
+ "education_history must include every school, intermediate, diploma, graduation, post graduation, or additional qualification row that is explicitly present. "
+ "employer_history must include internships, current employer, previous employers, project/company experience, and work periods when explicitly present. "
+ "family_details must include every explicitly mentioned family member and must be [] if the resume has no family section. "
"For each employer entry, extract the role-specific work description into work_description. "
"Only include entries that are explicitly present in the document. "
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
@@ -2145,6 +2160,40 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
degree = fields.Char()
skills_text = fields.Text(string="Skills")
summary = fields.Text()
+ education_history_json = fields.Text(string="Education History")
+ employer_history_json = fields.Text(string="Employer History")
+ family_details_json = fields.Text(string="Family Details")
+ other_payload_json = fields.Text(string="Other Parsed Data")
+ education_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.education.line",
+ "wizard_line_id",
+ string="Education History",
+ )
+ employer_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.employer.line",
+ "wizard_line_id",
+ string="Employer History",
+ )
+ family_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.family.line",
+ "wizard_line_id",
+ string="Family Details",
+ )
+ certification_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.certification.line",
+ "wizard_line_id",
+ string="Certifications",
+ )
+ project_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.project.line",
+ "wizard_line_id",
+ string="Projects",
+ )
+ other_line_ids = fields.One2many(
+ "hr.recruitment.auto.doc.other.line",
+ "wizard_line_id",
+ string="Other Parsed Data",
+ )
request_id = fields.Char(string="Request ID")
start_date = fields.Char()
@@ -2197,6 +2246,16 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
"degree": parsed_data.get("degree"),
"skills_text": self._list_to_text(parsed_data.get("skills")),
"summary": parsed_data.get("summary"),
+ "education_history_json": self._json_to_text(parsed_data.get("education_history")),
+ "employer_history_json": self._json_to_text(parsed_data.get("employer_history")),
+ "family_details_json": self._json_to_text(parsed_data.get("family_details")),
+ "other_payload_json": self._json_to_text(self._get_other_parsed_data(parsed_data)),
+ "education_line_ids": self._prepare_education_line_commands(parsed_data.get("education_history")),
+ "employer_line_ids": self._prepare_employer_line_commands(parsed_data.get("employer_history"), parsed_data.get("projects")),
+ "family_line_ids": self._prepare_family_line_commands(parsed_data.get("family_details")),
+ "certification_line_ids": self._prepare_certification_line_commands(parsed_data.get("certifications")),
+ "project_line_ids": self._prepare_project_line_commands(parsed_data.get("projects")),
+ "other_line_ids": self._prepare_other_line_commands(self._get_other_parsed_data(parsed_data)),
"request_id": parsed_data.get("request_id"),
"start_date": parsed_data.get("start_date"),
"end_date": parsed_data.get("end_date"),
@@ -2245,9 +2304,158 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
"degree": self.degree,
"skills": self._text_to_list(self.skills_text),
"summary": self.summary,
+ "education_history": self._get_education_history_from_lines(),
+ "employer_history": self._get_employer_history_from_lines(),
+ "family_details": self._get_family_details_from_lines(),
+ "certifications": self._get_certifications_from_lines(),
+ "projects": self._get_projects_from_lines(),
})
+ data.update(self._get_other_payload_from_lines())
return data
+ def _prepare_education_line_commands(self, education_history):
+ commands = [(5, 0, 0)]
+ for education in self._normalize_dict_list(education_history):
+ commands.append((0, 0, {
+ "education_type": education.get("education_type"),
+ "specialization": education.get("specialization") or education.get("name") or education.get("degree"),
+ "university": education.get("university") or education.get("institution") or education.get("college"),
+ "start_year": education.get("start_year"),
+ "end_year": education.get("end_year"),
+ "marks_or_grade": education.get("marks_or_grade") or education.get("marks") or education.get("grade") or education.get("cgpa") or education.get("percentage"),
+ }))
+ return commands
+
+ def _prepare_employer_line_commands(self, employer_history, projects=None):
+ commands = [(5, 0, 0)]
+ employers = self._normalize_shared_employer_descriptions(
+ self._normalize_dict_list(employer_history),
+ projects,
+ )
+ for employer in employers:
+ commands.append((0, 0, {
+ "company_name": employer.get("company_name") or employer.get("employer"),
+ "designation": employer.get("designation") or employer.get("role") or employer.get("job_title"),
+ "date_of_joining": employer.get("date_of_joining") or employer.get("start_date"),
+ "last_working_day": employer.get("last_working_day") or employer.get("end_date"),
+ "ctc": employer.get("ctc") or employer.get("salary"),
+ "work_description": employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities"),
+ }))
+ return commands
+
+ def _prepare_family_line_commands(self, family_details):
+ commands = [(5, 0, 0)]
+ for member in self._normalize_dict_list(family_details):
+ commands.append((0, 0, {
+ "relation_type": member.get("relation_type") or member.get("relation"),
+ "name": member.get("name"),
+ "contact_no": member.get("contact_no") or member.get("contact") or member.get("phone"),
+ "dob": member.get("dob"),
+ "location": member.get("location") or member.get("address"),
+ }))
+ return commands
+
+ def _prepare_certification_line_commands(self, certifications):
+ commands = [(5, 0, 0)]
+ for certification in self._normalize_value_list(certifications):
+ if isinstance(certification, dict):
+ commands.append((0, 0, {
+ "certification_name": certification.get("certification_name") or certification.get("name") or certification.get("title"),
+ "provider": certification.get("provider") or certification.get("issuer") or certification.get("organization"),
+ "completion_date": certification.get("completion_date") or certification.get("date") or certification.get("year"),
+ }))
+ else:
+ commands.append((0, 0, {"certification_name": str(certification)}))
+ return commands
+
+ def _prepare_project_line_commands(self, projects):
+ commands = [(5, 0, 0)]
+ for project in self._normalize_dict_list(projects):
+ technologies = project.get("technologies")
+ commands.append((0, 0, {
+ "project_name": project.get("project_name") or project.get("name") or project.get("title"),
+ "role": project.get("role"),
+ "technologies": ", ".join(technologies) if isinstance(technologies, list) else technologies,
+ "duration": project.get("duration"),
+ "description": project.get("description") or project.get("summary") or project.get("work_description"),
+ }))
+ return commands
+
+ def _prepare_other_line_commands(self, other_payload):
+ commands = [(5, 0, 0)]
+ for key, value in (other_payload or {}).items():
+ commands.append((0, 0, {
+ "key": key,
+ "value": self._json_to_text(value) or str(value),
+ }))
+ return commands
+
+ def _get_education_history_from_lines(self):
+ return [line._to_payload() for line in self.education_line_ids if line._to_payload()]
+
+ def _get_employer_history_from_lines(self):
+ return [line._to_payload() for line in self.employer_line_ids if line._to_payload()]
+
+ def _get_family_details_from_lines(self):
+ return [line._to_payload() for line in self.family_line_ids if line._to_payload()]
+
+ def _get_certifications_from_lines(self):
+ return [line._to_payload() for line in self.certification_line_ids if line._to_payload()]
+
+ def _get_projects_from_lines(self):
+ return [line._to_payload() for line in self.project_line_ids if line._to_payload()]
+
+ def _get_other_payload_from_lines(self):
+ return {
+ line.key: line._json_value()
+ for line in self.other_line_ids
+ if line.key and line.value not in (False, None, "")
+ }
+
+ def _normalize_dict_list(self, value):
+ if isinstance(value, list):
+ return [item for item in value if isinstance(item, dict)]
+ if isinstance(value, dict):
+ return [value]
+ return []
+
+ def _normalize_value_list(self, value):
+ if isinstance(value, list):
+ return [item for item in value if item not in (False, None, "", {})]
+ if value in (False, None, "", {}):
+ return []
+ return [value]
+
+ def _normalize_shared_employer_descriptions(self, employers, projects=None):
+ if len(employers) < 2:
+ return employers
+
+ descriptions = [
+ employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")
+ for employer in employers
+ ]
+ descriptions = [description for description in descriptions if description]
+ if descriptions:
+ shared_description = max(descriptions, key=lambda item: len(str(item)))
+ else:
+ project_descriptions = []
+ for project in self._normalize_dict_list(projects):
+ description = project.get("description") or project.get("summary") or project.get("work_description")
+ if description:
+ project_descriptions.append("%s: %s" % (
+ project.get("project_name") or project.get("name") or project.get("title") or "Project",
+ description,
+ ))
+ shared_description = "\n".join(project_descriptions) if project_descriptions else False
+
+ if not shared_description:
+ return employers
+
+ for employer in employers:
+ if not (employer.get("work_description") or employer.get("summary") or employer.get("description") or employer.get("responsibilities")):
+ employer["work_description"] = shared_description
+ return employers
+
def _list_to_text(self, value):
if not value:
return False
@@ -2260,6 +2468,47 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
return []
return [item.strip(" -,\t") for item in str(value).splitlines() if item.strip(" -,\t")]
+ def _json_to_text(self, value):
+ if value in (False, None, "", [], {}):
+ return False
+ return json.dumps(value, indent=2, ensure_ascii=False)
+
+ def _text_to_json_list(self, value):
+ parsed = self._text_to_json_value(value, [])
+ if isinstance(parsed, list):
+ return [item for item in parsed if item not in (False, None, "", {})]
+ if isinstance(parsed, dict):
+ return [parsed]
+ return []
+
+ def _text_to_json_dict(self, value):
+ parsed = self._text_to_json_value(value, {})
+ return parsed if isinstance(parsed, dict) else {}
+
+ def _text_to_json_value(self, value, default):
+ if not value:
+ return default
+ try:
+ return json.loads(value)
+ except Exception:
+ return default
+
+ def _get_other_parsed_data(self, parsed_data):
+ known_keys = {
+ "full_name", "first_name", "last_name", "email", "phone", "alternate_phone",
+ "linkedin_profile", "current_location", "current_organization",
+ "total_experience_years", "relevant_experience_years", "notice_period",
+ "degree", "skills", "summary", "education_history", "employer_history",
+ "family_details", "certifications", "projects", "request_id", "start_date", "end_date", "site_location",
+ "job_title", "job_summary", "requirements", "primary_skills",
+ "secondary_skills", "budget", "experience_years", "job_category",
+ }
+ return {
+ key: value
+ for key, value in (parsed_data or {}).items()
+ if key not in known_keys and value not in (False, None, "", [], {})
+ }
+
def _float_or_zero(self, value):
if value in (False, None, ""):
return 0.0
@@ -2267,3 +2516,166 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
return float(value)
except Exception:
return 0.0
+
+
+class HrRecruitmentAutoDocEducationLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.education.line"
+ _description = "HR Recruitment Auto Document Education Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ education_type = fields.Char()
+ specialization = fields.Char()
+ university = fields.Char()
+ start_year = fields.Char()
+ end_year = fields.Char()
+ marks_or_grade = fields.Char()
+
+ def _to_payload(self):
+ self.ensure_one()
+ payload = {
+ "education_type": self.education_type,
+ "specialization": self.specialization,
+ "university": self.university,
+ "start_year": self.start_year,
+ "end_year": self.end_year,
+ "marks_or_grade": self.marks_or_grade,
+ }
+ return {key: value for key, value in payload.items() if value not in (False, None, "")}
+
+
+class HrRecruitmentAutoDocEmployerLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.employer.line"
+ _description = "HR Recruitment Auto Document Employer Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ company_name = fields.Char()
+ designation = fields.Char()
+ date_of_joining = fields.Char()
+ last_working_day = fields.Char()
+ ctc = fields.Char()
+ work_description = fields.Text()
+
+ def _to_payload(self):
+ self.ensure_one()
+ payload = {
+ "company_name": self.company_name,
+ "designation": self.designation,
+ "date_of_joining": self.date_of_joining,
+ "last_working_day": self.last_working_day,
+ "ctc": self.ctc,
+ "work_description": self.work_description,
+ }
+ return {key: value for key, value in payload.items() if value not in (False, None, "")}
+
+
+class HrRecruitmentAutoDocFamilyLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.family.line"
+ _description = "HR Recruitment Auto Document Family Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ relation_type = fields.Char()
+ name = fields.Char()
+ contact_no = fields.Char()
+ dob = fields.Char()
+ location = fields.Char()
+
+ def _to_payload(self):
+ self.ensure_one()
+ payload = {
+ "relation_type": self.relation_type,
+ "name": self.name,
+ "contact_no": self.contact_no,
+ "dob": self.dob,
+ "location": self.location,
+ }
+ return {key: value for key, value in payload.items() if value not in (False, None, "")}
+
+
+class HrRecruitmentAutoDocCertificationLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.certification.line"
+ _description = "HR Recruitment Auto Document Certification Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ certification_name = fields.Char()
+ provider = fields.Char()
+ completion_date = fields.Char()
+
+ def _to_payload(self):
+ self.ensure_one()
+ payload = {
+ "certification_name": self.certification_name,
+ "provider": self.provider,
+ "completion_date": self.completion_date,
+ }
+ return {key: value for key, value in payload.items() if value not in (False, None, "")}
+
+
+class HrRecruitmentAutoDocProjectLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.project.line"
+ _description = "HR Recruitment Auto Document Project Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ project_name = fields.Char()
+ role = fields.Char()
+ technologies = fields.Char()
+ duration = fields.Char()
+ description = fields.Text()
+
+ def _to_payload(self):
+ self.ensure_one()
+ payload = {
+ "project_name": self.project_name,
+ "role": self.role,
+ "technologies": [item.strip() for item in (self.technologies or "").split(",") if item.strip()],
+ "duration": self.duration,
+ "description": self.description,
+ }
+ return {key: value for key, value in payload.items() if value not in (False, None, "", [])}
+
+
+class HrRecruitmentAutoDocOtherLine(models.TransientModel):
+ _name = "hr.recruitment.auto.doc.other.line"
+ _description = "HR Recruitment Auto Document Other Parsed Data Line"
+ _order = "id"
+
+ wizard_line_id = fields.Many2one(
+ "hr.recruitment.auto.doc.wizard.line",
+ required=True,
+ ondelete="cascade",
+ )
+ key = fields.Char(required=True)
+ value = fields.Text()
+
+ def _json_value(self):
+ self.ensure_one()
+ if not self.value:
+ return False
+ try:
+ return json.loads(self.value)
+ except Exception:
+ return self.value
diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
index cf19f3130..ce46616fa 100644
--- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
+++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
@@ -95,6 +95,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+