import tkinter as tk from tkinter import ttk, filedialog, messagebox import re from datetime import datetime, date import os import openpyxl from openpyxl.styles import Font, Alignment from openpyxl.utils import get_column_letter class EDIDelforCumminsParser: def __init__(self, filepath=None): self.root = tk.Tk() self.root.title("EDI Cummins Parser") self.root.geometry("1200x800") self.header_info = {} self.partner_info = {} self.delivery_schedules = [] self.line_items = [] # Handle window close event self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.setup_ui() if filepath: self.load_file(filepath) def setup_ui(self): main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) # Add buttons ttk.Button(btn_frame, text="Zpět na hlavní okno", command=self.back_to_main).pack(side=tk.LEFT, padx=(10, 0)) ttk.Button(btn_frame, text="Export do Excelu", command=self.export_to_excel).pack(side=tk.LEFT, padx=5) self.notebook = ttk.Notebook(main_frame) self.notebook.pack(fill=tk.BOTH, expand=True) self.info_frame = ttk.Frame(self.notebook) self.notebook.add(self.info_frame, text="Základní informace") self.delivery_frame = ttk.Frame(self.notebook) self.notebook.add(self.delivery_frame, text="Plán dodávek") self.stats_frame = ttk.Frame(self.notebook) self.notebook.add(self.stats_frame, text="Statistiky") self.setup_info_tab() self.setup_delivery_tab() self.setup_stats_tab() def setup_info_tab(self): text_frame = ttk.Frame(self.info_frame) text_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.info_text = tk.Text(text_frame, wrap=tk.WORD, font=('Courier', 10)) scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.info_text.yview) self.info_text.configure(yscrollcommand=scrollbar.set) self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) def setup_delivery_tab(self): tree_frame = ttk.Frame(self.delivery_frame) tree_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Removed 'Jednotka' column as requested columns = ('Položka', 'Popis', 'Datum', 'Množství', 'Typ', 'SCC', 'Release') self.delivery_tree = ttk.Treeview(tree_frame, columns=columns, show='headings', height=15) for col in columns: self.delivery_tree.heading(col, text=col) if col == 'Popis': self.delivery_tree.column(col, width=200) elif col == 'Položka': self.delivery_tree.column(col, width=100) else: self.delivery_tree.column(col, width=80) v_scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.delivery_tree.yview) h_scrollbar = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.delivery_tree.xview) self.delivery_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) self.delivery_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) def setup_stats_tab(self): stats_frame = ttk.Frame(self.stats_frame) stats_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.stats_text = tk.Text(stats_frame, wrap=tk.WORD, font=('Courier', 10)) stats_scrollbar = ttk.Scrollbar(stats_frame, orient=tk.VERTICAL, command=self.stats_text.yview) self.stats_text.configure(yscrollcommand=stats_scrollbar.set) self.stats_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) stats_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) def parse_date(self, date_str, format_code): try: if format_code == '102': return datetime.strptime(date_str, '%Y%m%d').strftime('%d.%m.%Y') else: return date_str except: return date_str def parse_edi_datetime(self, datetime_str): try: if ':' in datetime_str: date_part, time_part = datetime_str.split(':') full_date = '20' + date_part formatted_date = datetime.strptime(full_date, '%Y%m%d').strftime('%d.%m.%Y') formatted_time = datetime.strptime(time_part, '%H%M').strftime('%H:%M') return f"{formatted_date} {formatted_time}" return datetime_str except: return datetime_str def get_scc_description(self, scc_code): scc_map = { '10': 'Backlog', '1': 'Firm', '4': 'Forecast' } return scc_map.get(scc_code, f'SCC-{scc_code}') def parse_edi_file(self, content): lines = content.strip().split("'") self.header_info = {} self.partner_info = {} self.delivery_schedules = [] self.line_items = [] # Current parsing state current_part_number = '' current_description = '' current_location = '' current_po = '' current_scc = '' current_release = '' # Track current line item details current_line_item = None # Temporary storage for quantity waiting for date pending_quantities = [] def create_or_update_line_item(): nonlocal current_line_item if not current_part_number: return None line_item = next((item for item in self.line_items if item['Položka'] == current_part_number), None) if not line_item: line_item = { 'Položka': current_part_number, 'Popis': current_description, 'Objednávka': current_po, 'Lokace': current_location, 'RFF': {} } self.line_items.append(line_item) # Update current line item reference current_line_item = line_item return line_item for line in lines: line = line.strip() if not line: continue if line.startswith('UNB'): parts = line.split('+') if len(parts) >= 5: self.header_info['Odesílatel'] = parts[2] self.header_info['Příjemce_kód'] = parts[3] self.header_info['Datum/Čas'] = self.parse_edi_datetime(parts[4]) elif line.startswith('UNH'): parts = line.split('+') if len(parts) >= 2: self.header_info['ID zprávy'] = parts[1] elif line.startswith('BGM'): parts = line.split('+') if len(parts) >= 3: self.header_info['Číslo zprávy'] = parts[2] elif line.startswith('DTM'): parts = line.split('+') if len(parts) >= 2: dtm_parts = parts[1].split(':') if len(dtm_parts) >= 3: code = dtm_parts[0] value = dtm_parts[1] fmt = dtm_parts[2] formatted_date = self.parse_date(value, fmt) if code == '137': self.header_info['Datum dokumentu'] = formatted_date elif code == '2': # This is a delivery date - match with pending quantities # Only create entries if we have quantities to process if pending_quantities: # For SCC 10 (Backlog), we only take the first quantity if current_scc == '10' and len(pending_quantities) > 0: qty_info = pending_quantities[0] # Create line item if it doesn't exist line_item = next((item for item in self.line_items if item['Položka'] == current_part_number), None) if not line_item: line_item = { 'Položka': current_part_number, 'Popis': current_description, 'Objednávka': current_po, 'Lokace': current_location } self.line_items.append(line_item) delivery = { 'Položka': current_part_number, 'Popis': current_description, 'Datum': formatted_date, 'Množství': qty_info['quantity'], 'Typ': qty_info['type'], 'SCC': self.get_scc_description(current_scc), 'Release': current_release, 'Objednávka': current_po } self.delivery_schedules.append(delivery) else: # For other SCCs, process all quantities for qty_info in pending_quantities: delivery = { 'Položka': current_part_number, 'Popis': current_description, 'Datum': formatted_date, 'Množství': qty_info['quantity'], 'Typ': qty_info['type'], 'SCC': self.get_scc_description(current_scc), 'Release': current_release } self.delivery_schedules.append(delivery) pending_quantities.clear() # Don't reset release here to maintain it for next entries elif line.startswith('NAD'): parts = line.split('+') if len(parts) >= 3: role = parts[1] if role == 'SU': # Supplier name_parts = [p.replace('?+', '').replace('?', '').strip() for p in parts[4:] if p] self.partner_info['Dodavatel'] = ' '.join(name_parts) elif role == 'ST': # Ship To name_parts = [p.replace('?+', '').replace('?', '').strip() for p in parts[4:] if p] self.partner_info['Příjemce'] = ' '.join(name_parts) # Store the full address for delivery location if len(parts) > 5: # If there are address components address_parts = [] # Get address lines (parts[5] and beyond) for part in parts[5:]: if ':' in part: # Skip parts with qualifiers break address_parts.append(part.replace('?+', '').replace('?', '').strip()) if address_parts: self.partner_info['Dodací adresa'] = ', '.join(address_parts) # If no specific address found, use the recipient name as fallback if not self.partner_info['Dodací adresa'] and name_parts: self.partner_info['Dodací adresa'] = ' '.join(name_parts) elif line.startswith('LIN'): # Save previous line item if it exists if current_part_number: # Process previous line item if exists create_or_update_line_item() parts = line.split('+') # Process LIN segment if len(parts) >= 4: # Reset part information for new line item current_part_number = '' current_description = '' current_scc = '' current_release = '' current_line_item = None pending_quantities = [] # Try to find part number in the LIN segment for i, part in enumerate(parts[3:], 3): # Skip the first 3 parts (LIN, line number, action code) # Process part if ':' in part: # If the part contains a colon, it might be a part number part_info = part.split(':') # Process part info if len(part_info) >= 2 and part_info[1] == 'IN': # Look for part number with 'IN' qualifier current_part_number = part_info[0] # Found part number with IN qualifier break elif not current_part_number: # If no 'IN' qualifier found, take the first part current_part_number = part_info[0] # Using first part as part number # If still no part number found, try to get it from the last part if not current_part_number and parts[3:]: current_part_number = parts[3].split(':')[0] # Using fallback part number # Final part number processed elif line.startswith('IMD'): parts = line.split('+') if len(parts) >= 4: # Extract item description - fixed to properly handle the format # Looking for the 4th element which contains the description desc_part = parts[3] if len(parts) > 3 else '' # Remove leading colons and extract the actual description if desc_part.startswith(':::'): current_description = desc_part[3:].strip() elif desc_part.startswith('::'): current_description = desc_part[2:].strip() elif desc_part.startswith(':'): current_description = desc_part[1:].strip() else: current_description = desc_part.strip() # Clean up any remaining formatting current_description = current_description.replace(':', '').strip() elif line.startswith('LOC'): parts = line.split('+') if len(parts) >= 3: current_location = parts[2] elif line.startswith('RFF'): parts = line.split('+') # Process RFF segment if len(parts) >= 2: ref_parts = parts[1].split(':') if len(ref_parts) >= 2: ref_type = ref_parts[0] ref_value = ref_parts[1] # Found RFF reference # Create or update line item if it doesn't exist if not current_line_item: # Create new line item if none exists create_or_update_line_item() # Store the reference in the current line item if current_line_item: if 'RFF' not in current_line_item: current_line_item['RFF'] = {} current_line_item['RFF'][ref_type] = ref_value # RFF stored in line item # Special handling for order numbers if ref_type == 'ON': current_po = ref_value current_line_item['Objednávka'] = current_po # Order number set elif ref_type == 'RE': current_release = ref_value # Clear any pending quantities to ensure release number is applied to new quantities pending_quantities = [] # Release number set else: # No line item available for RFF pass elif line.startswith('SCC'): parts = line.split('+') if len(parts) >= 2: current_scc = parts[1] # Clear pending quantities when new SCC starts to prevent duplicates pending_quantities = [] # Only reset release for backlog (SCC 10) if current_scc == '10': current_release = '' elif line.startswith('QTY'): parts = line.split('+') if len(parts) >= 2: qty_parts = parts[1].split(':') if len(qty_parts) >= 2: qty_type = qty_parts[0] quantity = qty_parts[1] # Removed unit extraction as we don't need it # Determine quantity type qty_type_desc = 'Neznámý' if qty_type == '1': qty_type_desc = 'Dodávka' elif qty_type == '3': qty_type_desc = 'Kumulativní' elif qty_type == '48': qty_type_desc = 'Plánované' # Store quantity info waiting for corresponding date pending_quantities.append({ 'quantity': quantity, 'type': qty_type_desc }) # Store line items for reference unique_parts = {} for delivery in self.delivery_schedules: part_num = delivery['Položka'] if part_num not in unique_parts: unique_parts[part_num] = { 'Položka': part_num, 'Popis': delivery['Popis'] } self.line_items = list(unique_parts.values()) def load_file(self, filepath=None): if filepath: try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() self.parse_edi_file(content) self.display_data() return True except Exception as e: messagebox.showerror("Chyba", f"Nelze načíst soubor: {str(e)}") return False return False def display_data(self): # Display header info self.info_text.delete(1.0, tk.END) info_content = "=== HLAVIČKA DOKUMENTU ===\n" for key, value in self.header_info.items(): if key != 'Příjemce_kód': info_content += f"{key}: {value}\n" info_content += "\n=== INFORMACE O PARTNERECH ===\n" for key, value in self.partner_info.items(): info_content += f"{key}: {value}\n" self.info_text.insert(1.0, info_content) # Display delivery schedules for item in self.delivery_tree.get_children(): self.delivery_tree.delete(item) # Sort deliveries by date def date_sort_key(delivery): date_str = delivery.get('Datum', '') try: return datetime.strptime(date_str, '%d.%m.%Y') if date_str else datetime.max except: return datetime.max sorted_deliveries = sorted(self.delivery_schedules, key=date_sort_key) for delivery in sorted_deliveries: self.delivery_tree.insert('', tk.END, values=( delivery.get('Položka', ''), delivery.get('Popis', ''), delivery.get('Datum', ''), delivery.get('Množství', ''), delivery.get('Typ', ''), delivery.get('SCC', ''), delivery.get('Release', '') )) # Display statistics self.stats_text.delete(1.0, tk.END) stats_content = "=== STATISTIKY ===\n" stats_content += f"Celkový počet dodávek: {len(self.delivery_schedules)}\n" stats_content += f"Počet různých položek: {len(self.line_items)}\n" # Group by SCC scc_stats = {} total_qty = 0 for delivery in self.delivery_schedules: scc = delivery.get('SCC', 'Neznámý') qty_str = delivery.get('Množství', '0') try: qty = int(qty_str) total_qty += qty if scc not in scc_stats: scc_stats[scc] = {'count': 0, 'total_qty': 0} scc_stats[scc]['count'] += 1 scc_stats[scc]['total_qty'] += qty except: pass stats_content += f"Celkové množství: {total_qty:,} kusů\n\n" stats_content += "=== STATISTIKY PO SCC ===\n" for scc, stats in scc_stats.items(): stats_content += f"{scc}: {stats['count']} dodávek, {stats['total_qty']:,} kusů\n" self.stats_text.insert(1.0, stats_content) def on_closing(self): """Handle window close event""" self.root.destroy() # Close the current window def back_to_main(self): """Closes the current window""" self.root.destroy() def get_week_number(self, date_str): """Convert date string to ISO week number""" try: # Handle different date formats if '.' in date_str: date_obj = datetime.strptime(date_str, '%d.%m.%Y').date() else: date_obj = datetime.strptime(date_str, '%Y%m%d').date() return date_obj.isocalendar()[1] # Returns ISO week number except Exception as e: # Log error silently return "" def export_to_excel(self): """Export delivery data to Excel with calendar weeks""" if not self.delivery_schedules: messagebox.showwarning("Upozornění", "Žádná data k exportu") return try: wb = openpyxl.Workbook() ws = wb.active ws.title = "Dodávky" # Headers with week number headers = ["Týden", "Datum", "Položka", "Popis", "Množství", "Typ", "SCC", "Release", "Dodací místo"] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.font = Font(bold=True) cell.alignment = Alignment(horizontal='center') # Add data - sort by date from oldest to newest def parse_date(date_str): try: if '.' in date_str: return datetime.strptime(date_str, '%d.%m.%Y').date() else: return datetime.strptime(date_str, '%Y%m%d').date() except: return datetime.min.date() row_num = 2 for item in sorted(self.delivery_schedules, key=lambda x: parse_date(x.get('Datum', ''))): # Get week number from date week_num = self.get_week_number(item.get('Datum', '')) # Format quantity as number, removing any leading quotes quantity = item.get('Množství', '') if isinstance(quantity, str): quantity = quantity.strip("'") try: quantity = float(quantity) if quantity else 0 except (ValueError, TypeError): quantity = item.get('Množství', '') # Format week number as number try: week_num = int(week_num) if week_num else 0 except (ValueError, TypeError): week_num = 0 # Format part number as number if possible part_number = item.get('Položka', '') try: if str(part_number).strip(): # Only convert if not empty part_number = int(part_number) else: part_number = None except (ValueError, AttributeError, TypeError): part_number = str(part_number) # Keep as string if can't convert to int # Format release as number if possible release = item.get('Release', '') try: if str(release).strip(): # Only convert if not empty release = int(release) else: release = None except (ValueError, AttributeError, TypeError): release = str(release) # Keep as string if can't convert to int # Add week number and date in the first two columns ws.cell(row=row_num, column=1, value=week_num) # Format date as Excel date date_str = item.get('Datum', '') try: if date_str: date_obj = datetime.strptime(date_str, '%d.%m.%Y') ws.cell(row=row_num, column=2, value=date_obj).number_format = 'DD.MM.YYYY' else: ws.cell(row=row_num, column=2, value='') except (ValueError, TypeError): ws.cell(row=row_num, column=2, value=date_str) # Part number - format as number if possible if part_number is not None and part_number != '': if isinstance(part_number, (int, float)): ws.cell(row=row_num, column=3, value=part_number).number_format = '0' else: ws.cell(row=row_num, column=3, value=str(part_number)).number_format = '@' # Text format if not a number else: ws.cell(row=row_num, column=3, value='') # Format text columns as text ws.cell(row=row_num, column=4, value=str(item.get('Popis', ''))).number_format = '@' # Popis as text ws.cell(row=row_num, column=5, value=quantity) # Quantity as number ws.cell(row=row_num, column=6, value=str(item.get('Typ', ''))).number_format = '@' # Typ as text ws.cell(row=row_num, column=7, value=str(item.get('SCC', ''))).number_format = '@' # SCC as text # Release - format as number if possible if release is not None and release != '': if isinstance(release, (int, float)): ws.cell(row=row_num, column=8, value=release).number_format = '0' else: ws.cell(row=row_num, column=8, value=str(release)).number_format = '@' # Text format if not a number else: ws.cell(row=row_num, column=8, value='') # Get delivery location - ensure we have a non-empty string dodaci_misto = str(self.partner_info.get('Dodací adresa', '') or '').strip() ws.cell(row=row_num, column=9, value=dodaci_misto).number_format = '@' # Dodací místo as text row_num += 1 # Apply number formatting to numeric columns for row in ws.iter_rows(min_row=2, max_row=ws.max_row): # Format week number (column 1) as number with no decimal places if row[0].value is not None and row[0].value != '': # Column 1 (0-based index 0) if isinstance(row[0].value, (int, float)): row[0].number_format = '0' # Format quantity (column 5) as number with no decimal places if row[4].value is not None and row[4].value != '': # Column 5 (0-based index 4) if isinstance(row[4].value, (int, float)): row[4].number_format = '0' # Format part number (column 3) as number with no decimal places if it's a number if row[2].value is not None and row[2].value != '': # Column 3 (0-based index 2) if isinstance(row[2].value, (int, float)): row[2].number_format = '0' # Format release (column 8) as number with no decimal places if it's a number if row[7].value is not None and row[7].value != '': # Column 8 (0-based index 7) if isinstance(row[7].value, (int, float)): row[7].number_format = '0' # Auto-adjust column widths for col in ws.columns: max_length = 0 column = col[0].column_letter for cell in col: try: # For dates, use the formatted string length if cell.is_date: cell_value = cell.value.strftime('%d.%m.%Y') if cell.value else '' else: cell_value = str(cell.value) if cell.value is not None else '' if len(cell_value) > max_length: max_length = len(cell_value) except: pass adjusted_width = (max_length + 2) ws.column_dimensions[column].width = min(adjusted_width, 30) # Add a summary sheet with just week and quantity ws_summary = wb.create_sheet("Přehled") # Save the file filename = f"dodavky_cummins_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" filepath = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")], initialfile=filename ) if filepath: wb.save(filepath) messagebox.showinfo("Hotovo", f"Data byla úspěšně exportována do souboru:\n{filepath}") except Exception as e: messagebox.showerror("Chyba", f"Při exportu došlo k chybě: {str(e)}") def run(self): self.root.mainloop() if __name__ == "__main__": # When run directly, use the main parser to handle file selection from edi_parser_main import EDIUnifiedParser EDIUnifiedParser()