first push message
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import ks_gantt_view_project_import
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Reference in New Issue
Block a user