first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from . import ks_gantt_view_project_import
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
import tempfile
import binascii
import datetime
import logging
import json
import dateutil.parser
import pandas as pd
from odoo import models, fields, _
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class KsGanttViewImport(models.TransientModel):
_name = 'ks.gantt.import.wizard'
_description = 'Ks Project Import Wizard'
ks_file_type = fields.Selection(
selection=[('xlsx', 'Excel'), ('json', 'JSON')],
string='File Type',
default='xlsx',
required=True,
)
ks_file = fields.Binary(string='Upload File', required=True)
def ks_action_import(self):
if self.ks_file_type == 'xlsx':
self.ks_import_xlsx_file()
elif self.ks_file_type == 'json':
self.ks_import_json_file()
return {'type': 'ir.actions.client', 'tag': 'reload'}
def ks_import_xlsx_file(self):
"""Parse an XLSX export and recreate tasks + task links."""
fp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
fp.write(binascii.a2b_base64(self.ks_file))
fp.seek(0)
try:
df = pd.read_excel(fp.name, engine='openpyxl')
except Exception:
raise ValidationError(
_("File can't be read — please upload a valid Excel file.")
)
df = df.astype(str)
column = df.columns.tolist()
ks_sheet_columns = list(column)
# These special columns are handled separately
for col in ['id', 'ks_target_task_id', 'ks_task_link_type']:
if col in ks_sheet_columns:
ks_sheet_columns.remove(col)
ks_project_dict = {}
ks_project_task_obj = self.env['project.task'].sudo()
updated_task_ids = {}
for row_no in range(df.shape[0]):
row = list(df.iloc[row_no])
row_id = row.pop(0) # 'id' column
if row[0] == 'False': # skip blank task rows (link-only rows)
continue
row = row[:-2] # drop ks_target_task_id, ks_task_link_type
ks_task_write_val = {}
for index, val in enumerate(ks_sheet_columns):
if val == 'project_id':
project_name = row[index]
if project_name not in ks_project_dict:
new_project = self.env['project.project'].sudo().create({
'name': project_name + ' ' + str(datetime.datetime.now())
})
ks_project_dict[project_name] = new_project.id
ks_task_write_val[val] = ks_project_dict[project_name]
elif ks_project_task_obj._fields[val].type in ['char', 'selection']:
ks_task_write_val[val] = row[index]
elif ks_project_task_obj._fields[val].type == 'many2one':
ks_task_write_val[val] = self.ks_valid_manny_to_one_data(
ks_project_task_obj._fields[val].comodel_name, row[index]
)
elif ks_project_task_obj._fields[val].type in ['datetime', 'date']:
try:
# Odoo 19: avoid deprecated fields.Datetime.from_string
ks_task_write_val[val] = pd.to_datetime(row[index]).to_pydatetime()
except Exception:
ks_task_write_val[val] = False
elif ks_project_task_obj._fields[val].type == 'boolean':
ks_task_write_val[val] = row[index] != 'False'
else:
_logger.warning(
"Field type '%s' not supported for import.",
ks_project_task_obj._fields[val].type,
)
created_task = ks_project_task_obj.create(ks_task_write_val)
updated_task_ids[int(row_id)] = created_task.id
# Reconstruct task links from the link columns
df.dropna(subset=['ks_target_task_id', 'ks_task_link_type'], how='all', inplace=True)
grouped = df.groupby('id').agg({
'ks_target_task_id': list,
'ks_task_link_type': list,
}).reset_index()
for _, row in grouped.iterrows():
id_val = int(row['id'])
target_task_ids = row['ks_target_task_id']
task_link_types = row['ks_task_link_type']
if target_task_ids != ['nan'] and task_link_types != ['nan']:
for i in range(len(target_task_ids)):
self.env['ks.task.link'].sudo().create({
'ks_source_task_id': updated_task_ids[id_val],
'ks_target_task_id': updated_task_ids[int(float(target_task_ids[i]))],
'ks_task_link_type': str(int(float(task_link_types[i]))),
})
def ks_import_json_file(self):
"""Parse a JSON Gantt export and recreate tasks + task links."""
fp = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
fp.write(binascii.a2b_base64(self.ks_file))
fp.seek(0)
try:
parsed_data = json.load(fp)
except Exception:
raise ValidationError(
_("File can't be read — please upload a valid JSON file.")
)
config = parsed_data.get('config', {})
task_data_block = config.get('ks_gantt_task_data', {}).get('data')
if not task_data_block:
raise ValidationError(
_('Required data not found in the JSON file. Please upload a correct JSON file.')
)
ks_project_task_obj = self.env['project.task'].sudo()
ks_project_dict = {}
filtered_task_data = [rec for rec in task_data_block if rec.get('type') != 'project']
# Odoo 19 fix: eval() on arbitrary DB content is a security risk.
# Use json.loads() instead — the field stores valid JSON.
link_task_data = []
for rec in task_data_block:
raw_links = rec.get('ks_task_link_ids', '[]')
if raw_links and raw_links != '[]':
try:
link_task_data.append(json.loads(raw_links))
except (json.JSONDecodeError, TypeError):
_logger.warning("Could not parse ks_task_link_ids: %s", raw_links)
updated_task_ids = {}
for task in filtered_task_data:
if not task:
continue
ks_task_write_val = {}
for key, value in self.ks_gantt_field_mapping().items():
if value == 'project_id':
if task.get(key) and task[key][1]:
proj_name = task[key][1]
if proj_name not in ks_project_dict:
new_project = self.env['project.project'].sudo().create({
'name': proj_name + ' ' + str(datetime.datetime.now())
})
ks_project_dict[proj_name] = new_project.id
ks_task_write_val[value] = ks_project_dict[proj_name]
elif ks_project_task_obj._fields[value].type in ['char', 'selection']:
ks_task_write_val[value] = task.get(key)
elif ks_project_task_obj._fields[value].type == 'many2one':
raw = task.get(key)
ks_task_write_val[value] = (
self.ks_valid_manny_to_one_data(
ks_project_task_obj._fields[value].comodel_name, raw[1]
)
if raw and raw[1] else False
)
elif ks_project_task_obj._fields[value].type in ['datetime', 'date']:
raw = task.get(key)
if raw:
# Odoo 19: fields.Datetime.from_string removed — parse directly.
parsed = dateutil.parser.parse(str(raw))
naive = parsed.replace(tzinfo=None)
ks_task_write_val[value] = datetime.datetime.strptime(
naive.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S'
)
else:
ks_task_write_val[value] = False
elif ks_project_task_obj._fields[value].type == 'boolean':
ks_task_write_val[value] = bool(task.get(key))
else:
_logger.warning(
"Field type '%s' not supported for import.",
ks_project_task_obj._fields[value].type,
)
created_task = ks_project_task_obj.create(ks_task_write_val)
updated_task_ids[task['id']] = created_task.id
for task_links in link_task_data:
for link in task_links:
try:
source_id = updated_task_ids[link['source']]
target_id = updated_task_ids[link['target']]
except KeyError:
source_id = updated_task_ids.get('task_' + str(link['source']))
target_id = updated_task_ids.get('task_' + str(link['target']))
if source_id and target_id:
self.env['ks.task.link'].sudo().create({
'ks_source_task_id': source_id,
'ks_target_task_id': target_id,
'ks_task_link_type': link['type'],
'ks_lag_days': link.get('lag', 0),
})
def ks_gantt_field_mapping(self):
return {
'text': 'name',
'mark_as_important': 'priority',
'project_id': 'project_id',
'ks_owner_task': 'user_ids',
'partner_id': 'partner_id',
'ks_deadline_tooltip': 'date_deadline',
'unscheduled': 'ks_task_unschedule',
'type': 'ks_task_type',
'ks_enable_task_duration': 'ks_enable_task_duration',
'start_date': 'ks_start_datetime',
'end_date': 'ks_end_datetime',
'ks_schedule_mode': 'ks_schedule_mode',
'constraint_type': 'ks_constraint_task_type',
'constraint_date': 'ks_constraint_task_date',
'stage_id': 'stage_id',
'planned_hours': 'planned_hours',
}
def ks_valid_manny_to_one_data(self, comodel, value):
"""Resolve a display name to a record id, or return False."""
if not value:
return False
model_obj = self.env[comodel].sudo()
ks_domain = []
if 'name' in model_obj._fields:
ks_domain = [('name', '=', value)]
else:
ks_domain = [('display_name', '=', value)]
record = model_obj.search(ks_domain, limit=1)
return record.id if record else False