Files
ihrm/employee/methods/methods.py

959 lines
31 KiB
Python

"""
employee/methods.py
"""
import logging
import re
import threading
from datetime import date, datetime
from itertools import chain, groupby
import pandas as pd
from django.apps import apps
from django.contrib.auth.models import User
from django.db import connection, models, transaction
from django.utils.translation import gettext as _
from base.context_processors import get_initial_prefix
from base.models import (
Company,
Department,
EmployeeShift,
EmployeeType,
JobPosition,
JobRole,
WorkType,
)
from employee.models import Employee, EmployeeWorkInformation
logger = logging.getLogger(__name__)
is_postgres = connection.vendor == "postgresql"
error_data_template = {
field: []
for field in [
"Badge ID",
"First Name",
"Last Name",
"Phone",
"Email",
"Gender",
"Department",
"Job Position",
"Job Role",
"Work Type",
"Shift",
"Employee Type",
"Reporting Manager",
"Company",
"Location",
"Date Joining",
"Contract End Date",
"Basic Salary",
"Salary Hour",
"Email Error",
"First Name Error",
"Name and Email Error",
"Phone Error",
"Gender Error",
"Joining Date Error",
"Contract Date Error",
"Badge ID Error",
"Basic Salary Error",
"Salary Hour Error",
"User ID Error",
"Company Error",
]
}
def chunked(iterable, size):
for i in range(0, len(iterable), size):
yield iterable[i : i + size]
def normalize_phone(phone):
phone = str(phone).strip()
if phone.startswith("+"):
return "+" + re.sub(r"\D", "", phone[1:])
return re.sub(r"\D", "", phone)
def import_valid_date(date_value, field_label, errors_dict, error_key):
if pd.isna(date_value) or date_value is None or str(date_value).strip() == "":
return None
if isinstance(date_value, datetime):
return date_value.date()
date_str = str(date_value).strip()
date_formats = ["%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y"]
for fmt in date_formats:
try:
return datetime.strptime(date_str, fmt).date()
except ValueError:
continue
errors_dict[error_key] = (
f"{field_label} is not a valid date. Expected formats: YYYY-MM-DD, DD/MM/YYYY"
)
return None
def clean_badge_id(value):
"""
Cleans and converts a badge ID value from Excel import.
- If the value is a whole number (e.g., 5480.0), returns it as an integer string ("5480").
- If the value is a decimal (e.g., 567.67), returns it as a float string ("567.67").
- If the value is a non-numeric string (e.g., "A101"), returns the stripped string.
- If the value is NaN or None, returns an empty string.
"""
if pd.isna(value):
return ""
try:
float_val = float(value)
if float_val.is_integer():
return str(int(float_val))
else:
return str(float_val)
except (ValueError, TypeError):
return str(value).strip()
def convert_nan(field, dicts):
"""
This method is returns None or field value
"""
field_value = dicts.get(field)
try:
float(field_value)
return None
except ValueError:
return field_value
def dynamic_prefix_sort(item):
"""
Sorts items based on a dynamic prefix length.
"""
# Assuming the dynamic prefix length is 3
prefix = get_initial_prefix(None)["get_initial_prefix"]
prefix_length = len(prefix) if len(prefix) >= 3 else 3
return item[:prefix_length]
def get_ordered_badge_ids():
"""
This method is used to return ordered badge ids
"""
employees = Employee.objects.entire()
data = (
employees.exclude(badge_id=None)
.order_by("badge_id")
.values_list("badge_id", flat=True)
)
if not data.first():
data = [
f'{get_initial_prefix(None)["get_initial_prefix"]}0001',
]
# Separate pure number strings and convert them to integers
pure_numbers = [int(item) for item in data if item.isdigit()]
# Remove pure number strings from the original data
data = [item for item in data if not item.isdigit()]
# Sort the remaining data by dynamic prefixes
sorted_data = sorted(data, key=dynamic_prefix_sort)
# Group the sorted data by dynamic prefixes
grouped_data = [
list(group) for _, group in groupby(sorted_data, key=dynamic_prefix_sort)
]
# Sort each subgroup alphabetically and numerically
for group in grouped_data:
group.sort()
filtered_group = [
item for item in group if any(char.isdigit() for char in item)
]
filtered_group.sort(key=lambda x: int("".join(filter(str.isdigit, x))))
# Create a list containing the first and last items from each group
result = [[group[0], group[-1]] for group in grouped_data]
# Add the list of pure numbers at the beginning
if pure_numbers:
result.insert(0, [pure_numbers[0], pure_numbers[-1]])
return result
def check_relationship_with_employee_model(model):
"""
Checks the relationship of a given model with the Employee model.
This function iterates through all the fields of the specified model
and identifies fields that are either `ForeignKey` or `ManyToManyField`
and are related to the `Employee` model. For each such field, it adds
the field name and the type of relationship to a list.
"""
related_fields = []
for field in model._meta.get_fields():
# Check if the field is a ForeignKey or ManyToManyField and related to Employee
if isinstance(field, models.ForeignKey) and field.related_model == Employee:
related_fields.append((field.name, "ForeignKey"))
elif (
isinstance(field, models.ManyToManyField)
and field.related_model == Employee
):
related_fields.append((field.name, "ManyToManyField"))
return related_fields
def valid_import_file_headers(data_frame):
if data_frame.empty:
message = _("The uploaded file is empty, Not contain records.")
return False, message
required_keys = [
"Badge ID",
"First Name",
"Last Name",
"Phone",
"Email",
"Gender",
"Department",
"Job Position",
"Job Role",
"Work Type",
"Shift",
"Employee Type",
"Reporting Manager",
"Company",
"Location",
"Date Joining",
"Contract End Date",
"Basic Salary",
"Salary Hour",
]
missing_keys = [key for key in required_keys if key not in data_frame.columns]
if missing_keys:
message = _(
"These required headers are missing in the uploaded file: "
) + ", ".join(missing_keys)
return False, message
return True, ""
def process_employee_records(data_frame):
email_regex = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
phone_regex = re.compile(r"^\+?\d{10,15}$")
allowed_genders = frozenset(choice[0] for choice in Employee.choice_gender)
existing_badge_ids = frozenset(Employee.objects.values_list("badge_id", flat=True))
existing_usernames = frozenset(User.objects.values_list("username", flat=True))
existing_name_emails = frozenset(
(fname, lname, email)
for fname, lname, email in Employee.objects.values_list(
"employee_first_name", "employee_last_name", "email"
)
)
existing_companies = frozenset(Company.objects.values_list("company", flat=True))
success_list, error_list = [], []
employee_dicts = data_frame.to_dict("records")
created_count = 0
seen_badge_ids = set(existing_badge_ids)
seen_usernames = set(existing_usernames)
seen_name_emails = set(existing_name_emails)
today = date.today()
for emp in employee_dicts:
errors = {}
save = True
email = str(emp.get("Email", "")).strip().lower()
raw_phone = emp.get("Phone", "")
phone = normalize_phone(raw_phone)
badge_id = clean_badge_id(emp.get("Badge ID"))
first_name = convert_nan("First Name", emp)
last_name = convert_nan("Last Name", emp)
gender = str(emp.get("Gender") or "").strip().lower()
company = convert_nan("Company", emp)
basic_salary = convert_nan("Basic Salary", emp)
salary_hour = convert_nan("Salary Hour", emp)
# Date validation
joining_date = import_valid_date(
emp.get("Date Joining"), "Joining Date", errors, "Joining Date Error"
)
if joining_date:
if joining_date > today:
errors["Joining Date Error"] = "Joining date cannot be in the future."
save = False
contract_end_date = import_valid_date(
emp.get("Contract End Date"),
"Contract End Date",
errors,
"Contract Date Error",
)
if contract_end_date and joining_date and contract_end_date < joining_date:
errors["Contract Date Error"] = (
"Contract end date cannot be before joining date."
)
save = False
# Email validation
if not email or not email_regex.match(email):
errors["Email Error"] = "Invalid email address."
save = False
# Name validation
if not first_name:
errors["First Name Error"] = "First name cannot be empty."
save = False
# Phone validation
if not phone_regex.match(phone):
errors["Phone Error"] = "Invalid phone number format."
save = False
# Badge ID validation
if badge_id in seen_badge_ids:
errors["Badge ID Error"] = "An employee with this badge ID already exists."
save = False
else:
# To resolve Badge ID Type Mismatch (Float vs String)
emp["Badge ID"] = badge_id
seen_badge_ids.add(badge_id)
# Username/email uniqueness
if email in seen_usernames:
errors["User ID Error"] = "User with this email already exists."
save = False
else:
seen_usernames.add(email)
# Name+email uniqueness
name_email_tuple = (first_name, last_name, email)
if name_email_tuple in seen_name_emails:
errors["Name and Email Error"] = (
"This employee already exists in the system."
)
save = False
else:
seen_name_emails.add(name_email_tuple)
# Gender validation
if gender and gender not in allowed_genders:
errors["Gender Error"] = (
f"Invalid gender. Allowed values: {', '.join(allowed_genders)}."
)
save = False
# Company validation
if company and company not in existing_companies:
errors["Company Error"] = f"Company '{company}' does not exist."
save = False
# Salary validation
if basic_salary not in [None, ""]:
try:
basic_salary_val = float(basic_salary)
if basic_salary_val <= 0:
raise ValueError
except (ValueError, TypeError):
errors["Basic Salary Error"] = "Basic salary must be a positive number."
save = False
if salary_hour not in [None, ""]:
try:
salary_hour_val = float(salary_hour)
if salary_hour_val < 0:
raise ValueError
except (ValueError, TypeError):
errors["Salary Hour Error"] = (
"Salary hour must be a non-negative number."
)
save = False
# Final processing
if save:
emp["Phone"] = phone
emp["Date Joining"] = joining_date
emp["Contract End Date"] = contract_end_date
success_list.append(emp)
created_count += 1
else:
emp.update(errors)
error_list.append(emp)
return success_list, error_list, created_count
def bulk_create_user_import(success_lists):
"""
Creates new User instances in bulk from a list of dictionaries containing user data.
Returns:
list: A list of created User instances. If no new users are created, returns an empty list.
"""
emails = [row["Email"] for row in success_lists]
existing_usernames = (
set(User.objects.filter(username__in=emails).values_list("username", flat=True))
if is_postgres
else set(
chain.from_iterable(
User.objects.filter(username__in=chunk).values_list(
"username", flat=True
)
for chunk in chunked(emails, 999)
)
)
)
users_to_create = [
User(
username=row["Email"],
email=row["Email"],
password=str(row["Phone"]).strip(),
is_superuser=False,
)
for row in success_lists
if row["Email"] not in existing_usernames
]
created_users = []
if users_to_create:
with transaction.atomic():
created_users = User.objects.bulk_create(
users_to_create, batch_size=None if is_postgres else 999
)
return created_users
def bulk_create_employee_import(success_lists):
"""
Creates Employee instances in bulk based on imported data.
Uses adaptive chunking for compatibility with SQLite, avoids chunking in PostgreSQL.
"""
emails = [row["Email"] for row in success_lists]
is_postgres = connection.vendor == "postgresql"
existing_users = {
user.username: user
for user in (
User.objects.filter(username__in=emails).only("id", "username")
if is_postgres
else chain.from_iterable(
User.objects.filter(username__in=chunk).only("id", "username")
for chunk in chunked(emails, 999)
)
)
}
employees_to_create = [
Employee(
employee_user_id=existing_users[row["Email"]],
badge_id=row["Badge ID"],
employee_first_name=convert_nan("First Name", row),
employee_last_name=convert_nan("Last Name", row),
email=row["Email"],
phone=row["Phone"],
gender=row.get("Gender", "").lower(),
)
for row in success_lists
if row["Email"] in existing_users
]
created_employees = []
if employees_to_create:
with transaction.atomic():
created_employees = Employee.objects.bulk_create(
employees_to_create, batch_size=None if is_postgres else 999
)
return created_employees
def set_initial_password(employees):
"""
method to set initial password
"""
logger.info("started to set initial password")
for employee in employees:
try:
employee.employee_user_id.set_password(str(employee.phone))
employee.employee_user_id.save()
except Exception as e:
logger.error(f"falied to set initial password for {employee}")
logger.info("initial password configured")
def optimize_reporting_manager_lookup():
"""
Optimizes the lookup of reporting managers from a list of work information.
This function identifies unique reporting manager names from the provided
list of work information, queries all matching `Employee` objects in a
single database query, and creates a dictionary for quick lookups based
on the full name of the reporting managers.
"""
employees = Employee.objects.entire()
employee_dict = {
f"{employee.employee_first_name} {employee.employee_last_name}": employee
for employee in employees
}
return employee_dict
def bulk_create_department_import(success_lists):
"""
Bulk creation of department instances based on the excel import of employees
"""
departments_to_import = {
dept
for work_info in success_lists
if (dept := convert_nan("Department", work_info))
}
existing_departments = set(Department.objects.values_list("department", flat=True))
new_departments = [
Department(department=dept)
for dept in departments_to_import - existing_departments
]
if new_departments:
with transaction.atomic():
Department.objects.bulk_create(
new_departments, batch_size=None if is_postgres else 999
)
def bulk_create_job_position_import(success_lists):
"""
Optimized: Bulk creation of job position instances based on the Excel import of employees.
"""
# Step 1: Extract unique (job_position, department_name) pairs
job_positions_to_import = {
(convert_nan("Job Position", item), convert_nan("Department", item))
for item in success_lists
if convert_nan("Job Position", item) and convert_nan("Department", item)
}
if not job_positions_to_import:
return # No valid data to import
# Step 2: Fetch all departments at once and build a name -> object map
department_objs = Department.objects.only("id", "department")
department_lookup = {dep.department: dep for dep in department_objs}
# Step 3: Filter out entries with unknown departments
valid_pairs = [
(jp, department_lookup[dept])
for jp, dept in job_positions_to_import
if dept in department_lookup
]
if not valid_pairs:
return # No valid (job_position, department_id) pairs to process
# Step 4: Fetch existing job positions
existing_pairs = set(
JobPosition.objects.filter(
department_id__in={dept_id for _, dept_id in valid_pairs}
).values_list("job_position", "department_id")
)
# Step 5: Create list of new JobPosition instances
new_positions = [
JobPosition(job_position=jp, department_id=dept_id)
for jp, dept_id in valid_pairs
if (jp, dept_id) not in existing_pairs
]
# Step 6: Bulk create in a transaction
if new_positions:
with transaction.atomic():
JobPosition.objects.bulk_create(
new_positions, batch_size=None if is_postgres else 999
)
def bulk_create_job_role_import(success_lists):
"""
Bulk creation of job role instances based on the excel import of employees
"""
# Extract unique (job_role, job_position) pairs, filtering out empty values
job_roles_to_import = {
(role, pos)
for work_info in success_lists
if (role := convert_nan("Job Role", work_info))
and (pos := convert_nan("Job Position", work_info))
}
# Prefetch existing data efficiently
job_positions = JobPosition.objects.only("id", "job_position")
existing_job_roles = set(JobRole.objects.values_list("job_role", "job_position_id"))
# Create new job roles
new_job_roles = [
JobRole(job_role=role, job_position_id=job_positions[pos].id)
for role, pos in job_roles_to_import
if pos in job_positions
and (role, job_positions[pos].id) not in existing_job_roles
]
# Bulk create if there are new roles
if new_job_roles:
with transaction.atomic():
JobRole.objects.bulk_create(
new_job_roles, batch_size=None if is_postgres else 999
)
def bulk_create_work_types(success_lists):
"""
Bulk creation of work type instances based on the excel import of employees
"""
# Extract unique work types, filtering out None values
work_types_to_import = {
wt for work_info in success_lists if (wt := convert_nan("Work Type", work_info))
}
# Get existing work types in one optimized query
existing_work_types = set(WorkType.objects.values_list("work_type", flat=True))
# Create new work type objects
new_work_types = [
WorkType(work_type=wt) for wt in work_types_to_import - existing_work_types
]
# Bulk create if there are new work types
if new_work_types:
with transaction.atomic():
WorkType.objects.bulk_create(
new_work_types, batch_size=None if is_postgres else 999
)
def bulk_create_shifts(success_lists):
"""
Bulk creation of shift instances based on the excel import of employees
"""
# Extract unique shifts, filtering out None values
shifts_to_import = {
shift
for work_info in success_lists
if (shift := convert_nan("Shift", work_info))
}
# Get existing shifts in one optimized query
existing_shifts = set(
EmployeeShift.objects.values_list("employee_shift", flat=True)
)
# Create new shift objects
new_shifts = [
EmployeeShift(employee_shift=shift)
for shift in shifts_to_import - existing_shifts
]
# Bulk create if there are new shifts
if new_shifts:
with transaction.atomic():
EmployeeShift.objects.bulk_create(
new_shifts, batch_size=None if is_postgres else 999
)
def bulk_create_employee_types(success_lists):
"""
Bulk creation of employee type instances based on the excel import of employees
"""
# Extract unique employee types, filtering out None values
employee_types_to_import = {
et
for work_info in success_lists
if (et := convert_nan("Employee Type", work_info))
}
# Get existing employee types in one optimized query
existing_employee_types = set(
EmployeeType.objects.values_list("employee_type", flat=True)
)
# Create new employee type objects
new_employee_types = [
EmployeeType(employee_type=et)
for et in employee_types_to_import - existing_employee_types
]
# Bulk create if there are new types
if new_employee_types:
with transaction.atomic():
EmployeeType.objects.bulk_create(
new_employee_types, batch_size=None if is_postgres else 999
)
def create_contracts_in_thread(new_work_info_list, update_work_info_list):
"""
Creates employee contracts in bulk based on provided work information.
"""
from payroll.models.models import Contract
def get_or_none(value):
return value if value else None
contracts_list = [
Contract(
contract_name=f"{work_info.employee_id}'s Contract",
employee_id=work_info.employee_id,
contract_start_date=(
work_info.date_joining if work_info.date_joining else datetime.today()
),
department=get_or_none(work_info.department_id),
job_position=get_or_none(work_info.job_position_id),
job_role=get_or_none(work_info.job_role_id),
shift=get_or_none(work_info.shift_id),
work_type=get_or_none(work_info.work_type_id),
wage=work_info.basic_salary or 0,
)
for work_info in new_work_info_list + update_work_info_list
if work_info.employee_id
]
Contract.objects.bulk_create(contracts_list)
def bulk_create_work_info_import(success_lists):
"""
Bulk creation of employee work info instances based on the excel import of employees
"""
new_work_info_list = []
update_work_info_list = []
badge_ids = [row["Badge ID"] for row in success_lists]
departments = set(row.get("Department") for row in success_lists)
job_positions = set(row.get("Job Position") for row in success_lists)
job_roles = set(row.get("Job Role") for row in success_lists)
work_types = set(row.get("Work Type") for row in success_lists)
employee_types = set(row.get("Employee Type") for row in success_lists)
shifts = set(row.get("Shift") for row in success_lists)
companies = set(row.get("Company") for row in success_lists)
chunk_size = None if is_postgres else 999
employee_qs = (
chain.from_iterable(
Employee.objects.entire().filter(badge_id__in=chunk).only("badge_id")
for chunk in chunked(badge_ids, chunk_size)
)
if chunk_size
else Employee.objects.entire().filter(badge_id__in=badge_ids).only("badge_id")
)
existing_employees = {emp.badge_id: emp for emp in employee_qs}
existing_employee_work_infos = {
emp.employee_id: emp
for emp in (
EmployeeWorkInformation.objects.filter(
employee_id__in=existing_employees.values()
).only("employee_id")
if is_postgres
else chain.from_iterable(
EmployeeWorkInformation.objects.filter(employee_id__in=chunk).only(
"employee_id"
)
for chunk in chunked(list(existing_employees.values()), 900)
)
)
}
existing_departments = {
dep.department: dep
for dep in Department.objects.filter(department__in=departments).only(
"department"
)
}
existing_job_positions = {
(jp.department_id, jp.job_position): jp
for jp in JobPosition.objects.filter(job_position__in=job_positions)
.select_related("department_id")
.only("department_id", "job_position")
}
existing_job_roles = {
(jr.job_position_id, jr.job_role): jr
for jr in JobRole.objects.filter(job_role__in=job_roles)
.select_related("job_position_id")
.only("job_position_id", "job_role")
}
existing_work_types = {
wt.work_type: wt
for wt in WorkType.objects.filter(work_type__in=work_types).only("work_type")
}
existing_shifts = {
es.employee_shift: es
for es in EmployeeShift.objects.filter(employee_shift__in=shifts).only(
"employee_shift"
)
}
existing_employee_types = {
et.employee_type: et
for et in EmployeeType.objects.filter(employee_type__in=employee_types).only(
"employee_type"
)
}
existing_companies = {
comp.company: comp
for comp in Company.objects.filter(company__in=companies).only("company")
}
reporting_manager_dict = optimize_reporting_manager_lookup()
for work_info in success_lists:
badge_id = work_info["Badge ID"]
employee_obj = existing_employees.get(badge_id)
if not employee_obj:
continue
email = work_info["Email"]
employee_work_info = existing_employee_work_infos.get(employee_obj)
department_obj = existing_departments.get(work_info.get("Department"))
job_position_key = (
existing_departments.get(work_info.get("Department")),
work_info.get("Job Position"),
)
job_position_obj = existing_job_positions.get(job_position_key)
job_role_key = (
job_position_obj,
work_info.get("Job Role"),
)
job_role_obj = existing_job_roles.get(job_role_key)
work_type_obj = existing_work_types.get(work_info.get("Work Type"))
employee_type_obj = existing_employee_types.get(work_info.get("Employee Type"))
shift_obj = existing_shifts.get(work_info.get("Shift"))
reporting_manager = work_info.get("Reporting Manager")
reporting_manager_obj = None
if isinstance(reporting_manager, str) and " " in reporting_manager:
if reporting_manager in reporting_manager_dict:
reporting_manager_obj = reporting_manager_dict[reporting_manager]
company_obj = existing_companies.get(work_info.get("Company"))
location = work_info.get("Location")
# Parsing dates and salary
date_joining = (
work_info["Date Joining"]
if not pd.isnull(work_info["Date Joining"])
else datetime.today()
)
contract_end_date = (
work_info["Contract End Date"]
if not pd.isnull(work_info["Contract End Date"])
else None
)
basic_salary = (
convert_nan("Basic Salary", work_info)
if type(convert_nan("Basic Salary", work_info)) is int
else 0
)
salary_hour = (
convert_nan("Salary Hour", work_info)
if type(convert_nan("Salary Hour", work_info)) is int
else 0
)
if employee_work_info is None:
# Create a new instance
employee_work_info = EmployeeWorkInformation(
employee_id=employee_obj,
email=email,
department_id=department_obj,
job_position_id=job_position_obj,
job_role_id=job_role_obj,
work_type_id=work_type_obj,
employee_type_id=employee_type_obj,
shift_id=shift_obj,
reporting_manager_id=reporting_manager_obj,
company_id=company_obj,
location=location,
date_joining=(
date_joining if not pd.isnull(date_joining) else datetime.today()
),
contract_end_date=(
contract_end_date if not pd.isnull(contract_end_date) else None
),
basic_salary=basic_salary,
salary_hour=salary_hour,
)
new_work_info_list.append(employee_work_info)
else:
# Update the existing instance
employee_work_info.email = email
employee_work_info.department_id = department_obj
employee_work_info.job_position_id = job_position_obj
employee_work_info.job_role_id = job_role_obj
employee_work_info.work_type_id = work_type_obj
employee_work_info.employee_type_id = employee_type_obj
employee_work_info.shift_id = shift_obj
employee_work_info.reporting_manager_id = reporting_manager_obj
employee_work_info.company_id = company_obj
employee_work_info.location = location
employee_work_info.date_joining = (
date_joining if not pd.isnull(date_joining) else datetime.today()
)
employee_work_info.contract_end_date = (
contract_end_date if not pd.isnull(contract_end_date) else None
)
employee_work_info.basic_salary = basic_salary
employee_work_info.salary_hour = salary_hour
update_work_info_list.append(employee_work_info)
if new_work_info_list:
EmployeeWorkInformation.objects.bulk_create(
new_work_info_list, batch_size=None if is_postgres else 999
)
if update_work_info_list:
EmployeeWorkInformation.objects.bulk_update(
update_work_info_list,
[
"email",
"department_id",
"job_position_id",
"job_role_id",
"work_type_id",
"employee_type_id",
"shift_id",
"reporting_manager_id",
"company_id",
"location",
"date_joining",
"contract_end_date",
"basic_salary",
"salary_hour",
],
batch_size=None if is_postgres else 999,
)
if apps.is_installed("payroll"):
contract_creation_thread = threading.Thread(
target=create_contracts_in_thread,
args=(new_work_info_list, update_work_info_list),
)
contract_creation_thread.start()