diff --git a/msp/hooks.py b/msp/hooks.py index 3195ce5..ad1895b 100644 --- a/msp/hooks.py +++ b/msp/hooks.py @@ -96,9 +96,15 @@ doc_events = { "x509_certificate": { "before_save": "msp.certificate_methods.process_certficate_data", "after_insert": "msp.certificate_methods.set_doctype_name" - } + }, + 'Service Report': { + 'before_save': 'msp.tools.save_backlinks', + 'on_cancel': 'msp.tools.handle_backlinks', + 'on_trash': 'msp.tools.handle_backlinks' + } } + # Scheduled Tasks # --------------- diff --git a/msp/tools.py b/msp/tools.py index 3ab2936..fda9044 100644 --- a/msp/tools.py +++ b/msp/tools.py @@ -83,44 +83,155 @@ def get_service_report_work(employee, from_date, to_date): return result +# def get_ticket_work_hours(employee, from_date, to_date): +# if isinstance(from_date, str): +# from_date = datetime.strptime(from_date, "%Y-%m-%d") +# if isinstance(to_date, str): +# to_date = datetime.strptime(to_date, "%Y-%m-%d") + +# to_date = to_date + timedelta(days=1) - timedelta(seconds=1) + +# user = frappe.get_all("OTRSConnect User", filters={"erpnext_employee": employee}, fields=["id"]) +# if not user: +# return [] +# user_id = user[0].id +# print(user_id) + +# result = frappe.db.sql(""" +# SELECT `create_time`, `time_unit` +# FROM `tabOTRSConnect Article` +# WHERE +# create_time BETWEEN %s AND %s +# AND create_by = %s; +# """, (from_date, to_date, user_id), as_dict=True) + +# # List for saving the processed ticket data +# ticket_hours = [] + +# for item in result: + +# # Calculate the hours and working times (15 minutes equals 0.25 hours) +# qty = float(item['time_unit']) / 4.0 +# work_begin = item['create_time'] - timedelta(hours=qty) + +# # Create the work item dictionary +# work_item = { +# "employee": employee, +# "begin": work_begin, +# "end": item['create_time'], +# "hours": qty +# } + + +# ticket_hours.append(work_item) + +# return ticket_hours + + +# def get_ticket_work_hours(employee, from_date, to_date): +# if isinstance(from_date, str): +# from_date = datetime.strptime(from_date, "%Y-%m-%d") +# if isinstance(to_date, str): +# to_date = datetime.strptime(to_date, "%Y-%m-%d") + +# to_date = to_date + timedelta(days=1) - timedelta(seconds=1) + +# user = frappe.get_all("OTRSConnect User", filters={"erpnext_employee": employee}, fields=["id"]) +# if not user: +# return [] +# user_id = user[0].id +# print(user_id) + +# # Fetch all article IDs that are already considered in service reports within the given date range +# considered_articles = frappe.db.sql(""" +# SELECT otrs_article +# FROM `tabService Report Work` +# WHERE otrs_article IS NOT NULL AND `begin` BETWEEN %s AND %s +# """, (from_date, to_date), as_list=True) + +# # Flatten the list of considered article IDs +# considered_article_ids = [article[0] for article in considered_articles] + +# # Fetch OTRSConnect Articles, excluding those already in service reports +# result = frappe.db.sql(""" +# SELECT `id`, `create_time`, `time_unit` +# FROM `tabOTRSConnect Article` +# WHERE +# create_time BETWEEN %s AND %s +# AND create_by = %s +# AND id NOT IN %s; +# """, (from_date, to_date, user_id, tuple(considered_article_ids)), as_dict=True) + +# # List for saving the processed ticket data +# ticket_hours = [] + +# for item in result: +# # Calculate the hours and working times (15 minutes equals 0.25 hours) +# qty = float(item['time_unit']) / 4.0 +# work_begin = item['create_time'] - timedelta(hours=qty) + +# # Create the work item dictionary +# work_item = { +# "employee": employee, +# "begin": work_begin, +# "end": item['create_time'], +# "hours": qty +# } + +# ticket_hours.append(work_item) + +# return ticket_hours +from datetime import datetime, timedelta +import frappe + def get_ticket_work_hours(employee, from_date, to_date): + if isinstance(from_date, str): + from_date = datetime.strptime(from_date, "%Y-%m-%d") + if isinstance(to_date, str): + to_date = datetime.strptime(to_date, "%Y-%m-%d") - # Get the OTRSConnect settings - settings = frappe.get_doc("OTRSConnect Settings") - otrsdb = get_db(host=settings.otrs_host, user=settings.db_user, password=settings.db_password) - otrsdb.connect() - otrsdb.use(settings.db_name) + to_date = to_date + timedelta(days=1) - timedelta(seconds=1) - # Get the OTRSConnect user via the LinkField erpnext_employee user = frappe.get_all("OTRSConnect User", filters={"erpnext_employee": employee}, fields=["id"]) if not user: return [] user_id = user[0].id + print(user_id) - # Define SQL query - sql = """ - SELECT - ticket.id, ticket.user_id, ticket.create_time, - time_accounting.time_unit - FROM - ticket - LEFT JOIN - time_accounting ON time_accounting.ticket_id = ticket.id - WHERE - ticket.ticket_state_id = 4 - AND time_accounting.time_unit > 0 - AND ticket.create_time BETWEEN %(from_date)s AND %(to_date)s - AND ticket.user_id = %(user_id)s; + # Fetch all article IDs that are already considered in service reports within the given date range + considered_articles = frappe.db.sql(""" + SELECT otrs_article + FROM `tabService Report Work` + WHERE otrs_article IS NOT NULL AND `begin` BETWEEN %s AND %s + """, (from_date, to_date), as_list=True) + + # Flatten the list of considered article IDs + considered_article_ids = [article[0] for article in considered_articles] + + # Build the SQL query for OTRSConnect Articles + sql_query = """ + SELECT `id`, `create_time`, `time_unit` + FROM `tabOTRSConnect Article` + WHERE + create_time BETWEEN %s AND %s + AND create_by = %s """ + + # Add the NOT IN condition if there are considered_article_ids + if considered_article_ids: + sql_query += " AND id NOT IN %s" - # Execute the query with parameter transfer - ticket_entries = otrsdb.sql(sql, {'from_date': from_date, 'to_date': to_date, 'user_id': user_id}, as_dict=True) - + # Execute the query + result = frappe.db.sql( + sql_query, + (from_date, to_date, user_id) + (tuple(considered_article_ids),) if considered_article_ids else (from_date, to_date, user_id), + as_dict=True + ) + # List for saving the processed ticket data ticket_hours = [] - for item in ticket_entries: - + for item in result: # Calculate the hours and working times (15 minutes equals 0.25 hours) qty = float(item['time_unit']) / 4.0 work_begin = item['create_time'] - timedelta(hours=qty) @@ -133,13 +244,14 @@ def get_ticket_work_hours(employee, from_date, to_date): "hours": qty } - ticket_hours.append(work_item) return ticket_hours + + @frappe.whitelist() def get_target_hours(employee, from_date, to_date): from_date = from_date.date() @@ -280,6 +392,217 @@ def get_act_stock(name): # return result +def get_otrsdb_connection(): + settings = frappe.get_doc("OTRSConnect Settings") + otrsdb = get_db(host=settings.otrs_host, user=settings.db_user, password=settings.db_password) + otrsdb.connect() + otrsdb.use(settings.db_name) + return otrsdb, settings + +def get_tickets_from_otrsdb(otrsdb, condition): + sql = f""" + SELECT DISTINCT ticket.id, ticket.tn, ticket.title, + ticket.queue_id, ticket.user_id, ticket.responsible_user_id, + ticket.ticket_priority_id, ticket.customer_id, ticket.customer_user_id, + ticket.ticket_state_id, ticket.create_time, ticket.create_by, + ticket.change_time, ticket.change_by + FROM ticket + LEFT JOIN users ON ticket.user_id=users.id + LEFT JOIN customer_company ON customer_company.customer_id=ticket.customer_id + LEFT JOIN time_accounting ON time_accounting.ticket_id=ticket.id + WHERE time_accounting.time_unit > 0 + AND {condition} + """ + return otrsdb.sql(sql, as_dict=1) + +def get_articles_from_otrsdb(otrsdb, last_article_id): + sql = f""" + SELECT article.id, article.ticket_id, article.create_time, + article.create_by, article_data_mime.a_from, article_data_mime.a_to, + article_data_mime.a_subject, article_data_mime.a_body, + time_accounting.time_unit + FROM article + LEFT JOIN article_data_mime ON article.id=article_data_mime.id + LEFT JOIN time_accounting time_accounting ON article.id=time_accounting.article_id + WHERE time_accounting.time_unit > 0 + AND article.id > {last_article_id} + """ + return otrsdb.sql(sql, as_dict=1) + +@frappe.whitelist() +def update_tickets_and_articles(): + otrsdb, settings = get_otrsdb_connection() + sync_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Ensure settings.last_ticket_sync has a default value + last_ticket_sync = settings.last_ticket_sync or '2024-07-01T00:00:00' + + new_tickets = get_tickets_from_otrsdb(otrsdb, f"ticket.id > {int(settings.last_ticket_id)}") + print(len(new_tickets)) + updated_tickets = get_tickets_from_otrsdb(otrsdb, f"ticket.change_time > '{last_ticket_sync}'") + print(len(updated_tickets)) + articles = get_articles_from_otrsdb(otrsdb, int(settings.last_article_id)) + print(len(articles)) + + messages = [] + if new_tickets: + messages.append(f"Found {len(new_tickets)} new tickets.") + else: + messages.append("No new tickets found.") + + if updated_tickets: + messages.append(f"Found {len(updated_tickets)} updated tickets.") + else: + messages.append("No updated tickets found.") + + if articles: + messages.append(f"Found {len(articles)} new articles.") + else: + messages.append("No new articles found.") + + # Display messages to the user + for message in messages: + frappe.msgprint(message) + + # Process and store new and updated tickets + for ticket in new_tickets + updated_tickets: + process_ticket(ticket) + frappe.db.commit() + # Update last_ticket_id and last_ticket_sync based on the processed tickets + if new_tickets: + settings.last_ticket_id = max([int(t['id']) for t in new_tickets] + [int(settings.last_ticket_id)]) + settings.last_ticket_sync = sync_time + + # Process and store articles + for article in articles: + process_article(article) + frappe.db.commit() + # Update last_article_id based on the processed articles + if articles: + settings.last_article_id = max([int(a['id']) for a in articles] + [int(settings.last_article_id)]) + + settings.last_article_sync = sync_time + + settings.save() + frappe.db.commit() + +def process_ticket(ticket): + ERPNext_tickets = frappe.get_all("OTRSConnect Ticket", filters={"id": ticket["id"]}) + + if not ERPNext_tickets: + frappe_doctype_dict = {"doctype": "OTRSConnect Ticket", "status": "fetched"} + ticket["id"] = str(ticket["id"]) + frappe_doctype_dict.update(ticket) + ticket_doc = frappe.get_doc(frappe_doctype_dict) + ticket_doc.insert() + link_ERPNext_OTRS_Ticket(ticket_doc) + else: + existing_ticket = frappe.get_doc("OTRSConnect Ticket", ERPNext_tickets[0].name) + read_only_fields = {"create_time", "create_by"} + for key, value in ticket.items(): + if key not in read_only_fields: + setattr(existing_ticket, key, value) + existing_ticket.save(ignore_permissions=True, ignore_version=True) + +def process_article(article): + # Ensure the referenced ticket exists + ticket_exists = frappe.db.exists("OTRSConnect Ticket", {"id": article["ticket_id"]}) + + if not ticket_exists: + # Fetch the ticket from OTRS and insert it into ERPNext + otrsdb, _ = get_otrsdb_connection() + ticket = get_tickets_from_otrsdb(otrsdb, f"ticket.id = {article['ticket_id']}") + if ticket: + process_ticket(ticket[0]) + + # Check again if the ticket now exists + ticket_exists = frappe.db.exists("OTRSConnect Ticket", {"id": article["ticket_id"]}) + if ticket_exists: + ERPNext_articles = frappe.get_all("OTRSConnect Article", filters={"id": article["id"]}) + + if not ERPNext_articles: + # Article doesn't exist, create a new one + frappe_doctype_dict = {"doctype": "OTRSConnect Article"} + article["id"] = str(article["id"]) + article["ticket_id"] = str(article["ticket_id"]) + frappe_doctype_dict.update(article) + article_doc = frappe.get_doc(frappe_doctype_dict) + article_doc.insert() + else: + print(f"Warning: Referenced ticket ID {article['ticket_id']} does not exist in ERPNext.") + +def link_ERPNext_OTRS_Ticket(OTRSConnect_Ticket): + + if OTRSConnect_Ticket.customer_id == None: + frappe.msgprint("Keine Kundenummer vorhanden für: " + str(OTRSConnect_Ticket.title) + "
" + str(OTRSConnect_Ticket.tn)) + return False + + if OTRSConnect_Ticket.customer_id == "": + frappe.msgprint("Kundennummerzuweisung nicht eindeutig möglich für: " + str(OTRSConnect_Ticket.title) + "
" + str(OTRSConnect_Ticket.id)) + return False + naming_series = "CUST-" + str(OTRSConnect_Ticket.customer_id) + customers_for_customer_id = frappe.get_all("Customer", filters={"naming_series": naming_series}) + if len(customers_for_customer_id) == 1: + OTRSConnect_Ticket.erpnext_customer = naming_series + OTRSConnect_Ticket.save() + else: + frappe.msgprint("Kundennummerzuweisung nicht eindeutig möglich für: " + str(OTRSConnect_Ticket.customer_id) + "
" + str(OTRSConnect_Ticket.title) + "
" + str(OTRSConnect_Ticket.id)) + +@frappe.whitelist() +def save_backlinks(doc, method): + service_report = doc + + if hasattr(service_report, 'work'): + for item in service_report.work: + if hasattr(item, 'otrs_article'): + service_report_article = item.name + article_name = item.otrs_article + articles = frappe.get_all('OTRSConnect Article', filters={'name': article_name}, fields=['name']) + + for article in articles: + article_doc = frappe.get_doc('OTRSConnect Article', article.name) + updated = False + + if not article_doc.service_report: + article_doc.service_report = service_report.name + updated = True + + if not article_doc.service_report_work_item: + article_doc.service_report_work_item = service_report_article + updated = True + + if updated: + article_doc.save() + frappe.db.commit() + +def handle_backlinks(doc, method): + if method == "on_trash": + clear_backlinks(doc) + elif method == "on_cancel": + clear_backlinks(doc) + +def clear_backlinks(doc): + if hasattr(doc, 'work'): + for item in doc.work: + if hasattr(item, 'otrs_article'): + article_name = item.otrs_article + articles = frappe.get_all('OTRSConnect Article', filters={'name': article_name, 'service_report': doc.name}, fields=['name']) + + for article in articles: + article_doc = frappe.get_doc('OTRSConnect Article', article.name) + updated = False + + if article_doc.service_report == doc.name: + article_doc.service_report = None + updated = True + + if article_doc.service_report_work_item == item.name: + article_doc.service_report_work_item = None + updated = True + + if updated: + article_doc.save() + frappe.db.commit()