commit f497805d08551485b9f91a915cb418a60840c9ca Author: cerenX9 Date: Mon Jul 20 19:41:40 2026 +0300 İlk versiyon: MyMach Fleet Intelligence ve yapılandırmalar diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..981bbe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +.venv/ +ENV/ +env.bak/ +venv.bak/ +.env +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +pgdata/ diff --git a/addons/mymach_fleet_intelligence/__init__.py b/addons/mymach_fleet_intelligence/__init__.py new file mode 100644 index 0000000..f7209b1 --- /dev/null +++ b/addons/mymach_fleet_intelligence/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/addons/mymach_fleet_intelligence/__manifest__.py b/addons/mymach_fleet_intelligence/__manifest__.py new file mode 100644 index 0000000..b77f682 --- /dev/null +++ b/addons/mymach_fleet_intelligence/__manifest__.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'MyMach Akıllı Filo & Finansal Rota Motoru', + 'version': '1.0', + 'summary': 'Standart Odoo filo modülünden bağımsız, otonom rota takibi, cihaz entegrasyonu ve finansal maliyet hesaplaması yapan gelişmiş filo modülü.', + 'description': 'MyMach Fleet Intelligence - Faz 1 (Sözleşme Yönetimi)', + 'author': 'MyMach', + 'category': 'Human Resources/Fleet', + 'depends': ['base', 'fleet', 'hr', 'web', 'crm', 'project', 'product'], + 'data': [ + 'security/security_groups.xml', + 'security/ir.model.access.csv', + 'data/cron_jobs.xml', + 'views/contract_views.xml', + 'views/maintenance_cost_views.xml', + 'views/periodic_maintenance_views.xml', + 'views/fleet_vehicle_views.xml', + 'views/fleet_reservation_views.xml', + 'views/security_log_views.xml', + 'views/route_exception_wizard_views.xml', + 'views/dashboard_template.xml', + 'views/fleet_fine_views.xml', + 'views/fleet_handover_views.xml', + 'views/home_views.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', + 'assets': { + 'web.assets_backend': [ + 'mymach_fleet_intelligence/static/src/css/aesthetics.css', + 'mymach_fleet_intelligence/static/src/js/dashboard_action.js', + 'mymach_fleet_intelligence/static/src/xml/dashboard_action.xml', + ], + } +} diff --git a/addons/mymach_fleet_intelligence/check_all_postgres_databases.py b/addons/mymach_fleet_intelligence/check_all_postgres_databases.py new file mode 100644 index 0000000..c22dcaf --- /dev/null +++ b/addons/mymach_fleet_intelligence/check_all_postgres_databases.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=postgres user=odoo password=odoo host=db') +cr = conn.cursor() +cr.execute("SELECT datname FROM pg_database WHERE datistemplate = false;") +dbs = cr.fetchall() +print("ALL DATABASES IN POSTGRESQL:", dbs) +conn.close() diff --git a/addons/mymach_fleet_intelligence/check_database_modules.py b/addons/mymach_fleet_intelligence/check_database_modules.py new file mode 100644 index 0000000..6b9cfd3 --- /dev/null +++ b/addons/mymach_fleet_intelligence/check_database_modules.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() + +cr.execute("SELECT name, state FROM ir_module_module WHERE name IN ('base', 'web', 'fleet', 'mymach_fleet_intelligence', 'hr');") +print("MODULE STATES IN DB:", cr.fetchall()) + +cr.execute("SELECT count(*) FROM ir_ui_menu;") +print("TOTAL MENUS IN DB:", cr.fetchone()[0]) + +cr.execute("SELECT id, login, active FROM res_users WHERE login = 'admin';") +print("ADMIN USER:", cr.fetchone()) + +conn.close() diff --git a/addons/mymach_fleet_intelligence/check_fields.py b/addons/mymach_fleet_intelligence/check_fields.py new file mode 100644 index 0000000..d186128 --- /dev/null +++ b/addons/mymach_fleet_intelligence/check_fields.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +model_obj = env['mymach.fleet.handover'] +print("FIELDS OF mymach.fleet.handover:") +for f_name, f_obj in model_obj._fields.items(): + print(f"Field: {f_name}, Type: {f_obj.type}") diff --git a/addons/mymach_fleet_intelligence/check_handovers.py b/addons/mymach_fleet_intelligence/check_handovers.py new file mode 100644 index 0000000..4063c02 --- /dev/null +++ b/addons/mymach_fleet_intelligence/check_handovers.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +records = env['mymach.fleet.handover'].browse([18, 21]) +for r in records: + print(f"ID {r.id}: {r.manager_note}") diff --git a/addons/mymach_fleet_intelligence/check_modules.py b/addons/mymach_fleet_intelligence/check_modules.py new file mode 100644 index 0000000..7bcb3f8 --- /dev/null +++ b/addons/mymach_fleet_intelligence/check_modules.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() +cr.execute("SELECT name, state FROM ir_module_module WHERE name IN ('base', 'web', 'fleet', 'mymach_fleet_intelligence');") +print("MODULE STATES:", cr.fetchall()) +conn.close() diff --git a/addons/mymach_fleet_intelligence/cleanup_test.py b/addons/mymach_fleet_intelligence/cleanup_test.py new file mode 100644 index 0000000..6dcbab6 --- /dev/null +++ b/addons/mymach_fleet_intelligence/cleanup_test.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +vehicle = env['fleet.vehicle'].search([('license_plate', '=', 'TEST-BAKIM-34')]) +if vehicle: + odometers = env['fleet.vehicle.odometer'].search([('vehicle_id', '=', vehicle.id)]) + odometers.unlink() + maintenances = env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', '=', vehicle.id)]) + maintenances.unlink() + vehicle.unlink() + print("TEST RECORDS CLEANED SUCCESSFULLY!") +else: + print("NO TEST RECORDS FOUND.") diff --git a/addons/mymach_fleet_intelligence/clear_all_demo_data.py b/addons/mymach_fleet_intelligence/clear_all_demo_data.py new file mode 100644 index 0000000..8a639f3 --- /dev/null +++ b/addons/mymach_fleet_intelligence/clear_all_demo_data.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +print("Finishing cleanup for employees and demo driver partners...") + +# 1. Employees +try: + employees = env['hr.employee'].search([]) + employees.unlink() + print(f"Employees deleted.") +except Exception as e: + print(f"Employee delete notice: {e}") + +# 2. Demo Partners +try: + env.cr.execute(""" + DELETE FROM res_partner + WHERE id NOT IN (1, 2, 3) + AND name NOT IN ('YourCompany', 'Administrator', 'OdooBot', 'Public user', 'Mitchell Admin'); + """) + print("Demo partners deleted.") +except Exception as e: + print(f"Partner delete notice: {e}") + +env.cr.commit() +print("DATA RESET COMPLETELY FINISHED!") diff --git a/addons/mymach_fleet_intelligence/controllers/__init__.py b/addons/mymach_fleet_intelligence/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/addons/mymach_fleet_intelligence/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons/mymach_fleet_intelligence/controllers/main.py b/addons/mymach_fleet_intelligence/controllers/main.py new file mode 100644 index 0000000..7a4947b --- /dev/null +++ b/addons/mymach_fleet_intelligence/controllers/main.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +from odoo import http +from odoo.http import request +import json + +class MymachFleetController(http.Controller): + + @http.route('/mymach/fleet/ping', type='jsonrpc', auth='public', methods=['POST'], csrf=False) + def fleet_ping(self, vehicle_id, latitude, longitude, **kwargs): + """ + Faz 4.1: Otonom Mobil/Kiosk cihazlarından gelen anlık GPS pingini alır. + Faz 8 (Zero-Trust): Sadece geçerli API_TOKEN ile veri kabul edilir. (Anti-Spoofing) + """ + api_token = kwargs.get('api_token') + # Gerçek senaryoda bu token config dosyasından veya veritabanından çekilir. + if api_token != 'MYMACH_SECURE_TOKEN_2026': + return {'status': 'error', 'message': 'Erişim Reddedildi! Geçersiz API Token (Zero-Trust Security).'} + + vehicle = request.env['fleet.vehicle'].sudo().browse(vehicle_id) + if not vehicle.exists(): + return {'status': 'error', 'message': 'Araç bulunamadı.'} + + # Sürücüye ait personel kartı var mı? + employee = vehicle.driver_id.employee_ids[0] if vehicle.driver_id.employee_ids else None + + status = 'exception' # Sürpriz/İstisna rota (Varsayılan) + + # Otonom Kontrol 1: Personel Ofiste Mi? (İçerideyken Araç Hareket Ediyorsa) + if employee: + attendance = request.env['hr.attendance'].sudo().search([ + ('employee_id', '=', employee.id), + ('check_out', '=', False) + ], limit=1) + if attendance: + # Alarm: Personel hala şirkette kart basılı gözüküyor, ama aracı dışarıda ping atıyor! + status = 'in_office_moving' + + # Otonom Kontrol 2: GPS Eşleşmesi (Proje Lokasyonunda mı?) + is_project_zone = kwargs.get('is_project_zone', False) + if is_project_zone and status != 'in_office_moving': + status = 'project_match' + + # Güvenlik Logunu Yarat (Eğer İhlal varsa Otomatik Notification atacaktır) + log = request.env['mymach.fleet.security.log'].sudo().create({ + 'vehicle_id': vehicle.id, + 'latitude': latitude, + 'longitude': longitude, + 'status': status, + 'name': f'Otonom API Ping ({vehicle.license_plate})' + }) + + return {'status': 'success', 'log_id': log.id, 'security_status': status} + + @http.route('/mymach/fleet/dashboard', type='http', auth='user') + def fleet_dashboard(self, **kwargs): + """ + Canlı harita ve telemetri paneli için 5 test sürücüsünün rota ve sapma verilerini toplar. + """ + # 5 Farklı Sürücü İçin Gerçek Cadde/Sokak/Otoban Kıvrımlı Rota Verileri (Sürücü A - E) + routes_db = { + '34-AHM-101': { + 'driver_tag': 'Sürücü A', + 'start_location': 'Genel Merkez Ofis (Levent Kampüs)', + 'destination': 'İstanbul Havalimanı (Proje Bazlı Görev)', + 'planned_km': 15.2, + 'driven_km': 15.4, + 'planned_time': '35 dk (Planlanan Rota)', + 'deviation_label': '0.2 km Uyumlu', + 'green_route': [ + [41.0766, 29.0124], [41.0815, 29.0065], [41.0850, 28.9980], [41.0925, 28.9850], + [41.1000, 28.9720], [41.1050, 28.9650], [41.1180, 28.9500], [41.1350, 28.9300], + [41.1500, 28.9100], [41.1680, 28.8950], [41.1800, 28.8700], [41.1950, 28.8550], + [41.2100, 28.8350], [41.2300, 28.8100], [41.2500, 28.7850], [41.2650, 28.7650], [41.2752, 28.7519] + ], + 'red_route': [], + 'alt1': [ + [41.0766, 29.0124], [41.0650, 29.0010], [41.0500, 28.9900], [41.0620, 28.9450], + [41.0800, 28.9200], [41.1050, 28.9050], [41.1200, 28.8800], [41.1600, 28.8500], + [41.2100, 28.8200], [41.2500, 28.7800], [41.2752, 28.7519] + ], + 'alt2': [ + [41.0766, 29.0124], [41.0900, 29.0300], [41.1100, 29.0500], [41.1350, 29.0400], + [41.1600, 29.0200], [41.1850, 28.9850], [41.2000, 28.9500], [41.2300, 28.9000], + [41.2500, 28.8500], [41.2752, 28.7519] + ] + }, + '34-AYS-202': { + 'driver_tag': 'Sürücü B', + 'start_location': 'Genel Merkez Ofis (Levent Kampüs)', + 'destination': 'Kadıköy Şantiye Sahası (Şantiye Rotası)', + 'planned_km': 12.0, + 'driven_km': 12.3, + 'planned_time': '25 dk (Planlanan Rota)', + 'deviation_label': '0.3 km Uyumlu', + 'green_route': [ + [41.0766, 29.0124], [41.0680, 29.0145], [41.0600, 29.0120], [41.0520, 29.0080], + [41.0450, 29.0060], [41.0440, 29.0200], [41.0455, 29.0330], [41.0450, 29.0400], + [41.0380, 29.0450], [41.0250, 29.0380], [41.0150, 29.0320], [41.0050, 29.0280], + [40.9950, 29.0260], [40.9900, 29.0250] + ], + 'red_route': [], + 'alt1': [ + [41.0766, 29.0124], [41.0720, 29.0300], [41.0600, 29.0450], [41.0480, 29.0520], + [41.0350, 29.0480], [41.0180, 29.0350], [40.9900, 29.0250] + ], + 'alt2': [ + [41.0766, 29.0124], [41.0900, 29.0600], [41.0750, 29.0800], [41.0500, 29.0750], + [41.0300, 29.0600], [41.0100, 29.0450], [40.9900, 29.0250] + ] + }, + '34-MHM-303': { + 'driver_tag': 'Sürücü C', + 'start_location': 'Genel Merkez Ofis (Levent Kampüs)', + 'destination': 'Maslak Proje Sahası (Saha Görevi)', + 'planned_km': 14.0, + 'driven_km': 19.5, + 'planned_time': '35 dk (Planlanan Rota)', + 'deviation_label': '5.5 km Sapma (İhlal)', + 'green_route': [ + [41.0766, 29.0124], [41.0820, 29.0140], [41.0890, 29.0160], [41.0950, 29.0180], [41.1080, 29.0220] + ], + 'red_route': [ + [41.1080, 29.0220], [41.1120, 29.0350], [41.1150, 29.0480], [41.1220, 29.0580], + [41.1350, 29.0620], [41.1450, 29.0600], [41.1380, 29.0480] + ], + 'alt1': [ + [41.0766, 29.0124], [41.0850, 29.0100], [41.0950, 29.0120], [41.1080, 29.0220] + ], + 'alt2': [ + [41.0766, 29.0124], [41.0800, 28.9950], [41.0950, 28.9850], [41.1080, 29.0220] + ] + }, + '34-CNN-404': { + 'driver_tag': 'Sürücü D', + 'start_location': 'Genel Merkez Ofis (Levent Kampüs)', + 'destination': 'Gebze Lojistik Merkezi (Depo Rotası)', + 'planned_km': 25.0, + 'driven_km': 35.0, + 'planned_time': '45 dk (Planlanan Rota)', + 'deviation_label': '10.0 km Sapma (İhlal)', + 'green_route': [ + [41.0766, 29.0124], [41.0850, 29.0350], [41.0900, 29.0600], [41.0850, 29.0850], + [41.0600, 29.1100], [41.0200, 29.1350], [40.9800, 29.1500] + ], + 'red_route': [ + [40.9800, 29.1500], [40.9600, 29.1750], [40.9400, 29.2100], [40.9250, 29.2450], + [40.9100, 29.2800], [40.8900, 29.3150], [40.8700, 29.3500], [40.8400, 29.3900], [40.8000, 29.4300] + ], + 'alt1': [ + [41.0766, 29.0124], [41.0450, 29.0400], [41.0000, 29.1000], [40.9200, 29.2500], [40.8000, 29.4300] + ], + 'alt2': [ + [41.0766, 29.0124], [41.1000, 29.1000], [41.1200, 29.2000], [40.9800, 29.3500], [40.8000, 29.4300] + ] + }, + '34-BRK-505': { + 'driver_tag': 'Sürücü E', + 'start_location': 'Genel Merkez Ofis (Levent Kampüs)', + 'destination': 'Personel Ev Adresi (Kadıköy/Moda - Eve İzinli)', + 'planned_km': 10.0, + 'driven_km': 14.0, + 'planned_time': '20 dk (Planlanan Rota)', + 'deviation_label': 'Serbest İzinli', + 'green_route': [ + [41.0766, 29.0124], [41.0800, 29.0220], [41.0820, 29.0340], [41.0750, 29.0430], + [41.0650, 29.0440], [41.0480, 29.0250], [41.0450, 29.0060], [41.0550, 29.0110], [41.0766, 29.0124] + ], + 'red_route': [], + 'alt1': [ + [41.0766, 29.0124], [41.0600, 29.0100], [41.0480, 29.0250] + ], + 'alt2': [ + [41.0766, 29.0124], [41.0700, 28.9900], [41.0480, 29.0250] + ] + } + } + + vehicles = request.env['fleet.vehicle'].sudo().search([ + ('license_plate', 'in', list(routes_db.keys())) + ]) + + # Eğer veri tabanında bu 5 plaka yoksa veya eksikse, eksikleri dummy veri olarak oluştur: + existing_plates = vehicles.mapped('license_plate') + for plate in routes_db.keys(): + if plate not in existing_plates: + # Veritabanında yoksa fallback kullanması için en azından döngüde bulunması gerekir, ancak + # araç model vs oluşturmak uzun sürer. + pass + + vehicle_list = [] + for v in vehicles: + score = 100 + commute_allowed = 'forbidden' + policy = 'forbidden' + policy_desc = 'Yasaklı (Şahsi Kullanım Dışı)' + contract = None + + if v.driver_id: + contract = request.env['mymach.fleet.contract'].sudo().search([ + ('driver_id', '=', v.driver_id.id) + ], limit=1) + if contract: + score = contract.driver_score + commute_allowed = contract.commute_allowed + policy = contract.personal_use_policy + policy_desc = dict(contract._fields['personal_use_policy'].selection).get(policy, policy) + + # Personel Ev Adresi + driver_home_address = v.driver_id.street or "Personel Ev Adresi (Kadıköy/Moda)" + + rdata = routes_db.get(v.license_plate, { + 'driver_tag': 'Sürücü X', + 'start_location': 'Genel Merkez Ofis (Levent)', + 'destination': 'Genel Merkez Rotası', + 'planned_km': 10.0, + 'driven_km': 10.0, + 'planned_time': '20 dk (Planlanan Rota)', + 'deviation_label': 'Normal', + 'green_route': [[41.0766, 29.0124], [41.0800, 29.0200]], + 'red_route': [], + 'alt1': [[41.0766, 29.0124], [41.0850, 29.0250]], + 'alt2': [[41.0766, 29.0124], [41.0750, 29.0150]] + }) + + # Yönetici Onaylı Görevlendirme / Rota Var mı? (Canlı Harita Otomatik Güncelleme) + approved_res = request.env['mymach.fleet.reservation'].sudo().search([ + ('vehicle_id', '=', v.id), + ('state', '=', 'approved') + ], limit=1) + + destination = rdata['destination'] + is_manager_approved = False + + if approved_res: + is_manager_approved = True + destination = f"✅ Yönetici Onaylı Görev: {approved_res.destination_location or approved_res.purpose}" + + # 1 KM Sapma Kuralı ve Sözleşme KM Sınırı Uyumluluk Hesaplaması + deviation = round(rdata['driven_km'] - rdata['planned_km'], 1) + has_free_use = policy in ('allowed', 'unlimited', 'rental') + + # Sapma 1.0 KM kuralı kontrolü + is_over_1km = deviation > 1.0 + + # Uyumluluk ve İhlal Mantığı + if is_manager_approved: + is_violation = False + deviation_label = "✅ Yönetici Tarafından Onaylı Rota" + elif is_over_1km and policy == 'forbidden': + is_violation = True + deviation_label = f"🚨 UYUMSUZ ({deviation} km Yasaklı Şahsi Kullanım)" + elif is_over_1km and policy == 'limit' and contract and ((contract.personal_km_used + deviation) > (contract.max_km_limit or 0.0)): + is_violation = True + deviation_label = f"🚨 UYUMSUZ ({deviation} km Sözleşme KM Sınırı Aşıldı)" + elif has_free_use: + is_violation = False + deviation_label = "Serbest İzinli Şahsi Kullanım" + elif is_over_1km: + is_violation = True + deviation_label = f"🚨 UYUMSUZ ({deviation} km Sapma - Yöneticiye Alarm)" + else: + is_violation = False + deviation_label = f"{deviation} km Uyumlu Rota" + + # Eğer UYUMSUZ (İhlal) ise yöneticiye güvenlik bildirimi/logu üret + if is_violation and v.driver_id: + log_exists = request.env['mymach.fleet.security.log'].sudo().search([ + ('vehicle_id', '=', v.id), + ('status', '=', 'exception') + ], limit=1) + if not log_exists: + request.env['mymach.fleet.security.log'].sudo().create({ + 'vehicle_id': v.id, + 'driver_id': v.driver_id.id, + 'latitude': rdata['green_route'][-1][0] if rdata['green_route'] else 41.0766, + 'longitude': rdata['green_route'][-1][1] if rdata['green_route'] else 29.0124, + 'status': 'exception', + 'name': f"🚨 UYUMSUZ ROTA SAPMASI ({v.license_plate}): {deviation_label}" + }) + + route_color = '#ef4444' if is_violation else '#10b981' + + vehicle_list.append({ + 'id': v.id, + 'name': v.name, + 'license_plate': v.license_plate, + 'driver_name': v.driver_id.name or 'Sürücü Atanmamış', + 'driver_home_address': driver_home_address, + 'driver_tag': rdata['driver_tag'], + 'fuel_level': round(v.current_fuel_level, 1), + 'tank_capacity': v.tank_capacity, + 'needs_predictive_maintenance': v.needs_predictive_maintenance, + 'is_maintenance_critical': v.is_maintenance_critical, + 'score': score, + 'commute_allowed': commute_allowed, + 'policy': policy, + 'policy_desc': policy_desc, + 'start_location': rdata['start_location'], + 'destination': destination, + 'planned_km': rdata['planned_km'], + 'driven_km': rdata['driven_km'], + 'planned_time': rdata['planned_time'], + 'deviation_label': deviation_label, + 'deviation': deviation, + 'has_free_use': has_free_use, + 'is_manager_approved': is_manager_approved, + 'is_violation': is_violation, + 'route_color': route_color, + 'green_route': rdata['green_route'], + 'red_route': rdata['red_route'], + 'alt1': rdata['alt1'], + 'alt2': rdata['alt2'] + }) + + # Son 50 güvenlik logunu çek (Haritada göstermek için) + logs = request.env['mymach.fleet.security.log'].sudo().search([], limit=50) + log_list = [] + for l in logs: + log_list.append({ + 'id': l.id, + 'vehicle_name': l.vehicle_id.license_plate, + 'driver_name': l.driver_id.name or 'Bilinmiyor', + 'latitude': l.latitude, + 'longitude': l.longitude, + 'status': l.status, + 'status_desc': dict(l._fields['status'].selection).get(l.status, ''), + 'datetime': l.datetime.strftime('%Y-%m-%d %H:%M:%S') if l.datetime else '' + }) + + google_maps_api_key = request.env['ir.config_parameter'].sudo().get_param('mymach.google_maps_api_key', default='') + + return request.render('mymach_fleet_intelligence.dashboard_template', { + 'vehicles_json': json.dumps(vehicle_list, ensure_ascii=True), + 'logs_json': json.dumps(log_list, ensure_ascii=True), + 'google_maps_api_key': google_maps_api_key + }) diff --git a/addons/mymach_fleet_intelligence/create_demo_maintenances.py b/addons/mymach_fleet_intelligence/create_demo_maintenances.py new file mode 100644 index 0000000..ca9061d --- /dev/null +++ b/addons/mymach_fleet_intelligence/create_demo_maintenances.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from datetime import date, timedelta + +env = env(user=1) +model = env['fleet.vehicle.model'].search([], limit=1) +if not model: + raise ValueError("Veritabanında tanımlı araç modeli bulunamadı!") + +# Temizlik: Varsa eski demo araçları ve bakımları silelim +old_plates = ['34-YAZ-2026', '34-GEC-1903', '34-AGY-9999'] +existing_vehicles = env['fleet.vehicle'].search([('license_plate', 'in', old_plates)]) +if existing_vehicles: + # İlişkili odometreleri sil + env['fleet.vehicle.odometer'].search([('vehicle_id', 'in', existing_vehicles.ids)]).unlink() + # İlişkili bakımları sil + env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', 'in', existing_vehicles.ids)]).unlink() + existing_vehicles.unlink() + +print("Eski demo verileri temizlendi.") + +# 1. Araç: Yaklaşan Bakım (Benzinli) +vehicle_1 = env['fleet.vehicle'].create({ + 'model_id': model.id, + 'license_plate': '34-YAZ-2026', + 'fuel_type': 'gasoline', + 'odometer': 14200.0, + 'depreciation_cost_try': 120000.0, + 'depreciation_cost_usd': 3500.0, + 'depreciation_cost_eur': 3200.0, +}) +# İlk bakım otomatik olarak oluşturulmuştur (create hook), onu bulup tarihlerini manipüle edelim +maint_1 = env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', '=', vehicle_1.id)], limit=1) +if maint_1: + maint_1.write({ + 'maintenance_type': 'small', + 'target_odometer': 15000.0, + 'target_date': date.today() + timedelta(days=15) # 15 gün sonra + }) + +# 2. Araç: Gecikmiş / Kritik Bakım (Dizel) +vehicle_2 = env['fleet.vehicle'].create({ + 'model_id': model.id, + 'license_plate': '34-GEC-1903', + 'fuel_type': 'diesel', + 'odometer': 11500.0, + 'depreciation_cost_try': 150000.0, + 'depreciation_cost_usd': 4500.0, + 'depreciation_cost_eur': 4200.0, +}) +maint_2 = env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', '=', vehicle_2.id)], limit=1) +if maint_2: + maint_2.write({ + 'maintenance_type': 'small', + 'target_odometer': 10000.0, # Hedef aşılmış + 'target_date': date.today() - timedelta(days=5) # 5 gün önce (gecikmiş) + }) + +# 3. Araç: Yaklaşan Orta Bakım (Dizel) +vehicle_3 = env['fleet.vehicle'].create({ + 'model_id': model.id, + 'license_plate': '34-AGY-9999', + 'fuel_type': 'diesel', + 'odometer': 28500.0, + 'depreciation_cost_try': 220000.0, + 'depreciation_cost_usd': 6800.0, + 'depreciation_cost_eur': 6400.0, +}) +maint_3 = env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', '=', vehicle_3.id)], limit=1) +if maint_3: + maint_3.write({ + 'maintenance_type': 'medium', + 'target_odometer': 30000.0, + 'target_date': date.today() + timedelta(days=45) # 45 gün sonra + }) + +# Commit transactions so they are saved to db permanently +env.cr.commit() + +print("Yeni demo veriler başarıyla oluşturuldu:") +print("1. 34-YAZ-2026 (Yaklaşan Küçük Bakım - 15 gün kaldı)") +print("2. 34-GEC-1903 (Gecikmiş Küçük Bakım - 5 gün geçti / Limit aşıldı)") +print("3. 34-AGY-9999 (Yaklaşan Orta Bakım - 45 gün kaldı)") diff --git a/addons/mymach_fleet_intelligence/data/cron_jobs.xml b/addons/mymach_fleet_intelligence/data/cron_jobs.xml new file mode 100644 index 0000000..830d924 --- /dev/null +++ b/addons/mymach_fleet_intelligence/data/cron_jobs.xml @@ -0,0 +1,27 @@ + + + + + + MyMach: Akaryakıt Senkronizasyonu & Prediktif Bakım (AI) + + code + model.action_sync_fuel_data_cron() + + 1 + days + + + + + MyMach: 10 Günlük Teslimat Fotoğraflarını Temizleme + + code + model.action_purge_expired_photos() + + 1 + days + + + + diff --git a/addons/mymach_fleet_intelligence/export_project_to_desktop.py b/addons/mymach_fleet_intelligence/export_project_to_desktop.py new file mode 100644 index 0000000..7497b89 --- /dev/null +++ b/addons/mymach_fleet_intelligence/export_project_to_desktop.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import os +import shutil + +src_dir = r"C:\Users\Ortak\Desktop\Odoo-Ceren-MyMach\addons\mymach_fleet_intelligence" +dest_dir = r"C:\Users\Ortak\Desktop\MyMach_Fleet_Intelligence_Yedek" + +if os.path.exists(dest_dir): + shutil.rmtree(dest_dir) + +shutil.copytree(src_dir, os.path.join(dest_dir, "mymach_fleet_intelligence")) + +# Create docker-compose.yml in root for instant deployment +docker_compose_content = """version: '3.8' + +services: + db: + image: postgres:17 + environment: + - POSTGRES_DB=postgres + - POSTGRES_PASSWORD=odoo + - POSTGRES_USER=odoo + volumes: + - odoo-db-data:/var/lib/postgresql/data + + web: + image: odoo:19 + depends_on: + - db + ports: + - "8070:8069" + volumes: + - ./mymach_fleet_intelligence:/mnt/extra-addons/mymach_fleet_intelligence + environment: + - HOST=db + - USER=odoo + - PASSWORD=odoo + +volumes: + odoo-db-data: +""" + +with open(os.path.join(dest_dir, "docker-compose.yml"), "w", encoding="utf-8") as f: + f.write(docker_compose_content) + +readme_content = """=================================================== +MYMACH AKILLI FİLO & ROTA ZEKA SİSTEMİ - PROJE YEDEK +=================================================== + +Bu klasör, projenin en güncel ve hatasız kodlarını içerir. + +İÇERİK: +- mymach_fleet_intelligence/ (Tüm Modül Kodları, Görünümler, Kontrolcüler ve Harita Motoru) +- docker-compose.yml (Tek tıkla Docker sunucusu kurma dosyası) + +NASIL ÇALIŞTIRILIR? +1. Docker Desktop açıkken bu klasörde terminal (PowerShell) açın. +2. `docker-compose up -d` komutunu çalıştırın. +3. Tarayıcınızdan `http://localhost:8070` adresine girin. +""" + +with open(os.path.join(dest_dir, "README_KURULUM.txt"), "w", encoding="utf-8") as f: + f.write(readme_content) + +print("SUCCESS: Project exported to Desktop at", dest_dir) diff --git a/addons/mymach_fleet_intelligence/fix_base_groups.py b/addons/mymach_fleet_intelligence/fix_base_groups.py new file mode 100644 index 0000000..c4ed259 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_base_groups.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# 1. Reset stuck modules +cr.execute("UPDATE ir_module_module SET state = 'installed' WHERE state IN ('to install', 'to upgrade', 'to remove');") + +# 2. Ensure group_user exists in res_groups and ir_model_data +cr.execute("SELECT id FROM res_groups LIMIT 1;") +group_res = cr.fetchone() + +if not group_res: + cr.execute("""INSERT INTO res_groups (name, create_date, write_date) VALUES ('{"en_US": "Internal User"}', NOW(), NOW()) RETURNING id;""") + group_id = cr.fetchone()[0] +else: + group_id = group_res[0] + +cr.execute(""" + INSERT INTO ir_model_data (module, name, model, res_id) + VALUES ('base', 'group_user', 'res.groups', %s) + ON CONFLICT (module, name) DO UPDATE SET res_id = EXCLUDED.res_id; +""", (group_id,)) + +cr.commit() +print("SUCCESS: group_user XML ID restored!") diff --git a/addons/mymach_fleet_intelligence/fix_company_and_modules.py b/addons/mymach_fleet_intelligence/fix_company_and_modules.py new file mode 100644 index 0000000..2c1bfd8 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_company_and_modules.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() + +cr.execute("UPDATE res_users SET company_id = 1 WHERE id IN (1, 2, 4);") + +try: + cr.execute("INSERT INTO res_company_users_rel (user_id, cid) VALUES (1, 1), (2, 1) ON CONFLICT DO NOTHING;") +except Exception as e: + conn.rollback() + +cr.execute("UPDATE ir_module_module SET state = 'installed' WHERE state IN ('to install', 'to upgrade', 'to remove');") + +conn.commit() +conn.close() +print("SUCCESS: Company and module states fixed!") diff --git a/addons/mymach_fleet_intelligence/fix_db_sql.py b/addons/mymach_fleet_intelligence/fix_db_sql.py new file mode 100644 index 0000000..5de2f84 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_db_sql.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# Query to find all tables and character/text columns in the public schema +cr.execute(""" + SELECT table_name, column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND data_type IN ('character varying', 'text') + AND table_name NOT LIKE 'pg_%' + AND table_name NOT LIKE 'sql_%' +""") +columns = cr.fetchall() + +# Map of common broken words/phrases (ASCII-safe hex/unicode escape format) +replacements = { + 'g\x3f\x3fn': 'g\u00fcn', + '\x3f\x3fnce': '\u00f6nce', + 'onayland\x3f\x3f': 'onayland\u0131', + 'Foto\x3f\x3fraflar\x3f\x3f': 'Foto\u011fraflar\u0131', + 'y\x3f\x3fk\x3f\x3fnden': 'y\u00fck\u00fcnden', + 'ar\x3f\x3fnd\x3f\x3fr\x3f\x3fld\x3f\x3f': 'ar\u0131nd\u0131r\u0131ld\u0131', + 'S\x3f\x3fr\x3f\x3fc\x3f\x3f': 'S\u00fcr\u00fcc\u00fc', + 'Y\x3f\x3flmaz': 'Y\u0131lmaz', + 'Ara\x3f\x3f': 'Ara\u00e7', + 'g\x3f\x3fnderildi': 'g\u00f6nderildi', + 'G\x3f\x3fnderildi': 'G\u00f6nderildi', + 'edildi\x3f\x3fi': 'edildi\u011fi', + 'de\x3f\x3fer': 'de\u011fer', + 'De\x3f\x3fer': 'De\u011fer', + 'cezalar\x3f\x3f': 'cezalar\u0131', + 'Cezalar\x3f\x3f': 'Cezalar\u0131', + 'G\x3f\x3fvenlik': 'G\u00fcvenlik', + 'g\x3f\x3fvenlik': 'g\u00fcvenlik', + '\x3f\x3fhlal': '\u0130hlal', + '\x3f\x3fhlalleri': '\u0130hlalleri', + '\x3f\x3frtak': 'Ortak', + '\x3f\x3fmit': '\u00dcmit', + '\x3f\x3fener': '\u015eener', + '\x3f\x3felik': '\u00c7elik', + '\x3f\x3fz': '\u00d6z', + '\x3f\x3fzcan': '\u00d6zcan', + '\x3f\x3fzt\x3f\x3frk': '\u00d6zt\u00fcrk', + '\x3f\x3fzkan': '\u00d6zkan', + # Specific missing ones from the log + 'Ay\x3f\x3fe': 'Ay\u015fe', + 's\x3f\x3f': 's\u00fc', + 'S\x3f\x3fr\x3f\x3fc\x3fs\x3f\x3f': 'S\u00fcr\u00fcc\u00fcs\u00fc', + 'S\x3f\x3fr\x3f\x3fc\x3f_X': 'S\u00fcr\u00fcc\u00fc_X', + 'S\x3f\x3fr\x3f\x3fc\x3f_Yeni': 'S\u00fcr\u00fcc\u00fc_Yeni', + # Maintenance cost patterns + 'Yak\x3f\x3ft': 'Yak\u0131t', + 'Bak\x3f\x3fm': 'Bak\u0131m', + 'Al\x3f\x3fm\x3f\x3f': 'Al\u0131m\u0131', + 'Y\x3f\x3fksek': 'Y\u00fcksek', + 'T\x3f\x3fketim': 'T\u00fcketim', + 'Sim\x3f\x3flasyon': 'Sim\u00fclasyon', + 'A\x3f\x3f\x3f\x3fr': 'A\u011f\u0131r', +} + +fixed_count = 0 + +for table_name, column_name in columns: + if table_name.startswith(('ir_logging', 'ir_attachment', 'mail_message', 'mail_tracking', 'ir_model_data', 'ir_translation')): + continue + + try: + cr.execute(f'SELECT id FROM "{table_name}" WHERE "{column_name}" LIKE \'%??%\'') + res = cr.fetchall() + if res: + record_ids = [r[0] for r in res] + + for rec_id in record_ids: + cr.execute(f'SELECT "{column_name}" FROM "{table_name}" WHERE id = %s', (rec_id,)) + val = cr.fetchone()[0] + if val: + fixed_val = val + for broken, fixed in replacements.items(): + fixed_val = fixed_val.replace(broken, fixed) + + if fixed_val != val: + cr.execute(f'UPDATE "{table_name}" SET "{column_name}" = %s WHERE id = %s', (fixed_val, rec_id)) + print(f"Table: {table_name}, Column: {column_name}, ID: {rec_id} -> Fixed to: '{fixed_val}'") + fixed_count += 1 + except Exception: + pass + +if fixed_count > 0: + cr.commit() + print(f"SUCCESS: Fixed {fixed_count} columns across the database!") +else: + print("NO CORRUPTED DATA FOUND/FIXED IN DATABASE VIA SQL.") diff --git a/addons/mymach_fleet_intelligence/fix_db_turkish_chars.py b/addons/mymach_fleet_intelligence/fix_db_turkish_chars.py new file mode 100644 index 0000000..c07e575 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_db_turkish_chars.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +models_to_check = [ + 'mymach.fleet.contract', + 'mymach.fleet.handover', + 'mymach.fleet.reservation', + 'mymach.fleet.fine', + 'mymach.fleet.security.log', + 'res.partner', + 'hr.employee' +] + +# Replacement function for broken Turkish characters in Odoo database +def fix_text(text): + if not text: + return text + # Map of common broken words/phrases + replacements = { + 'g??n': 'gün', + '??nce': 'önce', + 'onayland??': 'onaylandı', + 'Foto??raflar??': 'Fotoğrafları', + 'y??k??nden': 'yükünden', + 'ar??nd??r??ld??': 'arındırıldı', + 'S??r??c??': 'Sürücü', + 'Y??lmaz': 'Yılmaz', + 'Ara??': 'Araç', + 'g??nderildi': 'gönderildi', + 'G??nderildi': 'Gönderildi', + 'edildi??i': 'edildiği', + 'de??er': 'değer', + 'De??er': 'Değer', + 'cezalar??': 'cezaları', + 'Cezalar??': 'Cezaları', + 'ihlal': 'ihlal', + 'G??venlik': 'Güvenlik', + 'g??venlik': 'güvenlik', + '??hlal': 'İhlal', + '??hlalleri': 'İhlalleri', + '??rtak': 'Ortak', + '??mit': 'Ümit', + '??ener': 'Şener', + '??elik': 'Çelik', + '??z': 'Öz', + '??zcan': 'Özcan', + '??ztürk': 'Öztürk', + '??zkan': 'Özkan', + } + + new_text = text + for broken, fixed in replacements.items(): + new_text = new_text.replace(broken, fixed) + + # Also catch generic character issues + # e.g. ?? -> ü / ö / ı / ş / ç depending on context or replace them individually if they are alone + return new_text + +updated_count = 0 + +for model_name in models_to_check: + try: + model_obj = env[model_name] + except KeyError: + continue + + string_fields = [f_name for f_name, f_obj in model_obj._fields.items() if f_obj.type in ('char', 'text', 'html') and not f_obj.compute] + + records = model_obj.search([]) + for rec in records: + updates = {} + for field in string_fields: + val = rec[field] + if val and isinstance(val, str) and '??' in val: + fixed_val = fix_text(val) + if fixed_val != val: + updates[field] = fixed_val + print(f"[{model_name} ID {rec.id}] Field '{field}': '{val}' -> '{fixed_val}'") + + if updates: + rec.write(updates) + updated_count += 1 + +if updated_count > 0: + env.cr.commit() + print(f"SUCCESS: Fixed Turkish characters in {updated_count} database records!") +else: + print("NO CORRUPTED TEXT RECORDS FOUND IN SPECIFIED MODELS.") diff --git a/addons/mymach_fleet_intelligence/fix_external_ids.py b/addons/mymach_fleet_intelligence/fix_external_ids.py new file mode 100644 index 0000000..e8788af --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_external_ids.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# Create Public Partner & Public User if not exists +cr.execute(""" + INSERT INTO res_partner (id, name, active, autopost_bills, group_rfq, group_on, create_date, write_date) + VALUES (4, 'Public user', true, 'ask', 'all', 'all', NOW(), NOW()) + ON CONFLICT (id) DO NOTHING; +""") + +cr.execute(""" + INSERT INTO res_users (id, login, password, partner_id, active, company_id, notification_type, create_date, write_date) + VALUES (4, 'public', '', 4, true, 1, 'email', NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET active = true; +""") + +# Insert/Restore XML IDs in ir_model_data +xmlids = [ + ('base', 'public_user', 'res.users', 4), + ('base', 'public_partner', 'res.partner', 4), + ('base', 'user_admin', 'res.users', 1), + ('base', 'partner_admin', 'res.partner', 3), + ('base', 'main_partner', 'res.partner', 1), + ('base', 'main_company', 'res.company', 1), + ('base', 'user_root', 'res.users', 2), + ('base', 'partner_root', 'res.partner', 2), + ('base', 'USD', 'res.currency', 1), + ('base', 'TRY', 'res.currency', 31) +] + +for module, name, model, res_id in xmlids: + cr.execute(""" + INSERT INTO ir_model_data (module, name, model, res_id) + VALUES (%s, %s, %s, %s) + ON CONFLICT (module, name) DO UPDATE SET res_id = EXCLUDED.res_id, model = EXCLUDED.model; + """, (module, name, model, res_id)) + +cr.commit() +print("SUCCESS: Missing External IDs (base.public_user, etc.) successfully restored in ir_model_data!") diff --git a/addons/mymach_fleet_intelligence/fix_lang_xmlid.py b/addons/mymach_fleet_intelligence/fix_lang_xmlid.py new file mode 100644 index 0000000..2aead25 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_lang_xmlid.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# 1. Clear manually inserted res_lang or link ir_model_data +cr.execute("SELECT id FROM res_lang WHERE code = 'en_US';") +lang_id = cr.fetchone() +if lang_id: + cr.execute(""" + INSERT INTO ir_model_data (module, name, model, res_id) + VALUES ('base', 'lang_en', 'res.lang', %s) + ON CONFLICT (module, name) DO UPDATE SET res_id = EXCLUDED.res_id; + """, (lang_id[0],)) + +cr.commit() +print("Language XML ID fixed!") diff --git a/addons/mymach_fleet_intelligence/fix_odoo_conf.py b/addons/mymach_fleet_intelligence/fix_odoo_conf.py new file mode 100644 index 0000000..eb67af8 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_odoo_conf.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +conf_content = """[options] +addons_path = /mnt/extra-addons +db_host = db +db_port = 5432 +db_user = odoo +db_password = odoo +db_name = dev +list_db = False +log_level = info +""" + +with open('/etc/odoo/odoo.conf', 'w', encoding='utf-8') as f: + f.write(conf_content) + +print("SUCCESS: Updated /etc/odoo/odoo.conf with db_name = dev and list_db = False!") diff --git a/addons/mymach_fleet_intelligence/fix_users.py b/addons/mymach_fleet_intelligence/fix_users.py new file mode 100644 index 0000000..31dcfa6 --- /dev/null +++ b/addons/mymach_fleet_intelligence/fix_users.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +# Disable constraints temporarily to create via ORM +env.cr.execute("UPDATE res_users SET partner_id = NULL WHERE id = 1;") +env.cr.commit() + +# Create Admin Partner via ORM to fill all default fields +admin_partner = env['res.partner'].create({'name': 'Administrator'}) +print("Created Admin Partner:", admin_partner.id) + +company_partner = env['res.partner'].create({'name': 'YourCompany', 'is_company': True}) +print("Created Company Partner:", company_partner.id) + +bot_partner = env['res.partner'].create({'name': 'OdooBot'}) +print("Created Bot Partner:", bot_partner.id) + +# Link partners back to users +env.cr.execute("UPDATE res_users SET partner_id = %s WHERE id = 1;", (admin_partner.id,)) +env.cr.execute("UPDATE res_users SET partner_id = %s WHERE id = 2;", (bot_partner.id,)) +env.cr.execute("UPDATE res_company SET partner_id = %s WHERE id = 1;", (company_partner.id,)) + +env.cr.commit() +print("BASE ADMIN & ODOOBOT USERS AND PARTNERS FULLY RESTORED VIA ORM!") diff --git a/addons/mymach_fleet_intelligence/i18n/tr_TR.po b/addons/mymach_fleet_intelligence/i18n/tr_TR.po new file mode 100644 index 0000000..8a46f5e --- /dev/null +++ b/addons/mymach_fleet_intelligence/i18n/tr_TR.po @@ -0,0 +1,547 @@ +# Translation of Odoo Server. +# This file contains the translation of the following module: +# * mymach_fleet_intelligence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-16 12:00+0000\n" +"PO-Revision-Date: 2026-07-16 12:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__name +msgid "Contract Reference" +msgstr "Sözleşme Referansı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__driver_id +msgid "Driver" +msgstr "Sürücü" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__employee_id +msgid "Related Employee" +msgstr "İlişkili Çalışan" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__department_id +msgid "Department" +msgstr "Departman" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__personal_use_policy +msgid "Personal Use Policy" +msgstr "Şahsi Kullanım Politikası" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_contract__personal_use_policy__allowed +msgid "Allowed" +msgstr "İzinli" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_contract__personal_use_policy__limit +msgid "Limited" +msgstr "Limitli" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_contract__personal_use_policy__forbidden +msgid "Forbidden" +msgstr "Yasaklı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__max_km_limit +msgid "Maximum KM Limit" +msgstr "Maksimum KM Limiti" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__commute_allowed +msgid "Commute Allowed" +msgstr "Eve Git/Gel İzni" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__personal_device_gps_allowed +msgid "Personal Device GPS Allowed" +msgstr "Kişisel Cihaz GPS İzni" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__company_phone_device_id +msgid "Company Phone Device ID (UUID)" +msgstr "Şirket Telefonu Cihaz ID (UUID)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_contract__driver_score +msgid "Driver Score Card (0-100)" +msgstr "Sürücü Skor Kartı (0-100)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__name +msgid "Charge/Transit Reference" +msgstr "Ceza/Geçiş Referansı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__charge_type +msgid "Expense Type" +msgstr "Gider Türü" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__charge_type__hgs +msgid "HGS Toll Charge" +msgstr "HGS Geçiş Ücreti" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__charge_type__fine +msgid "Traffic Fine" +msgstr "Trafik Cezası" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__charge_type__parking +msgid "Parking Fee" +msgstr "Otopark Ücreti" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__amount +msgid "Amount" +msgstr "Tutar" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__currency_id +msgid "Currency" +msgstr "Para Birimi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__date +msgid "Transaction Date" +msgstr "İşlem Tarihi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__is_personal_violation +msgid "Personal Use Violation?" +msgstr "Şahsi Kullanım İhlali?" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_fine__state +msgid "Status" +msgstr "Durum" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__state__draft +msgid "Draft" +msgstr "Taslak" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__state__confirmed +msgid "Confirmed" +msgstr "Onaylandı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_fine__state__deducted +msgid "Deducted to Payroll/Current" +msgstr "Bordroya/Cariye Yansıtıldı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__name +msgid "Handover Reference" +msgstr "Teslim Referansı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__vehicle_id +msgid "Vehicle" +msgstr "Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__driver_id +msgid "Driver (Employee)" +msgstr "Sürücü (Personel)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__state +msgid "Status" +msgstr "Durum" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_handover__state__draft +msgid "Draft" +msgstr "Taslak" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_handover__state__taken +msgid "Handed Over (In Use)" +msgstr "Teslim Alındı (Kullanımda)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_handover__state__returned +msgid "Returned" +msgstr "Teslim Edildi (İade Edildi)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_handover__state__compared +msgid "Inspected / Approved" +msgstr "Denetlendi / Onaylandı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__take_datetime +msgid "Take Datetime" +msgstr "Teslim Alım Zamanı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__start_odometer +msgid "Start Odometer" +msgstr "Alış Kilometresi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__start_fuel_level +msgid "Start Fuel Level (%)" +msgstr "Alış Yakıt Seviyesi (%)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__start_photo +msgid "Take Photo" +msgstr "Teslim Alım Fotoğrafı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__return_datetime +msgid "Return Datetime" +msgstr "İade Zamanı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__end_odometer +msgid "End Odometer" +msgstr "İade Kilometresi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__end_fuel_level +msgid "End Fuel Level (%)" +msgstr "İade Yakıt Seviyesi (%)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__end_photo +msgid "Return Photo" +msgstr "İade Fotoğrafı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__odometer_diff +msgid "Odometer Diff (KM)" +msgstr "Yapılan Yol (KM)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__fuel_diff +msgid "Fuel Diff (%)" +msgstr "Yakıt Tüketim Farkı (%)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__manager_note +msgid "Manager Inspection Notes" +msgstr "Yönetici Denetim Notları" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_handover__is_approved +msgid "Inspection Approval" +msgstr "Denetim Onayı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__name +msgid "Request No" +msgstr "Talep No" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__employee_id +msgid "Requested Employee" +msgstr "Talep Eden Çalışan" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__vehicle_id +msgid "Requested Vehicle" +msgstr "Talep Edilen Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__start_datetime +msgid "Start Datetime" +msgstr "Başlangıç Tarihi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__end_datetime +msgid "End Datetime" +msgstr "Bitiş Tarihi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__purpose +msgid "Purpose / Justification" +msgstr "Kullanım Amacı / Gerekçe" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_reservation__state +msgid "Status" +msgstr "Durum" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_reservation__state__draft +msgid "Draft" +msgstr "Taslak" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_reservation__state__submitted +msgid "Submitted" +msgstr "Yönetici Onayı Bekliyor" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_reservation__state__approved +msgid "Approved (Reserved)" +msgstr "Onaylandı (Rezerve)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_reservation__state__rejected +msgid "Rejected" +msgstr "Reddedildi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_reservation__state__done +msgid "Completed / Handed Over" +msgstr "Tamamlandı / Teslim Edildi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__name +msgid "Description" +msgstr "Gider Açıklaması" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__product_id +msgid "Expense Item (Product)" +msgstr "Masraf Kalemi (Ürün)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__vehicle_id +msgid "Vehicle" +msgstr "Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__amount +msgid "Amount" +msgstr "Tutar" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__currency_id +msgid "Currency" +msgstr "Para Birimi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_maintenance_cost__date +msgid "Date" +msgstr "Tarih" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__purchase_value +msgid "Purchase Value" +msgstr "Alım Bedeli" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__depreciation_years +msgid "Depreciation Years" +msgstr "Amortisman Yılı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__annual_fixed_cost +msgid "Annual Fixed Cost" +msgstr "Yıllık Sabit Giderler (Kasko/MTV vb.)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__index_currency_id +msgid "Index Currency (Depreciation)" +msgstr "Endeks Para Birimi (Amortisman)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__tank_capacity +msgid "Fuel Tank Capacity (Liters)" +msgstr "Yakıt Depo Kapasitesi (Litre)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__current_fuel_level +msgid "Current Fuel Level (Liters)" +msgstr "Anlık Yakıt Seviyesi (Tahmini Litre)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__needs_predictive_maintenance +msgid "Predictive Maintenance Warning (AI)" +msgstr "Kestirimci Bakım Uyarısı (AI)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__factory_avg_consumption +msgid "Factory Average Consumption (L/100KM)" +msgstr "Fabrika Verisi (L/100KM)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__indexed_purchase_value +msgid "Indexed Purchase Value" +msgstr "Endeksli Alım Bedeli" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__moving_average_maintenance_cost +msgid "Moving Average Maintenance Cost" +msgstr "Hareketli Ortalama Bakım Maliyeti" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__is_maintenance_critical +msgid "Critical Cost Alarm" +msgstr "Kritik Maliyet Alarmı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__name +msgid "Log Reference" +msgstr "Log Referansı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__vehicle_id +msgid "Vehicle" +msgstr "Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__driver_id +msgid "Driver" +msgstr "Sürücü" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__datetime +msgid "Datetime" +msgstr "Zaman" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__latitude +msgid "Latitude" +msgstr "Enlem" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__longitude +msgid "Longitude" +msgstr "Boylam" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__status +msgid "Security Status" +msgstr "Güvenlik Durumu" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_security_log__status__in_office_moving +msgid "WARNING: Employee in Office but Vehicle Moving" +msgstr "UYARI: Personel Ofiste Ama Araç Hareket Halinde" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_security_log__status__project_match +msgid "CONFIRMED: Project Route Match" +msgstr "ONAY: Proje Rotası Eşleşti" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_security_log__status__exception +msgid "ATTENTION: Surprise Route Exception" +msgstr "DİKKAT: İstisna (Sürpriz) Rota" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__is_resolved +msgid "Is Resolved?" +msgstr "Çözüldü mü?" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__deduction_amount +msgid "Payroll Deduction Amount" +msgstr "Cari/Bordro Kesinti Tutarı" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_security_log__currency_id +msgid "Currency" +msgstr "Para Birimi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__log_id +msgid "Security Log" +msgstr "Güvenlik Logu" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__vehicle_id +msgid "Vehicle" +msgstr "Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__exception_type +msgid "Route Reason" +msgstr "Rota Gerekçesi" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__exception_type__crm +msgid "CRM Opportunity" +msgstr "CRM Fırsatı (Müşteri Ziyareti)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__exception_type__project +msgid "Project Task" +msgstr "Proje Görevi (Şantiye)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,selection:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__exception_type__personal +msgid "Personal Use" +msgstr "Kişisel Kullanım" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__description +msgid "Description / Reason" +msgstr "Açıklama / Sebep" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_route_exception_wizard__estimated_cost +msgid "Estimated Route Cost" +msgstr "Tahmini Rota Maliyeti" + +#. module: mymach_fleet_intelligence +#: model:ir.model,name:mymach_fleet_intelligence.model_mymach_fleet_periodic_maintenance +msgid "Vehicle Periodic Maintenance Reminders" +msgstr "Araç Periyodik Bakım Hatırlatıcıları" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_periodic_maintenance__vehicle_id +msgid "Vehicle" +msgstr "Araç" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_periodic_maintenance__maintenance_type +msgid "Maintenance Type" +msgstr "Bakım Türü" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_periodic_maintenance__target_odometer +msgid "Target Odometer" +msgstr "Hedef Kilometre" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_periodic_maintenance__target_date +msgid "Target Date" +msgstr "Hedef Tarih" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_mymach_fleet_periodic_maintenance__state +msgid "Status" +msgstr "Durum" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__depreciation_cost_try +msgid "Amortisman Bedeli (TRY)" +msgstr "Amortisman Bedeli (TRY)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__depreciation_cost_usd +msgid "Amortisman Bedeli (USD)" +msgstr "Amortisman Bedeli (USD)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__depreciation_cost_eur +msgid "Amortisman Bedeli (EUR)" +msgstr "Amortisman Bedeli (EUR)" + +#. module: mymach_fleet_intelligence +#: model:ir.model.fields,field_description:mymach_fleet_intelligence.field_fleet_vehicle__depreciation_cost_avg +msgid "Ortalama Amortisman (TRY)" +msgstr "Ortalama Amortisman (TRY)" + diff --git a/addons/mymach_fleet_intelligence/inspect_cols.py b/addons/mymach_fleet_intelligence/inspect_cols.py new file mode 100644 index 0000000..530829e --- /dev/null +++ b/addons/mymach_fleet_intelligence/inspect_cols.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# 0. Insert base language +cr.execute(""" + INSERT INTO res_lang (name, code, iso_code, url_code, direction, active, date_format, time_format, grouping, decimal_point, thousands_sep, week_start) + VALUES ('English', 'en_US', 'en_US', 'en', 'ltr', true, '%m/%d/%Y', '%H:%M:%S', '[3,0]', '.', ',', '7') + ON CONFLICT (code) DO UPDATE SET active = true; +""") + +# 1. Insert Base Currency USD (id=1) & TRY (id=31) +cr.execute(""" + INSERT INTO res_currency (id, name, symbol, decimal_places, active, create_date, write_date) + VALUES + (1, 'USD', '$', 2, true, NOW(), NOW()), + (31, 'TRY', '₺', 2, true, NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET active = true; +""") + +# 2. Insert base partners +cr.execute(""" + INSERT INTO res_partner (id, name, active, autopost_bills, group_rfq, group_on, create_date, write_date) + VALUES + (1, 'YourCompany', true, 'ask', 'all', 'all', NOW(), NOW()), + (2, 'OdooBot', true, 'ask', 'all', 'all', NOW(), NOW()), + (3, 'Administrator', true, 'ask', 'all', 'all', NOW(), NOW()) + ON CONFLICT (id) DO NOTHING; +""") + +# 3. Insert base company with all NOT NULL columns +cr.execute(""" + INSERT INTO res_company ( + id, name, partner_id, currency_id, layout_background, fiscalyear_last_day, fiscalyear_last_month, + account_price_include, horizon_days, inventory_period, cost_method, security_lead, + point_of_sale_ticket_portal_url_display_mode, create_date, write_date + ) + VALUES ( + 1, 'YourCompany', 1, 1, 'Blank', 31, '12', + 'tax_excluded', 30, 30, 'fifo', 0, + 'none', NOW(), NOW() + ) + ON CONFLICT (id) DO UPDATE SET currency_id = 1; +""") + +# 4. Insert base users +cr.execute(""" + INSERT INTO res_users (id, login, password, partner_id, active, company_id, notification_type, create_date, write_date) + VALUES + (1, 'admin', 'admin', 3, true, 1, 'email', NOW(), NOW()), + (2, '__system__', '', 2, true, 1, 'email', NOW(), NOW()) + ON CONFLICT (id) DO UPDATE SET partner_id = EXCLUDED.partner_id, active = true, notification_type = 'email', company_id = 1; +""") + +cr.commit() +print("SUCCESS: Base language, currency, partners, company, and users fully restored!") diff --git a/addons/mymach_fleet_intelligence/inspect_users.py b/addons/mymach_fleet_intelligence/inspect_users.py new file mode 100644 index 0000000..924fec2 --- /dev/null +++ b/addons/mymach_fleet_intelligence/inspect_users.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +fields = list(env['res.users']._fields.keys()) +print("Fields in res.users:") +print(fields) diff --git a/addons/mymach_fleet_intelligence/install_all_modules.py b/addons/mymach_fleet_intelligence/install_all_modules.py new file mode 100644 index 0000000..52a4181 --- /dev/null +++ b/addons/mymach_fleet_intelligence/install_all_modules.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() +cr.execute("UPDATE ir_module_module SET state = 'to install' WHERE name IN ('web', 'fleet', 'mymach_fleet_intelligence');") +conn.commit() +conn.close() +print("SUCCESS: Modules set to install!") diff --git a/addons/mymach_fleet_intelligence/mark_installed_and_upgrade.py b/addons/mymach_fleet_intelligence/mark_installed_and_upgrade.py new file mode 100644 index 0000000..a750bbc --- /dev/null +++ b/addons/mymach_fleet_intelligence/mark_installed_and_upgrade.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() +cr.execute("UPDATE ir_module_module SET state = 'installed' WHERE name IN ('web', 'fleet', 'mymach_fleet_intelligence');") +conn.commit() +conn.close() +print("SUCCESS: Modules marked installed in database!") diff --git a/addons/mymach_fleet_intelligence/models/__init__.py b/addons/mymach_fleet_intelligence/models/__init__.py new file mode 100644 index 0000000..70b5db6 --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/__init__.py @@ -0,0 +1,10 @@ +from . import fleet_contract +from . import maintenance_cost +from . import fleet_vehicle +from . import security_log +from . import route_exception_wizard +from . import fleet_reservation +from . import home +from . import fleet_fine +from . import fleet_handover +from . import periodic_maintenance diff --git a/addons/mymach_fleet_intelligence/models/fleet_contract.py b/addons/mymach_fleet_intelligence/models/fleet_contract.py new file mode 100644 index 0000000..5232bfd --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/fleet_contract.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + +class MymachFleetContract(models.Model): + """ + Sürücüler ile havuz araçları arasındaki şahsi kullanım, + kilometre sınırları ve güvenlik yetkilerini tanımlayan Tahsis Sözleşmesi Modeli. + (Şirket Anayasası Madde 3, 4 & 5 referansı) + """ + _name = 'mymach.fleet.contract' + _description = 'Filo Araç Tahsis Sözleşmesi' + _inherit = ['mail.thread'] + + # Sözleşme Adı + name = fields.Char(string="Contract Reference", required=True) + + # Sürücü (İş Ortağı) + driver_id = fields.Many2one('res.partner', string="Driver", required=True) + + # İlgili Çalışan (hr.employee) - Bağlantılı alanlar için gizli tutulacak + employee_id = fields.Many2one('hr.employee', string="Related Employee", compute="_compute_employee", store=True) + + # Departman (Çalışan kaydından otomatik hesaplanır) + department_id = fields.Many2one( + 'hr.department', + string="Department", + related="employee_id.department_id", + readonly=True, + store=True + ) + + # Şahsi Kullanım Politikası + personal_use_policy = fields.Selection([ + ('allowed', 'Serbest Şahsi Kullanım (İzinli)'), + ('limit', 'Limitli Şahsi Kullanım (Sözleşmeli KM Sınırı)'), + ('unlimited', 'Sınırsız (Limitsiz) Şahsi Kullanım'), + ('rental', 'Kiralık (Kira Bedelli) Tahsis'), + ('forbidden', 'Yasaklı (Şahsi Kullanım Dışı)') + ], string="Şahsi Kullanım Politikası", required=True, default='forbidden') + + # Maksimum KM Limiti (Sadece "allowed" veya "limit" seçildiğinde arayüzde görünür olacak) + max_km_limit = fields.Float(string="Maximum KM Limit", help="Monthly kilometer limit determined for personal use.") + + # Kişisel Kullanılan KM + personal_km_used = fields.Float(string="Kişisel Kullanım KM (Bu Ay)", default=0.0) + + # Eve Git/Gel İzni + commute_allowed = fields.Selection([ + ('allowed', 'Allowed'), + ('forbidden', 'Forbidden') + ], string="Commute Allowed", required=True, default='forbidden') + + # Cihaz İzni Yönetimi (Madde 5 referansı) + personal_device_gps_allowed = fields.Boolean(string="Personal Device GPS Allowed") + company_phone_device_id = fields.Char(string="Company Phone Device ID (UUID)") + + # Sürücü Skor Kartı (Gamification) + driver_score = fields.Integer(string="Driver Score Card (0-100)", default=100, tracking=True) + + def write(self, vals): + if 'driver_score' in vals: + # Puan 0 ile 100 arasında sınırlandırılsın + vals['driver_score'] = max(0, min(100, vals['driver_score'])) + if vals['driver_score'] < 70: + vals['commute_allowed'] = 'forbidden' + for record in self: + if record.commute_allowed != 'forbidden': + record.message_post( + body=f"🚨 KRİTİK GÜVENLİK ALARMI: Sürücü skoru 70'in altına düştü ({vals['driver_score']})! Eve Git/Gel izni otonom olarak askıya alınmıştır!" + ) + return super(MymachFleetContract, self).write(vals) + + @api.depends('driver_id') + def _compute_employee(self): + """ + Sürücü seçildiğinde (res.partner), bağlantılı olan İK çalışanını (hr.employee) tespit eder. + Bu sayede departman bilgisi otomatik çekilir. + """ + for record in self: + if record.driver_id: + employee = self.env['hr.employee'].search([ + '|', + ('work_contact_id', '=', record.driver_id.id), + ('user_id.partner_id', '=', record.driver_id.id) + ], limit=1) + record.employee_id = employee.id if employee else False + else: + record.employee_id = False diff --git a/addons/mymach_fleet_intelligence/models/fleet_fine.py b/addons/mymach_fleet_intelligence/models/fleet_fine.py new file mode 100644 index 0000000..85aac07 --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/fleet_fine.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +import datetime +from odoo import models, fields, api + +class MymachFleetFine(models.Model): + """ + Otonom HGS, Ceza ve Park Gider Yönetimi Modeli + Güvenlik ihlalleriyle entegre olarak giderlerin şahsi mi yoksa şirket gideri mi olduğunu saptar. + """ + _name = 'mymach.fleet.fine' + _description = 'Filo Trafik Ceza ve HGS Yönetimi' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string="Charge/Transit Reference", required=True, tracking=True) + charge_type = fields.Selection([ + ('tax', 'Vergi Gideri'), + ('insurance', 'Zorunlu Trafik Sigortası'), + ('casco', 'Kasko Sigortası'), + ('repair', 'Onarım / Hasar Gideri'), + ('maintenance', 'Bakım Gideri'), + ('rental', 'Kira Masrafı'), + ('hgs', 'HGS / Otoyol Geçiş Ücreti'), + ('fine', 'Trafik Cezası'), + ('parking', 'Otopark Ücreti') + ], string="Masraf Türü", required=True, default='hgs', tracking=True) + + vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True, tracking=True) + driver_id = fields.Many2one('res.partner', string="Driver", related='vehicle_id.driver_id', store=True) + + amount = fields.Monetary(string="Amount", currency_field='currency_id', required=True, tracking=True) + currency_id = fields.Many2one('res.currency', string="Currency", default=lambda self: self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id) + date = fields.Date(string="Transaction Date", default=fields.Date.context_today, required=True, tracking=True) + + is_personal_violation = fields.Boolean( + string="Personal Use Violation?", + compute="_compute_personal_violation", + store=True, + tracking=True + ) + + state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('deducted', 'Deducted to Payroll/Current') + ], string="Status", default='draft', tracking=True) + + @api.depends('vehicle_id', 'date') + def _compute_personal_violation(self): + """ + Eğer ceza gününde araçla ilgili bir güvenlik ihlali logu (in_office_moving veya exception) varsa + otomatik olarak şahsi ihlal olarak işaretlenir. + """ + for record in self: + if record.vehicle_id and record.date: + # O gün bu araca ait ihlal logu var mı? + date_start = datetime.datetime.combine(record.date, datetime.time.min) + date_end = datetime.datetime.combine(record.date, datetime.time.max) + log_exists = self.env['mymach.fleet.security.log'].search([ + ('vehicle_id', '=', record.vehicle_id.id), + ('status', 'in', ['in_office_moving', 'exception']), + ('datetime', '>=', fields.Datetime.to_string(date_start)), + ('datetime', '<=', fields.Datetime.to_string(date_end)) + ], limit=1) + record.is_personal_violation = bool(log_exists) + else: + record.is_personal_violation = False + + def action_confirm(self): + self.write({'state': 'confirmed'}) + + def action_deduct_to_payroll(self): + """ + Eğer şahsi ihlalse, tutarı bordro/cari borç olarak yansıtır. + Burada basitlik adına ilgili güvenlik loguna ceza bedeli olarak yazar ve durumu güncelleriz. + """ + for record in self: + if not record.is_personal_violation: + continue + + date_start = datetime.datetime.combine(record.date, datetime.time.min) + date_end = datetime.datetime.combine(record.date, datetime.time.max) + log = self.env['mymach.fleet.security.log'].search([ + ('vehicle_id', '=', record.vehicle_id.id), + ('status', 'in', ['in_office_moving', 'exception']), + ('datetime', '>=', fields.Datetime.to_string(date_start)), + ('datetime', '<=', fields.Datetime.to_string(date_end)) + ], limit=1) + + if log: + log.write({'deduction_amount': record.amount}) + + if record.driver_id: + record.driver_id.message_post( + body=f"🚨 Şahsi Kullanım İhlal Cezası: {record.date} tarihinde gerçekleşen ihlal nedeniyle {record.amount} {record.currency_id.name} tutarındaki ceza/ücret cari hesabınıza borç olarak yansıtılmıştır." + ) + + record.write({'state': 'deducted'}) diff --git a/addons/mymach_fleet_intelligence/models/fleet_handover.py b/addons/mymach_fleet_intelligence/models/fleet_handover.py new file mode 100644 index 0000000..3a0d7a1 --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/fleet_handover.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +import datetime +import random +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class MymachFleetHandover(models.Model): + """ + Araç Teslim Alım / Teslim Etme (Handover) Takip Modeli + Personelin araç alırken/bırakırken km ve yakıt bilgilerini girip fotoğraf yüklemesini sağlar. + Yöneticinin bu verileri karşılaştırıp denetlemesini sağlar. + """ + _name = 'mymach.fleet.handover' + _description = 'Araç Teslim Alım ve İade Takibi' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'id desc' + + name = fields.Char(string="Handover Reference", required=True, copy=False, readonly=True, default="/") + + vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True, tracking=True) + driver_id = fields.Many2one('res.partner', string="Driver (Employee)", required=True, tracking=True, + default=lambda self: self.env.user.partner_id) + + state = fields.Selection([ + ('draft', 'Draft'), + ('taken', 'Handed Over (In Use)'), + ('returned', 'Returned'), + ('compared', 'Inspected / Approved') + ], string="Status", default='draft', required=True, tracking=True) + + # --- TAKE (START) DETAILS --- + take_datetime = fields.Datetime(string="Take Datetime", tracking=True) + start_odometer = fields.Float(string="Start Odometer", tracking=True) + start_fuel_level = fields.Float(string="Start Fuel Level (%)", help="E.g. 50 (%50 Fuel)", tracking=True) + start_photo = fields.Binary(string="Take Photo", attachment=True) + + # --- RETURN (END) DETAILS --- + return_datetime = fields.Datetime(string="Return Datetime", tracking=True) + end_odometer = fields.Float(string="End Odometer", tracking=True) + end_fuel_level = fields.Float(string="End Fuel Level (%)", help="E.g. 30 (%30 Fuel)", tracking=True) + end_photo = fields.Binary(string="Return Photo", attachment=True) + + # --- MANAGER INSPECTION DETAILS --- + odometer_diff = fields.Float(string="Odometer Diff (KM)", compute="_compute_diffs", store=True) + fuel_diff = fields.Float(string="Fuel Diff (%)", compute="_compute_diffs", store=True) + manager_note = fields.Text(string="Manager Inspection Notes", tracking=True) + is_approved = fields.Boolean(string="Inspection Approval", tracking=True) + + @api.depends('start_odometer', 'end_odometer', 'start_fuel_level', 'end_fuel_level') + def _compute_diffs(self): + for record in self: + record.odometer_diff = record.end_odometer - record.start_odometer if record.end_odometer else 0.0 + record.fuel_diff = record.end_fuel_level - record.start_fuel_level if record.end_fuel_level else 0.0 + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get('name') or vals.get('name') == '/': + today_str = datetime.date.today().strftime('%Y%m%d') + rand_id = random.randint(1000, 9999) + vals['name'] = f"HO/{today_str}/{rand_id}" + return super(MymachFleetHandover, self).create(vals_list) + + def action_take(self): + """ + Personel aracı teslim alır. Araç bilgilerini günceller. + """ + self.ensure_one() + if not self.take_datetime: + self.take_datetime = fields.Datetime.now() + if not self.start_odometer or not self.start_photo: + raise UserError(_("Aracı teslim alabilmek için başlangıç kilometresi ve fotoğrafı girmelisiniz!")) + + # Araç kilometresini güncelle (odometer kaydı oluşturarak) + self.env['fleet.vehicle.odometer'].create({ + 'vehicle_id': self.vehicle_id.id, + 'value': self.start_odometer, + 'date': fields.Date.context_today(self) + }) + + # Yakıt seviyesini güncelle (Litreye dönüştür) + fuel_liter = (self.start_fuel_level / 100.0) * self.vehicle_id.tank_capacity + self.vehicle_id.sudo().write({'current_fuel_level': min(fuel_liter, self.vehicle_id.tank_capacity)}) + + self.write({'state': 'taken'}) + self.message_post(body=f"🚗 Araç Teslim Alındı. Başlangıç KM: {self.start_odometer}, Yakıt: %{self.start_fuel_level}") + + def action_return(self): + """ + Personel aracı iade eder. Araç bilgilerini günceller. + """ + self.ensure_one() + if not self.return_datetime: + self.return_datetime = fields.Datetime.now() + if not self.end_odometer or not self.end_photo: + raise UserError(_("Aracı iade edebilmek için bitiş kilometresi ve fotoğrafı girmelisiniz!")) + if self.end_odometer < self.start_odometer: + raise UserError(_("İade kilometresi, teslim alış kilometresinden düşük olamaz!")) + + # Araç kilometresini güncelle (odometer kaydı oluşturarak) + self.env['fleet.vehicle.odometer'].create({ + 'vehicle_id': self.vehicle_id.id, + 'value': self.end_odometer, + 'date': fields.Date.context_today(self) + }) + + # Yakıt seviyesini güncelle (Litreye dönüştür) + fuel_liter = (self.end_fuel_level / 100.0) * self.vehicle_id.tank_capacity + self.vehicle_id.sudo().write({'current_fuel_level': min(fuel_liter, self.vehicle_id.tank_capacity)}) + + self.write({'state': 'returned'}) + self.message_post(body=f"🔋 Araç İade Edildi. Bitiş KM: {self.end_odometer}, Yakıt: %{self.end_fuel_level}. Toplam yol: {self.odometer_diff} KM") + + def action_compare(self): + """ + Yönetici denetimi onaylar. + """ + self.ensure_one() + self.write({ + 'state': 'compared', + 'is_approved': True + }) + self.message_post(body=f"✅ Yönetici Denetimi Tamamlandı. Onay: Evet. Not: {self.manager_note or 'Yok'}") + + @api.model + def action_purge_expired_photos(self): + """ + Faz 9 / Otonom Depo Temizliği: + İade edilme tarihi üzerinden 10 gün geçen kayıtların fotoğraflarını veritabanından siler. + """ + limit_date = fields.Datetime.now() - datetime.timedelta(days=10) + records = self.search([ + ('state', 'in', ['returned', 'compared']), + ('return_datetime', '<=', limit_date), + '|', + ('start_photo', '!=', False), + ('end_photo', '!=', False) + ]) + count = len(records) + for record in records: + record.write({ + 'start_photo': False, + 'end_photo': False + }) + record.message_post( + body="🗑️ Otonom Depo Temizliği: Veritabanı boyutunu korumak amacıyla 10 günden eski teslimat fotoğrafları otonom olarak silinmiştir." + ) + return count diff --git a/addons/mymach_fleet_intelligence/models/fleet_reservation.py b/addons/mymach_fleet_intelligence/models/fleet_reservation.py new file mode 100644 index 0000000..8572c6a --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/fleet_reservation.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +from odoo.exceptions import ValidationError + +class MymachFleetReservation(models.Model): + """ + Faz 5: Araç Talep ve Rezervasyon Sistemi + Personelin araçları belirli tarih aralıklarında talep etmesini sağlar. + """ + _name = 'mymach.fleet.reservation' + _description = 'Araç Rezervasyonu' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'start_datetime desc' + + name = fields.Char(string="Request No", required=True, copy=False, readonly=True, default=lambda self: 'New Request') + employee_id = fields.Many2one('hr.employee', string="Requested Employee", required=True, default=lambda self: self.env.user.employee_id) + vehicle_id = fields.Many2one('fleet.vehicle', string="Requested Vehicle", required=True) + + start_datetime = fields.Datetime(string="Start Datetime", required=True, tracking=True) + end_datetime = fields.Datetime(string="End Datetime", required=True, tracking=True) + + purpose = fields.Text(string="Purpose / Justification", required=True) + destination_location = fields.Char(string="Hedef Görev Konumu / Proje Sahası", help="Personelin seçeceği görev konumu") + + state = fields.Selection([ + ('draft', 'Draft'), + ('submitted', 'Onay Bekliyor (Yöneticiye Bildirildi)'), + ('approved', 'Approved (Reserved & Live Map Updated)'), + ('rejected', 'Rejected'), + ('done', 'Completed / Handed Over') + ], string="Status", default='draft', tracking=True, required=True) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', 'Yeni Talep') == 'Yeni Talep': + vals['name'] = self.env['ir.sequence'].next_by_code('mymach.fleet.reservation') or 'Talep' + return super(MymachFleetReservation, self).create(vals_list) + + @api.constrains('start_datetime', 'end_datetime', 'vehicle_id', 'state') + def _check_overlap(self): + """ + Overlap (Zaman Çakışması) Kontrolü. Aynı araca aynı saatte randevu alınamaz. + """ + for record in self: + if record.state in ['approved', 'done']: + overlapping_reservations = self.search([ + ('id', '!=', record.id), + ('vehicle_id', '=', record.vehicle_id.id), + ('state', 'in', ['approved', 'done']), + ('start_datetime', '<', record.end_datetime), + ('end_datetime', '>', record.start_datetime) + ]) + if overlapping_reservations: + raise ValidationError(f"Seçtiğiniz tarihlerde '{record.vehicle_id.license_plate}' plakalı araç zaten rezerve edilmiştir! Çakışan talep: {overlapping_reservations[0].name}") + + def action_submit(self): + self.write({'state': 'submitted'}) + for record in self: + record.message_post( + body=f"📩 YÖNETİCİYE GÖREV BİLDİRİMİ: {record.employee_id.name} personeli '{record.vehicle_id.license_plate}' aracı ile '{record.destination_location or record.purpose}' konumuna görevlendirme talebi oluşturdu. Yönetici onayı bekleniyor." + ) + + def action_approve(self): + """ + Admin (Yönetici) talebi onaylar ve Takvim Aktivitesi atar. + """ + self.write({'state': 'approved'}) + for record in self: + # Aracı teslim edecek yöneticiye (veya aracı talep eden çalışanın bağlı olduğu kullanıcıya) aktivite aç + user_id = self.env.user.id + record.activity_schedule( + 'mail.mail_activity_data_todo', + summary=f'Araç Teslimatı: {record.vehicle_id.license_plate}', + note=f'{record.employee_id.name} adlı çalışana aracı teslim edin. Gerekçe: {record.purpose}', + user_id=user_id, + date_deadline=fields.Date.to_date(record.start_datetime) + ) + + def action_reject(self): + self.write({'state': 'rejected'}) + + def action_done(self): + self.write({'state': 'done'}) diff --git a/addons/mymach_fleet_intelligence/models/fleet_vehicle.py b/addons/mymach_fleet_intelligence/models/fleet_vehicle.py new file mode 100644 index 0000000..49baadf --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/fleet_vehicle.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +from datetime import timedelta + +class FleetVehicle(models.Model): + """ + Odoo Standart fleet.vehicle Modeli Uzantısı + Döviz stabilizasyonu ve hareketli ortalama bakım hesaplamalarını otonom olarak yapar. + """ + _inherit = 'fleet.vehicle' + + # Alt Görev 2.2: Akıllı Maliyet Tablosu ve Döviz Stabilizasyonu + purchase_value = fields.Monetary(string="Purchase Value", currency_field='currency_id') + depreciation_years = fields.Integer(string="Depreciation Years", default=5) + annual_fixed_cost = fields.Float(string="Annual Fixed Cost") + index_currency_id = fields.Many2one('res.currency', string="Index Currency (Depreciation)", + default=lambda self: self.env.ref('base.TRY').id if self.env.ref('base.TRY', raise_if_not_found=False) else False) + + # 3 Para Birimli Amortisman Maliyet Alanları ve Ortalama Değer + depreciation_cost_try = fields.Float(string="Amortisman Bedeli (TRY)", default=0.0) + depreciation_cost_usd = fields.Float(string="Amortisman Bedeli (USD)", default=0.0) + depreciation_cost_eur = fields.Float(string="Amortisman Bedeli (EUR)", default=0.0) + depreciation_cost_avg = fields.Float(string="Ortalama Amortisman (TRY)", compute='_compute_depreciation_avg', store=True) + + # Son Bakım Kilometresi ve Tarihi + last_maintenance_odometer = fields.Float(string="Son Bakım Kilometresi (KM)", default=0.0) + last_maintenance_date = fields.Date(string="Son Bakım Tarihi") + + # KM Maliyeti + km_cost = fields.Float(string="Kilometre Başına Maliyet (TRY)", default=5.0) + + # Finans & Gider Matrisi Hesaplanmış Metrikler + maintenance_cost_ids = fields.One2many('mymach.fleet.maintenance.cost', 'vehicle_id', string="Gider Kalemleri") + total_expense_amount = fields.Float(string="Toplam Gider Tutarı (TRY)", compute="_compute_financial_metrics", store=True) + total_driven_odometer = fields.Float(string="Toplam Kat Edilen Yol (KM)", compute="_compute_financial_metrics", store=True) + cost_per_km_calculated = fields.Float(string="KM Başına Maliyet (TRY/KM)", compute="_compute_financial_metrics", store=True) + annual_total_cost_calculated = fields.Float(string="Yıllık Toplam Maliyet (TRY/Yıl)", compute="_compute_financial_metrics", store=True) + monthly_avg_cost_calculated = fields.Float(string="Aylık Ortalama Maliyet (TRY/Ay)", compute="_compute_financial_metrics", store=True) + + # Çoklu Para Birimi ve Listbox Seçimleri (TL Ana Para Birimi + 2 Seçilebilir Döviz + 3'lü Ortalama) + try_currency_id = fields.Many2one('res.currency', string="TRY Para Birimi", default=lambda self: self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id) + cost_curr1_id = fields.Many2one('res.currency', string="Seçilen 2. Para Birimi (Döviz 1)", default=lambda self: self.env.ref('base.USD', raise_if_not_found=False)) + cost_curr2_id = fields.Many2one('res.currency', string="Seçilen 3. Para Birimi (Döviz 2)", default=lambda self: self.env.ref('base.EUR', raise_if_not_found=False)) + active_display_currency_id = fields.Many2one('res.currency', string="Aktif Gösterim Para Birimi", compute="_compute_financial_metrics") + cost_display_mode = fields.Selection([ + ('try', '🇹🇷 Ana Para Birimi (TL)'), + ('curr1', '💵 2. Para Birimi (Döviz 1)'), + ('curr2', '💶 3. Para Birimi (Döviz 2)'), + ('average', '📊 3 Para Birimli Ortalama Tahminî Değer (TL)') + ], string="Para Birimi Gösterim Modu (Listbox)", default='average') + + cost_per_km_avg_display = fields.Float(string="KM Başına Ortalama Tahminî Değer", compute="_compute_financial_metrics") + annual_cost_avg_display = fields.Float(string="Yıllık Ortalama Tahminî Değer", compute="_compute_financial_metrics") + monthly_cost_avg_display = fields.Float(string="Aylık Ortalama Tahminî Değer", compute="_compute_financial_metrics") + + # Çoklu Para Birimi Karşılaştırması & 3 Dövizli Ortalama Tahmini Değer Alanları + cost_per_km_try = fields.Monetary(string="KM Başına (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + annual_cost_try = fields.Monetary(string="Yıllık (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + monthly_cost_try = fields.Monetary(string="Aylık (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + + cost_per_km_c1 = fields.Monetary(string="KM Başına (Döviz 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_id') + annual_cost_c1 = fields.Monetary(string="Yıllık (Döviz 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_id') + monthly_cost_c1 = fields.Monetary(string="Aylık (Döviz 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_id') + + cost_per_km_c2 = fields.Monetary(string="KM Başına (Döviz 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_id') + annual_cost_c2 = fields.Monetary(string="Yıllık (Döviz 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_id') + monthly_cost_c2 = fields.Monetary(string="Aylık (Döviz 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_id') + + cost_per_km_avg = fields.Monetary(string="KM Başına Ortalama (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + annual_cost_avg = fields.Monetary(string="Yıllık Ortalama (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + monthly_cost_avg = fields.Monetary(string="Aylık Ortalama (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id') + + + # Yakıt Türü Seçenekleri Özelleştirme (Benzin / LPG Dahil) + fuel_type = fields.Selection([ + ('gasoline', 'Benzinli'), + ('diesel', 'Dizel'), + ('electric', 'Tam Elektrikli (EV)'), + ('lpg', 'Benzin / LPG'), + ('gasoline_lpg', 'Benzin / LPG'), + ('hybrid_gasoline', 'Hibrit (Elektrik / Benzin)'), + ('hybrid_diesel', 'Hibrit (Elektrik / Dizel)') + ], string="Yakıt Türü", default='diesel') + + # Faz 8: Akaryakıt API ve Prediktif Bakım + tank_capacity = fields.Float(string="Fuel Tank Capacity (Liters)", default=50.0) + current_fuel_level = fields.Float(string="Current Fuel Level (Liters)", readonly=True) + needs_predictive_maintenance = fields.Boolean(string="Predictive Maintenance Warning (AI)", default=False, readonly=True) + factory_avg_consumption = fields.Float(string="Factory Average Consumption (L/100KM)", default=6.0) + + indexed_purchase_value = fields.Monetary( + string="Indexed Purchase Value", + compute="_compute_indexed_purchase_value", + currency_field='index_currency_id' + ) + + moving_average_maintenance_cost = fields.Monetary( + string="Moving Average Maintenance Cost", + compute="_compute_moving_average", + currency_field='currency_id' + ) + is_maintenance_critical = fields.Boolean( + string="Critical Cost Alarm", + compute="_compute_moving_average", + search="_search_is_maintenance_critical" + ) + + @api.depends('maintenance_cost_ids.amount', 'maintenance_cost_ids.date', 'odometer', 'annual_fixed_cost', 'cost_curr1_id', 'cost_curr2_id', 'cost_display_mode') + def _compute_financial_metrics(self): + current_year = fields.Date.today().year + try_curr = self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id + + for vehicle in self: + costs = vehicle.maintenance_cost_ids + total_amount_try = sum(costs.mapped('amount')) if costs else 0.0 + + odo = vehicle.odometer or 1.0 + if odo <= 0: + odo = 1.0 + + vehicle.total_expense_amount = total_amount_try + vehicle.total_driven_odometer = odo + + # TL Değerleri + cpk_tl = total_amount_try / odo if odo > 0 else 0.0 + this_year_costs_tl = sum(c.amount for c in costs if c.date and c.date.year == current_year) + annual_tl = this_year_costs_tl + (vehicle.annual_fixed_cost or 0.0) + if annual_tl == 0.0 and total_amount_try > 0: + annual_tl = total_amount_try + monthly_tl = annual_tl / 12.0 + + vehicle.cost_per_km_calculated = cpk_tl + vehicle.annual_total_cost_calculated = annual_tl + vehicle.monthly_avg_cost_calculated = monthly_tl + + # Döviz 1 ve Döviz 2 Dönüştürme + curr1 = vehicle.cost_curr1_id or self.env.ref('base.USD', raise_if_not_found=False) + curr2 = vehicle.cost_curr2_id or self.env.ref('base.EUR', raise_if_not_found=False) + + cpk_c1 = try_curr._convert(cpk_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + cpk_c2 = try_curr._convert(cpk_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + annual_c1 = try_curr._convert(annual_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + annual_c2 = try_curr._convert(annual_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + monthly_c1 = try_curr._convert(monthly_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + monthly_c2 = try_curr._convert(monthly_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + # 3 Para Biriminin Ortalama Tahminî Değer Dönüştürmesi (Çapraz Kur Ortalama TL) + c1_back_try = curr1._convert(cpk_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else cpk_tl + c2_back_try = curr2._convert(cpk_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else cpk_tl + cpk_avg = (cpk_tl + c1_back_try + c2_back_try) / 3.0 + + ann_c1_try = curr1._convert(annual_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else annual_tl + ann_c2_try = curr2._convert(annual_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else annual_tl + annual_avg = (annual_tl + ann_c1_try + ann_c2_try) / 3.0 + + mon_c1_try = curr1._convert(monthly_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else monthly_tl + mon_c2_try = curr2._convert(monthly_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else monthly_tl + monthly_avg = (monthly_tl + mon_c1_try + mon_c2_try) / 3.0 + + mode = vehicle.cost_display_mode or 'average' + c1_name = curr1.name if curr1 else 'USD' + c2_name = curr2.name if curr2 else 'EUR' + + if mode == 'try': + vehicle.cost_per_km_avg_display = cpk_tl + vehicle.annual_cost_avg_display = annual_tl + vehicle.monthly_cost_avg_display = monthly_tl + vehicle.active_display_currency_id = try_curr + elif mode == 'curr1': + vehicle.cost_per_km_avg_display = cpk_c1 + vehicle.annual_cost_avg_display = annual_c1 + vehicle.monthly_cost_avg_display = monthly_c1 + vehicle.active_display_currency_id = curr1 + elif mode == 'curr2': + vehicle.cost_per_km_avg_display = cpk_c2 + vehicle.annual_cost_avg_display = annual_c2 + vehicle.monthly_cost_avg_display = monthly_c2 + vehicle.active_display_currency_id = curr2 + else: # 'average' + vehicle.cost_per_km_avg_display = cpk_avg + vehicle.annual_cost_avg_display = annual_avg + vehicle.monthly_cost_avg_display = monthly_avg + vehicle.active_display_currency_id = try_curr + + # Alanlara atama yap (HTML yerine) + vehicle.cost_per_km_try = cpk_tl + vehicle.annual_cost_try = annual_tl + vehicle.monthly_cost_try = monthly_tl + + vehicle.cost_per_km_c1 = cpk_c1 + vehicle.annual_cost_c1 = annual_c1 + vehicle.monthly_cost_c1 = monthly_c1 + + vehicle.cost_per_km_c2 = cpk_c2 + vehicle.annual_cost_c2 = annual_c2 + vehicle.monthly_cost_c2 = monthly_c2 + + vehicle.cost_per_km_avg = cpk_avg + vehicle.annual_cost_avg = annual_avg + vehicle.monthly_cost_avg = monthly_avg + + @api.depends('depreciation_cost_try', 'depreciation_cost_usd', 'depreciation_cost_eur') + def _compute_depreciation_avg(self): + usd = self.env.ref('base.USD', raise_if_not_found=False) + eur = self.env.ref('base.EUR', raise_if_not_found=False) + try_curr = self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id + + for vehicle in self: + val_try = vehicle.depreciation_cost_try or 0.0 + + val_usd_try = 0.0 + if vehicle.depreciation_cost_usd and usd: + val_usd_try = usd._convert( + vehicle.depreciation_cost_usd, + try_curr, + vehicle.company_id or self.env.company, + fields.Date.today() + ) + + val_eur_try = 0.0 + if vehicle.depreciation_cost_eur and eur: + val_eur_try = eur._convert( + vehicle.depreciation_cost_eur, + try_curr, + vehicle.company_id or self.env.company, + fields.Date.today() + ) + + vehicle.depreciation_cost_avg = (val_try + val_usd_try + val_eur_try) / 3.0 + + @api.model_create_multi + def create(self, vals_list): + vehicles = super(FleetVehicle, self).create(vals_list) + for vehicle in vehicles: + # Araç ilk oluşturulduğunda otonom ilk bakımı (küçük bakım) planla + interval = 10000 if vehicle.fuel_type == 'diesel' else 15000 + self.env['mymach.fleet.periodic.maintenance'].create({ + 'vehicle_id': vehicle.id, + 'maintenance_type': 'small', + 'last_odometer': 0.0, + 'target_odometer': interval, + 'target_date': fields.Date.today() + timedelta(days=365) + }) + return vehicles + + @api.depends('purchase_value', 'currency_id', 'index_currency_id') + def _compute_indexed_purchase_value(self): + """ + Alım bedelini, güncel Merkez Bankası kurları (Odoo'nun döviz motoru) üzerinden + seçilen endeks dövize (örn: USD/EUR) çevirir (Madde 8 & 9). + """ + for vehicle in self: + if vehicle.purchase_value and vehicle.currency_id and vehicle.index_currency_id: + vehicle.indexed_purchase_value = vehicle.currency_id._convert( + vehicle.purchase_value, + vehicle.index_currency_id, + vehicle.company_id or self.env.company, + fields.Date.today() + ) + else: + vehicle.indexed_purchase_value = 0.0 + + def _compute_moving_average(self): + """ + Öğrenen / Hareketli Ortalama (Moving Average) + Araç için girilen bakım/onarım faturaları (maintenance_cost) toplandıkça otonom güncellenir. + """ + for vehicle in self: + costs = self.env['mymach.fleet.maintenance.cost'].search([('vehicle_id', '=', vehicle.id)]) + total = sum(costs.mapped('amount')) if costs else 0.0 + vehicle.moving_average_maintenance_cost = total / len(costs) if costs else 0.0 + + # Finansal Zeka: Bakım maliyeti toplamı, aracın güncel (endeksli) alım bedelinin %15'ini aştı mı? + if vehicle.indexed_purchase_value > 0 and total > 0: + ratio = total / vehicle.indexed_purchase_value + vehicle.is_maintenance_critical = True if ratio >= 0.15 else False + else: + vehicle.is_maintenance_critical = False + + def _search_is_maintenance_critical(self, operator, value): + if operator not in ('=', '!='): + raise ValueError("Desteklenmeyen arama operatörü!") + + critical_vehicles = [] + vehicles = self.search([]) + for v in vehicles: + costs = self.env['mymach.fleet.maintenance.cost'].search([('vehicle_id', '=', v.id)]) + total = sum(costs.mapped('amount')) if costs else 0.0 + if v.indexed_purchase_value > 0 and total > 0: + ratio = total / v.indexed_purchase_value + is_critical = ratio >= 0.15 + else: + is_critical = False + + # Değeri karşılaştır (value True veya False olabilir) + match = is_critical if value else not is_critical + if (operator == '=' and match) or (operator == '!=' and not match): + critical_vehicles.append(v.id) + + return [('id', 'in', critical_vehicles)] + + + + + +class FleetVehicleLogServices(models.Model): + """ + Odoo Standart fleet.vehicle.log.services Modeli Uzantısı + """ + _inherit = 'fleet.vehicle.log.services' + + # Alt Görev 2.3: Hibrit Yakıt Girişi ve Veri Modelleme + fuel_entry_method = fields.Selection([ + ('manual', 'Manuel Giriş'), + ('mymach_ocr', 'MyMach Akıllı Fiş İşleme Motoru'), + ('hr_expense', 'Odoo Masraf Modülü') + ], string="Yakıt Giriş Yöntemi", default='manual', required=True, + help="Eğer harici modüller (OCR veya hr_expense) kurulu değilse 'manual' mod varsayılan olarak kullanılır.") + diff --git a/addons/mymach_fleet_intelligence/models/home.py b/addons/mymach_fleet_intelligence/models/home.py new file mode 100644 index 0000000..e7c0a1a --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/home.py @@ -0,0 +1,735 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + +class MymachFleetHome(models.Model): + """ + Faz 6: Estetik Ana Sayfa (Dashboard) + Bu tablo veritabanında kalıcı olarak 1 kayıt oluşturur + ve kullanıcıya butonlardan oluşan harika bir HTML form ekranı sunar. + """ + _name = 'mymach.fleet.home' + _description = 'MyMach Filo Zekası Ana Pano' + + name = fields.Char(default="Ana Sayfa", string="Başlık") + display_title = fields.Char(compute='_compute_display_title') + upcoming_maintenance_html = fields.Html(compute='_compute_upcoming_maintenance_html') + + # Grafik Filtreleri ve HTML Tuvali + selected_vehicle_id = fields.Many2one('fleet.vehicle', string="Seçilen Araç") + time_range = fields.Selection([ + ('week', 'Son 7 Gün (Haftalık)'), + ('month', 'Son 30 Gün (Aylık)'), + ('year', 'Son 12 Ay (Yıllık)') + ], string="Zaman Aralığı", default='month', required=True) + chart_html = fields.Html(compute='_compute_chart_html', string="Yakıt & KM Grafiği", sanitize=False) + chart_metric = fields.Selection([ + ('combined', 'Birleşik Analiz (KM & Yakıt Hacmi)'), + ('km', 'Kilometre Analizi (KM)'), + ('fuel_liters', 'Yakıt Tüketimi (Litre)'), + ('fuel_cost', 'Yakıt Maliyeti (TL / Gider)') + ], string="Grafik Modu", default='combined', required=True) + + # Rota Planlama ve Sapma Kontrolü Alanları + route_destination = fields.Char(string="Hedef Konum", default="İstanbul Havalimanı") + route_selected = fields.Selection([ + ('route_a', 'Rota A (En Kısa - 15.2 KM)'), + ('route_b', 'Rota B (Ekonomik - 16.5 KM)'), + ('route_c', 'Rota C (Alternatif - 18.0 KM)') + ], string="Seçilen Rota", default='route_a') + route_simulated_distance = fields.Float(string="Simüle Edilen Yolculuk Mesafesi (KM)", default=15.2) + route_results_html = fields.Html(string="Alternatif Rotalar Sonucu", sanitize=False) + + # Finans & Gider Matrisi Özel Alanları + cost_matrix_vehicle_id = fields.Many2one('fleet.vehicle', string="Gider Analizi İçin Araç Seçimi") + cost_per_km_matrix = fields.Float(string="KM BAŞINA MALİYET", compute="_compute_cost_matrix_summary") + annual_total_cost_matrix = fields.Float(string="YILLIK TOPLAM MALİYET", compute="_compute_cost_matrix_summary") + monthly_avg_cost_matrix = fields.Float(string="AYLIK ORTALAMA MALİYET", compute="_compute_cost_matrix_summary") + cost_matrix_cards_html = fields.Html(string="Gider Matrisi Başlık Kartları", compute="_compute_cost_matrix_summary", sanitize=False) + + @api.depends('cost_matrix_vehicle_id', 'selected_vehicle_id') + def _compute_cost_matrix_summary(self): + current_year = fields.Date.today().year + try_curr = self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id + + for record in self: + vehicle = record.cost_matrix_vehicle_id or record.selected_vehicle_id or self.env['fleet.vehicle'].search([], limit=1) + if not vehicle: + record.cost_per_km_matrix = 0.0 + record.annual_total_cost_matrix = 0.0 + record.monthly_avg_cost_matrix = 0.0 + record.cost_matrix_cards_html = "
Araç seçilmedi.
" + continue + + costs = self.env['mymach.fleet.maintenance.cost'].search([('vehicle_id', '=', vehicle.id)]) + total_amount_try = sum(costs.mapped('amount')) if costs else 0.0 + + odo = vehicle.odometer or vehicle.total_driven_odometer or 1.0 + if odo <= 0: + odo = 1.0 + + cpk_tl = total_amount_try / odo if odo > 0 else 0.0 + this_year_costs_tl = sum(c.amount for c in costs if c.date and c.date.year == current_year) + annual_tl = this_year_costs_tl + (vehicle.annual_fixed_cost or 0.0) + if annual_tl == 0.0 and total_amount_try > 0: + annual_tl = total_amount_try + monthly_tl = annual_tl / 12.0 + + # Döviz Conversions & Average + curr1 = vehicle.cost_curr1_id or self.env.ref('base.USD', raise_if_not_found=False) + curr2 = vehicle.cost_curr2_id or self.env.ref('base.EUR', raise_if_not_found=False) + + cpk_c1 = try_curr._convert(cpk_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + cpk_c2 = try_curr._convert(cpk_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + annual_c1 = try_curr._convert(annual_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + annual_c2 = try_curr._convert(annual_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + monthly_c1 = try_curr._convert(monthly_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0 + monthly_c2 = try_curr._convert(monthly_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0 + + c1_back_try = curr1._convert(cpk_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else cpk_tl + c2_back_try = curr2._convert(cpk_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else cpk_tl + cpk_avg = (cpk_tl + c1_back_try + c2_back_try) / 3.0 + + ann_c1_try = curr1._convert(annual_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else annual_tl + ann_c2_try = curr2._convert(annual_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else annual_tl + annual_avg = (annual_tl + ann_c1_try + ann_c2_try) / 3.0 + + mon_c1_try = curr1._convert(monthly_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else monthly_tl + mon_c2_try = curr2._convert(monthly_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else monthly_tl + monthly_avg = (monthly_tl + mon_c1_try + mon_c2_try) / 3.0 + + record.cost_per_km_matrix = cpk_tl + record.annual_total_cost_matrix = annual_tl + record.monthly_avg_cost_matrix = monthly_tl + + c1_name = curr1.name if curr1 else 'USD' + c2_name = curr2.name if curr2 else 'EUR' + + # High Impact Dashboard HTML Cards with Currency Selection Matrix + html = f""" +
+

+ 🚘 {vehicle.license_plate} ({vehicle.name or ""}) - FİNANSAL GİDER MATRİSİ ANALİZİ +

+
+
+
+
BİR KM BAŞINA MALİYET
+
{cpk_tl:,.2f} ₺ / KM
+
Tahminî Ortalama: {cpk_avg:,.2f} ₺
+
+
+
+
+
YILLIK TOPLAM MALİYET
+
{annual_tl:,.2f} ₺ / YIL
+
Tahminî Ortalama: {annual_avg:,.2f} ₺
+
+
+
+
+
AYLIK ORTALAMA MALİYET
+
{monthly_tl:,.2f} ₺ / AY
+
Tahminî Ortalama: {monthly_avg:,.2f} ₺
+
+
+
+
+
+
+ 💱 3 Para Birimli Karşılaştırmalı Gider Listbox Tablosu +
+ Canlı Kur Dönüşümü +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrik Başlığı🇹🇷 TL (Ana Para Birimi)💵 {c1_name} (Döviz 1)💶 {c2_name} (Döviz 2)📊 3 Dövizli Ortalama Tahminî Değer (TL)
BİR KM BAŞINA MALİYET{cpk_tl:,.2f} ₺{cpk_c1:,.2f} {c1_name}{cpk_c2:,.2f} {c2_name}{cpk_avg:,.2f} ₺
YILLIK TOPLAM MALİYET{annual_tl:,.2f} ₺{annual_c1:,.2f} {c1_name}{annual_c2:,.2f} {c2_name}{annual_avg:,.2f} ₺
AYLIK ORTALAMA MALİYET{monthly_tl:,.2f} ₺{monthly_c1:,.2f} {c1_name}{monthly_c2:,.2f} {c2_name}{monthly_avg:,.2f} ₺
+
+
+
+ """ + record.cost_matrix_cards_html = html + + def _compute_display_title(self): + for record in self: + if self.env.user.has_group('mymach_fleet_intelligence.group_mymach_fleet_manager'): + record.display_title = 'YÖNETİCİ PORTALI / MANAGEMENT PANEL' + else: + record.display_title = 'PERSONEL PORTALI / DRIVER DASHBOARD' + + def _compute_upcoming_maintenance_html(self): + # Yaklaşan veya Süresi/KM'si geçmiş tüm bakımları tarihe ve KM hedefine göre sırala + maintenances = self.env['mymach.fleet.periodic.maintenance'].search([ + ('state', 'in', ['pending', 'overdue']) + ], order='target_date asc, target_odometer asc') + + if not maintenances: + html = """ +
+ + Yaklaşan veya süresi geçmiş periyodik bakım hatırlatıcısı bulunmamaktadır. +
+ """ + for record in self: + record.upcoming_maintenance_html = html + return + + html_lines = [] + html_lines.append('
') + html_lines.append('

⏳ Yaklaşan ve Geciken Periyodik Bakım Listesi

') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + html_lines.append('') + + for m in maintenances: + date_str = m.target_date.strftime('%d.%m.%Y') if m.target_date else '' + + if m.state == 'overdue': + badge_class = 'mymach_badge mymach_badge_danger' + status_text = '⚠️ Gecikmiş / Kritik' + row_style = 'border-left: 4px solid #ef4444; background: rgba(239, 68, 68, 0.02);' + else: + badge_class = 'mymach_badge mymach_badge_warning' + status_text = '⏳ Zamanı Geliyor' + row_style = 'border-left: 4px solid #f59e0b; background: rgba(245, 158, 11, 0.02);' + + m_label = dict(m._fields['maintenance_type'].selection).get(m.maintenance_type, 'Bakım') + warning_msg = f"Bu aracın bu bakıma {date_str} tarihinde ihtiyacı var." + + html_lines.append(f'') + html_lines.append(f'') + html_lines.append(f'') + html_lines.append(f'') + html_lines.append(f'') + html_lines.append(f'') + html_lines.append('') + + html_lines.append('') + html_lines.append('
Araç (Plaka)Bakım TürüHedef KilometreHedef TarihÖncelik & Bildirim Durumu
{m.vehicle_id.license_plate or ""}
{m.vehicle_id.name or ""}
{m_label}{m.target_odometer:.0f} KM
Mevcut: {m.odometer:.0f} KM
{date_str}{status_text}
{warning_msg}
') + html_lines.append('
') + + html_content = "".join(html_lines) + for record in self: + record.upcoming_maintenance_html = html_content + + @api.depends('selected_vehicle_id', 'time_range', 'chart_metric') + def _compute_chart_html(self): + import datetime + from datetime import timedelta + import json + import re + + for record in self: + vehicle = record.selected_vehicle_id or self.env['fleet.vehicle'].search([], limit=1) + if not vehicle: + record.chart_html = """ +
+ + Grafik gösterimi için sistemde kayıtlı aktif bir araç bulunamadı. +
+ """ + continue + + today = datetime.date.today() + labels = [] + date_list = [] + months_list = [] + + # 1. Zaman Aralığına Göre Tarih Listesi Hazırla + if record.time_range == 'week': + for i in range(6, -1, -1): + d = today - timedelta(days=i) + date_list.append(d) + labels.append(d.strftime('%d.%m')) + elif record.time_range == 'month': + for i in range(29, -1, -1): + d = today - timedelta(days=i) + date_list.append(d) + labels.append(d.strftime('%d.%m')) + elif record.time_range == 'year': + for i in range(11, -1, -1): + m = (today.month - i - 1) % 12 + 1 + y = today.year + (today.month - i - 1) // 12 + months_list.append((y, m)) + labels.append(f"{m:02d}.{y}") + + # 2. Olayları Tarihe Göre Topla (Bakım, Lastik Değişimi, Onarımlar vb.) + events_by_date = {} + + # Periyodik Bakım Kayıtları + maintenances = self.env['mymach.fleet.periodic.maintenance'].search([ + ('vehicle_id', '=', vehicle.id), + ('date_completed', '!=', False) + ]) + for m in maintenances: + d = m.date_completed + events_by_date.setdefault(d, []).append(f"🔧 {m.name} Tamamlandı ({m.odometer_completed:.0f} KM)") + + # Gider/Masraf Kayıtları (Yakıt alımları hariç tutulacak, onlar çizgi grafiğe gider) + costs = self.env['mymach.fleet.maintenance.cost'].search([ + ('vehicle_id', '=', vehicle.id), + ('date', '!=', False) + ]) + for c in costs: + if 'Yakıt' not in c.name and 'L' not in c.name: + d = c.date + events_by_date.setdefault(d, []).append(f"💰 {c.name} ({c.amount:.0f} {c.currency_id.name})") + + # Odometer/KM Değerlerini Getiren Yardımcı Fonksiyon (Forward Fill) + def get_odometer_for_date(target_d): + entry = self.env['fleet.vehicle.odometer'].search([ + ('vehicle_id', '=', vehicle.id), + ('date', '<=', target_d) + ], order='date desc', limit=1) + return entry.value if entry else 0.0 + + # Yakıt Litre Değerini Getiren Yardımcı Fonksiyon + def get_fuel_for_date(target_d): + day_costs = self.env['mymach.fleet.maintenance.cost'].search([ + ('vehicle_id', '=', vehicle.id), + ('date', '=', target_d) + ]) + total_liters = 0.0 + for dc in day_costs: + if 'Yakıt' in dc.name or 'L' in dc.name: + try: + match = re.search(r'\(([\d\.]+)\s*L\)', dc.name) + if match: + total_liters += float(match.group(1)) + except Exception: + pass + return total_liters + + # Yakıt Giderini (TL) Getiren Yardımcı Fonksiyon + def get_fuel_cost_for_date(target_d): + day_costs = self.env['mymach.fleet.maintenance.cost'].search([ + ('vehicle_id', '=', vehicle.id), + ('date', '=', target_d) + ]) + total_cost = 0.0 + for dc in day_costs: + if 'Yakıt' in dc.name or 'L' in dc.name: + total_cost += dc.amount + return total_cost + + # 3. Grafik Çizgileri İçin KM, Yakıt ve Olay Listelerini Oluştur + km_data = [] + fuel_data = [] + fuel_cost_data = [] + event_labels = {} + + if record.time_range == 'year': + for idx, (y, m) in enumerate(months_list): + import calendar + last_day = calendar.monthrange(y, m)[1] + end_date = datetime.date(y, m, last_day) + km_data.append(get_odometer_for_date(end_date)) + + # O aya ait yakıt toplamını ve olayları biriktir + month_fuel = 0.0 + month_fuel_cost = 0.0 + month_events = [] + + start_d = datetime.date(y, m, 1) + end_d = datetime.date(y, m, last_day) + month_costs = self.env['mymach.fleet.maintenance.cost'].search([ + ('vehicle_id', '=', vehicle.id), + ('date', '>=', start_d), + ('date', '<=', end_d) + ]) + for c in month_costs: + if 'Yakıt' in c.name or 'L' in c.name: + month_fuel_cost += c.amount + try: + match = re.search(r'\(([\d\.]+)\s*L\)', c.name) + if match: + month_fuel += float(match.group(1)) + except Exception: + pass + + for edate, evs in events_by_date.items(): + if edate.year == y and edate.month == m: + month_events.extend(evs) + + fuel_data.append(month_fuel) + fuel_cost_data.append(month_fuel_cost) + if month_events: + event_labels[idx] = month_events + else: + for idx, d in enumerate(date_list): + km_data.append(get_odometer_for_date(d)) + fuel_data.append(get_fuel_for_date(d)) + fuel_cost_data.append(get_fuel_cost_for_date(d)) + day_events = events_by_date.get(d, []) + if day_events: + event_labels[idx] = day_events + + if max(km_data) == 0.0 and vehicle.odometer > 0: + km_data[-1] = vehicle.odometer + + # Dinamik Olarak Seçilen Metriğe Göre Veri Kümelerini (Datasets) Oluştur + raw_datasets = [] + metric = record.chart_metric + + # KM Serisi + if metric in ('combined', 'km'): + raw_datasets.append({ + 'label': 'Kat Edilen Kilometre (KM)', + 'data': km_data, + 'borderColor': '#38bdf8', + 'backgroundColor': 'rgba(56, 189, 248, 0.04)', + 'borderWidth': 2.5, + 'tension': 0.3, + 'fill': True, + 'yAxisID': 'yKM' if metric == 'combined' else 'yMain' + }) + + # Yakıt Hacmi Serisi + if metric in ('combined', 'fuel_liters'): + raw_datasets.append({ + 'label': 'Harcanan Yakıt (Litre)', + 'data': fuel_data, + 'borderColor': '#fb923c', + 'backgroundColor': 'rgba(251, 146, 60, 0.04)', + 'borderWidth': 2.5, + 'tension': 0.3, + 'fill': True, + 'yAxisID': 'yFuel' if metric == 'combined' else 'yMain' + }) + + # Yakıt Gideri Serisi + if metric in ('combined', 'fuel_cost'): + raw_datasets.append({ + 'label': 'Yakıt Gideri (TL)', + 'data': fuel_cost_data, + 'borderColor': '#10b981', + 'backgroundColor': 'rgba(16, 185, 129, 0.04)', + 'borderWidth': 2.5, + 'tension': 0.3, + 'fill': True, + 'yAxisID': 'yCost' if metric == 'combined' else 'yMain' + }) + + # 4. Premium HTML ve JS Şablonu Oluştur (Iframe srcdoc yapısında) + inner_html = f""" + + + + + + +
+

📈 {vehicle.license_plate or ""} Telemetri Analizi & Finansal Gider Analitiği

+
+ +
+
+ + + +""" + + # Escape double quotes for srcdoc attribute safety + escaped_html = inner_html.replace('"', '"') + record.chart_html = f'' + + def action_save_settings(self): + """ + Sihirbaz veya panodaki seçimleri veri tabanına kalıcı olarak kaydeder ve + kullanıcıya yeşil bir başarı bildirim penceresi döndürür. + """ + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Sistem Başarıyla Güncellendi', + 'message': 'Yaptığınız tüm değişiklikler ve seçimler veri tabanına kalıcı olarak kaydedilmiştir.', + 'sticky': False, + 'type': 'success', + 'next': {'type': 'ir.actions.client', 'tag': 'soft_reload'} + } + } + + def action_calculate_routes(self): + for record in self: + dest = record.route_destination or "İstanbul Havalimanı" + record.route_results_html = f""" +
+

🗺️ {dest} Konumu İçin Alternatif 3 Rota Belirlendi

+
+
+
+ ROTA A (En Kısa) +

15.2 KM

+

Tahmini Yakıt: 1.1 L (Otoyol)

+
+
+
+
+ ROTA B (Ekonomik) +

16.5 KM

+

Tahmini Yakıt: 0.9 L (Şehir İçi)

+
+
+
+
+ ROTA C (Alternatif) +

18.0 KM

+

Tahmini Yakıt: 1.3 L (Akıcı)

+
+
+
+
+ """ + return True + + def action_simulate_route_drive(self): + self.ensure_one() + vehicle = self.selected_vehicle_id or self.env['fleet.vehicle'].search([], limit=1) + if not vehicle: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Hata', + 'message': 'Simülasyon için geçerli bir araç seçilmelidir.', + 'type': 'danger', + } + } + + # Seçilen rotaya göre hedef mesafeyi belirle + route_dist = 15.2 + if self.route_selected == 'route_b': + route_dist = 16.5 + elif self.route_selected == 'route_c': + route_dist = 18.0 + + sim_dist = self.route_simulated_distance + deviation = sim_dist - route_dist + + # Eğer sapma 1.0 KM'den fazlaysa + if deviation > 1.0: + driver = vehicle.driver_id + if not driver: + has_free_use = False + else: + contract = self.env['mymach.fleet.contract'].search([ + ('driver_id', '=', driver.id), + ('state', '=', 'open') + ], limit=1) + if contract and contract.personal_use_policy in ('allowed', 'unlimited', 'rental'): + has_free_use = True + else: + has_free_use = False + + if not has_free_use: + # Güvenlik Logu (İhlal) kaydet + self.env['mymach.fleet.security.log'].create({ + 'vehicle_id': vehicle.id, + 'status': 'exception', + 'name': f"ROTA SAPMA İHLALİ: Sürücü ({driver.name if driver else 'Belirsiz'}), {self.route_destination} konumuna gitmek için seçilen rotadan {deviation:.1f} KM sapmıştır (Tolerans: 1.0 KM).", + 'is_resolved': False + }) + # Araç chatter'ına uyarısı postala + vehicle.message_post(body=f"🚨 ROTA SAPMA ALARMI (YÖNETİCİ UYARISI): Sürücü ({driver.name if driver else 'Belirsiz'}), {self.route_destination} rotasından {deviation:.1f} KM sapmıştır. Sözleşmesinde serbest kullanım hakkı bulunmadığı için yöneticiye ihlal uyarısı iletilmiştir.") + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '🚨 ROTA SAPMA UYARISI!', + 'message': f"Sürücü rotasından {deviation:.1f} KM sapmıştır! Yöneticiye uyarı iletildi ve ihlal logu kaydedildi.", + 'type': 'danger', + 'sticky': True, + } + } + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Sürüş Tamamlandı', + 'message': 'Yolculuk başarıyla tamamlandı. Herhangi bir sapma uyarısı tetiklenmedi.', + 'type': 'success', + 'sticky': False, + } + } diff --git a/addons/mymach_fleet_intelligence/models/maintenance_cost.py b/addons/mymach_fleet_intelligence/models/maintenance_cost.py new file mode 100644 index 0000000..b85b37d --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/maintenance_cost.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api + +class MymachFleetMaintenanceCost(models.Model): + """ + Periyodik Giderler Matrisi (mymach.fleet.maintenance.cost) + Araç bakım ve masraf kalemlerinin ürün (product.template) nesnesi ile doğrudan bağlandığı tablo. + """ + _name = 'mymach.fleet.maintenance.cost' + _description = 'Filo Periyodik Gider Matrisi' + + name = fields.Char(string="Description", required=True) + product_id = fields.Many2one('product.template', string="Expense Item (Product)", required=True) + vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True) + amount = fields.Monetary(string="Amount", currency_field='currency_id', required=True) + currency_id = fields.Many2one('res.currency', string="Currency", default=lambda self: self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id) + date = fields.Date(string="Date", default=fields.Date.context_today) + + @api.model_create_multi + def create(self, vals_list): + """ + Alt Görev 2.1: Python Kancası (Hook) + Tablodan veya dışarıdan yeni bir ürün (gider) kaydedildiğinde (veya seçildiğinde), + ilgili ürünün Odoo sisteminde otomatik olarak can_be_expensed=True ve purchase_ok=True olmasını sağlar. + """ + records = super(MymachFleetMaintenanceCost, self).create(vals_list) + for record in records: + if record.product_id: + # Odoo standartlarında satın alınabilir işaretle + update_vals = {'purchase_ok': True} + # Masraf modülü yüklüyse (Soft dependency), masraf bayrağını da işaretle + if 'can_be_expensed' in self.env['product.template']._fields: + update_vals['can_be_expensed'] = True + + record.product_id.sudo().write(update_vals) + return records + + def _check_expense_threshold(self, vehicle, total_cost): + if vehicle.purchase_value > 0: + ratio = total_cost / vehicle.purchase_value + if ratio > 0.15: + # Odoo'nun uyarı sistemini tetikleyebiliriz veya mesaj atabiliriz. + vehicle.message_post(body=f"UYARI: Bu aracın bakım maliyeti alım değerinin %15'ini aştı! ({ratio*100:.2f}%). Satmak daha karlı olabilir.") + + @api.model + def action_sync_fuel_data_cron(self): + """ + Faz 8: Her gece çalışan Cron Görevi. Tüm araçlar için sahte API'den yakıt çeker. + """ + vehicles = self.env['fleet.vehicle'].search([]) + for vehicle in vehicles: + self.sync_fuel_from_api(vehicle) + self._run_predictive_maintenance_ai(vehicle) + + def sync_fuel_from_api(self, vehicle): + """ + Dış Servis (Shell/Opet) Simülasyonu: Araca rastgele veya mantıklı yakıt ekler. + Ayrıca anlık depo seviyesini günceller. + """ + # API'den geldiği varsayılan litre (Örn: 20 ile 40 litre arası) + import random + added_liters = random.uniform(10.0, 40.0) + price_per_liter = 45.50 + + product = self.env['product.product'].search([], limit=1) + + self.create({ + 'vehicle_id': vehicle.id, + 'product_id': product.id, + 'amount': added_liters * price_per_liter, + 'name': f'Otonom API Yakıt Alımı ({added_liters:.1f} L)' + }) + + # Depo seviyesini güncelle (Kapasiteyi aşmasın) + new_level = vehicle.current_fuel_level + added_liters + vehicle.current_fuel_level = min(new_level, vehicle.tank_capacity) + + vehicle.message_post(body=f"🔋 Shell/Opet API: Araca {added_liters:.1f} Litre yakıt dolumu tespit edildi. Güncel Tahmini Depo: {vehicle.current_fuel_level:.1f} L") + + def _run_predictive_maintenance_ai(self, vehicle): + """ + Faz 8: Kestirimci Bakım Yapay Zekası + Son yakıt alımlarını ve KM'yi kıyaslayarak tüketim artışını bulur. + """ + # Basit AI Algoritması: Eğer çok yakıt masrafı varsa (Tüketim Fabrika verisinden %40 fazlaysa) + if vehicle.factory_avg_consumption > 0: + # Simülasyon: Rastgele bir motor yorulma ihtimali + import random + if random.random() > 0.8: # %20 ihtimalle AI arıza sezer + vehicle.needs_predictive_maintenance = True + vehicle.message_post(body="🚨 YZ UYARISI: Aracın son yakıt tüketim trendi fabrika verisinden (%40) daha fazla sapma gösterdi! Motor/Lastik basınç arızası olabilir. Acil Servis önerilir.") + + # Sürücü skorunu kötü kullanım nedeniyle düşür + if vehicle.driver_id: + contract = self.env['mymach.fleet.contract'].search([ + ('driver_id', '=', vehicle.driver_id.id), + ], limit=1) + if contract: + contract.write({'driver_score': contract.driver_score - 5}) + else: + vehicle.needs_predictive_maintenance = False diff --git a/addons/mymach_fleet_intelligence/models/periodic_maintenance.py b/addons/mymach_fleet_intelligence/models/periodic_maintenance.py new file mode 100644 index 0000000..6cf33cc --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/periodic_maintenance.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +from datetime import timedelta + +class MymachFleetPeriodicMaintenance(models.Model): + """ + Otonom KM ve Yıllık Bakım Hatırlatma Motoru (mymach.fleet.periodic.maintenance) + Araçların yakıt türü ve katettikleri kilometreye göre küçük/orta/ağır bakımları planlar. + """ + _name = 'mymach.fleet.periodic.maintenance' + _description = 'Araç Periyodik Bakım Hatırlatıcı' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'target_odometer asc, target_date asc' + + name = fields.Char(string="Bakım Referansı", compute="_compute_name", store=True, tracking=True) + vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", required=True, ondelete='cascade', tracking=True) + fuel_type = fields.Selection(related='vehicle_id.fuel_type', string="Yakıt Türü", readonly=True) + odometer = fields.Float(related='vehicle_id.odometer', string="Mevcut Kilometre (KM)", readonly=True) + + maintenance_type = fields.Selection([ + ('small', 'Küçük Bakım (10k-15k KM)'), + ('medium', 'Orta Bakım (30k-45k KM)'), + ('heavy', 'Ağır Bakım (60k-90k KM)'), + ('annual', 'Yıllık Periyodik Bakım') + ], string="Bakım Türü", required=True, default='small', tracking=True) + + last_odometer = fields.Float(string="Son Bakım Kilometresi (KM)", default=0.0) + target_odometer = fields.Float(string="Hedef Kilometre (KM)", required=True, tracking=True) + target_date = fields.Date(string="Hedef Tarih (Yıllık)", required=True, tracking=True) + + state = fields.Selection([ + ('pending', 'Bekliyor'), + ('overdue', 'Süresi/KM Geçti'), + ('done', 'Tamamlandı') + ], string="Durum", default='pending', tracking=True, compute='_compute_state', store=True) + + description = fields.Text(string="Yapılacak Standart İşlemler", compute='_compute_description', store=True) + date_completed = fields.Date(string="Tamamlanma Tarihi", tracking=True) + odometer_completed = fields.Float(string="Tamamlanma Kilometresi", tracking=True) + + @api.depends('maintenance_type', 'vehicle_id.license_plate') + def _compute_name(self): + for record in self: + m_label = dict(self._fields['maintenance_type'].selection).get(record.maintenance_type, 'Bakım') + plate = record.vehicle_id.license_plate or 'Yeni Araç' + record.name = f"{plate} - {m_label}" + + @api.depends('odometer', 'target_odometer', 'target_date', 'date_completed') + def _compute_state(self): + today = fields.Date.today() + for record in self: + if record.date_completed: + record.state = 'done' + elif (record.target_odometer and record.odometer >= record.target_odometer) or (record.target_date and today >= record.target_date): + record.state = 'overdue' + else: + record.state = 'pending' + + @api.depends('maintenance_type', 'fuel_type') + def _compute_description(self): + for record in self: + fuel = record.fuel_type or 'gasoline' + m_type = record.maintenance_type + + desc = [] + if m_type == 'small': + desc.append("🔧 KÜÇÜK PERİYODİK BAKIM İŞLEMLERİ:") + desc.append("1. Motor yağının ve yağ filtresinin değiştirilmesi") + desc.append("2. Hava ve polen filtrelerinin değiştirilmesi") + desc.append("3. Yakıt filtresinin kontrolü ve gerekiyorsa değişimi") + desc.append("4. Fren balataları, süspansiyon ve sıvı seviyelerinin (hidrolik, antifriz) kontrolü") + elif m_type == 'medium': + desc.append("🔧 ORTA PERİYODİK BAKIM İŞLEMLERİ:") + desc.append("1. Küçük bakımdaki tüm işlemlerin uygulanması") + if fuel == 'gasoline': + desc.append("2. Buji değişimi (Benzinli motor)") + else: + desc.append("2. Enjektör temizliği ve yakıt hattı sızdırmazlık testi (Dizel motor)") + desc.append("3. Fren hidroliği ve şanzıman yağı kontrolleri / tamamlanması") + desc.append("4. Silecek lastikleri ve aydınlatma (far/stop) sistemlerinin kontrolü") + elif m_type == 'heavy': + desc.append("🔥 AĞIR PERİYODİK BAKIM İŞLEMLERİ:") + desc.append("1. Triger kayışı / zincir setinin değişimi") + desc.append("2. Antifriz, şanzıman ve diferansiyel yağlarının tamamen değişimi") + desc.append("3. Akü şarj durumu ve marş sisteminin detaylı testi") + desc.append("4. Direksiyon kutusu ve alt takım rot-balans kontrolleri") + elif m_type == 'annual': + desc.append("📅 YILLIK BAKIM / EMNİYET KONTROLLERİ:") + desc.append("1. Motor yağı ve yağ filtresinin değişimi (KM dolmasa dahi 1 yıl dolduğu için)") + desc.append("2. Antifriz ve cam suyu seviyelerinin kontrolü") + desc.append("3. Fren balatası aşınma durumu kontrolü") + + record.description = "\n".join(desc) + + def action_complete(self): + """ Bakımın yapıldığını kaydeder ve sonraki bakımı otonom takvime ekler """ + self.ensure_one() + completed_date = fields.Date.today() + completed_odo = self.vehicle_id.odometer + + self.write({ + 'state': 'done', + 'date_completed': completed_date, + 'odometer_completed': completed_odo + }) + + # Aracın son bakım verilerini güncelle + self.vehicle_id.write({ + 'last_maintenance_odometer': completed_odo, + 'last_maintenance_date': completed_date + }) + + # Sonraki Bakım Hedefini Hesapla + # Dizel ise 10.000 KM, Benzinli ise 15.000 KM aralıklarla + interval = 10000 if self.fuel_type == 'diesel' else 15000 + next_odo = completed_odo + interval + + # Odometer hedefine göre sonraki bakım türünü seç + if next_odo >= 60000 and next_odo % 60000 == 0: + next_type = 'heavy' + elif next_odo >= 30000 and next_odo % 30000 == 0: + next_type = 'medium' + else: + next_type = 'small' + + self.create({ + 'vehicle_id': self.vehicle_id.id, + 'maintenance_type': next_type, + 'last_odometer': completed_odo, + 'target_odometer': next_odo, + 'target_date': completed_date + timedelta(days=365) # 1 yıl sonra + }) + + self.vehicle_id.message_post( + body=f"✅ Periyodik Bakım Yapıldı: {self.maintenance_type} tipi bakım {completed_odo:.0f} KM seviyesinde tamamlandı. Bir sonraki hedef: {next_odo:.0f} KM veya {completed_date + timedelta(days=365)} tarihidir." + ) + return True diff --git a/addons/mymach_fleet_intelligence/models/route_exception_wizard.py b/addons/mymach_fleet_intelligence/models/route_exception_wizard.py new file mode 100644 index 0000000..4c99f4c --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/route_exception_wizard.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +from odoo.exceptions import UserError + +class MymachFleetRouteExceptionWizard(models.TransientModel): + """ + Alt Görev 3.3: İstisna / Sürpriz Rota Karar Popup (Wizard) Ekranı + CRM, Proje ve Kişisel Kullanım iş kurallarını otonom olarak işletir. + """ + _name = 'mymach.fleet.route.exception.wizard' + _description = 'İstisna Rota Onay Sihirbazı' + + log_id = fields.Many2one('mymach.fleet.security.log', string="Security Log", required=True) + vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True) + + exception_type = fields.Selection([ + ('crm', 'CRM Fırsatı / Müşteri Ziyareti'), + ('project', 'Proje Görevi / Maliyet Yansıtma'), + ('personal', 'Kişisel / Bireysel Kullanım'), + ('other', 'Diğer Gerekçeler') + ], string="Rota Gerekçesi", required=True, default='crm') + + partner_id = fields.Many2one('res.partner', string="Ziyaret Edilen Kontak") + project_id = fields.Many2one('project.project', string="İlişkili Proje") + distance = fields.Float(string="Yolculuk Mesafesi (KM)", default=0.0) + estimated_cost = fields.Float(string="Tahmini Yolculuk Maliyeti (TRY)", default=0.0) + description = fields.Text(string="Açıklama / Görüşme Notları", required=True) + + def action_confirm(self): + self.ensure_one() + + # 1. CRM FIRSAT SENARYOSU + if self.exception_type == 'crm': + self.log_id.write({ + 'is_resolved': True, + 'name': f"CRM GÖRÜŞMESİNE YÖNLENDİRİLDİ: {self.description[:40]}..." + }) + + # CRM Modülüne Yönlendirerek Form Penceresi Döndür + return { + 'name': 'Yeni Fırsat / Görüşme Kaydı', + 'type': 'ir.actions.act_window', + 'res_model': 'crm.lead', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_type': 'opportunity', + 'default_partner_id': self.partner_id.id if self.partner_id else False, + 'default_name': f"Sürpriz Rota Fırsatı: {self.vehicle_id.license_plate}", + 'default_description': f"Sürücü: {self.vehicle_id.driver_id.name or ''}\n" + f"Araç: {self.vehicle_id.name}\n" + f"Sürüş Mesafesi: {self.distance:.1f} KM\n\n" + f"Görüşme / Ziyaret Detayları:\n{self.description or ''}" + } + } + + # 2. PROJE MALİYET SENARYOSU + elif self.exception_type == 'project': + if not self.project_id: + raise UserError("Lütfen yolculuk maliyetinin yansıtılacağı projeyi seçin!") + + analytic_account = self.project_id.account_id + if not analytic_account: + raise UserError(f"Seçilen projenin ({self.project_id.name}) analitik hesabı tanımlanmamış!") + + # Analitik gider kaydı (account.analytic.line) oluştur + self.env['account.analytic.line'].create({ + 'name': f"Rota Dışı Yolculuk Maliyeti: {self.vehicle_id.license_plate} ({self.distance:.1f} KM)", + 'account_id': analytic_account.id, + 'amount': -self.estimated_cost, # Gider kalemi negatif değerde olmalıdır + 'unit_amount': self.distance, + 'date': fields.Date.today() + }) + + self.log_id.write({ + 'is_resolved': True, + 'name': f"PROJE MALİYETİNE YANSITILDI: {self.project_id.name} ({self.estimated_cost:.2f} TRY)" + }) + + # 3. KİŞİSEL / BİREYSEL KULLANIM SENARYOSU + elif self.exception_type == 'personal': + contract = self.env['mymach.fleet.contract'].search([ + ('driver_id', '=', self.vehicle_id.driver_id.id) + ], limit=1) + + if not contract: + raise UserError("Sürücüye ait aktif bir tahsis sözleşmesi bulunamadı! Kişisel kullanım çözümlenemez.") + + distance = self.distance or 0.0 + charge_amount = 0.0 + + # İzinli / Limitsiz / Kiralık Senaryoları + if contract.personal_use_policy in ('allowed', 'unlimited', 'rental'): + contract.write({'personal_km_used': contract.personal_km_used + distance}) + self.log_id.write({ + 'is_resolved': True, + 'name': f"ŞAHSİ KULLANIM (İZİNLİ/LİMİTSİZ/KİRA) - KM düşüldü: {distance:.1f} KM" + }) + + # Limitli Senaryo + elif contract.personal_use_policy == 'limit': + limit = contract.max_km_limit or 0.0 + current_used = contract.personal_km_used or 0.0 + new_used = current_used + distance + + if new_used <= limit: + contract.write({'personal_km_used': new_used}) + self.log_id.write({ + 'is_resolved': True, + 'name': f"ŞAHSİ KULLANIM (LİMİT DAHİLİ) - Kalan Limit: {limit - new_used:.1f} KM" + }) + else: + if current_used >= limit: + excess_dist = distance + else: + excess_dist = new_used - limit + + # Maaştan kesilecek tutarı hesapla + charge_amount = excess_dist * (self.vehicle_id.km_cost or 5.0) + contract.write({'personal_km_used': new_used}) + + self.log_id.write({ + 'is_resolved': True, + 'deduction_amount': charge_amount, + 'name': f"ŞAHSİ LİMİT AŞIMI - Maaştan Kesilecek: {charge_amount:.2f} TRY" + }) + + # Yasaklı Senaryo + else: + charge_amount = distance * (self.vehicle_id.km_cost or 5.0) + contract.write({'personal_km_used': (contract.personal_km_used or 0.0) + distance}) + + self.log_id.write({ + 'is_resolved': True, + 'deduction_amount': charge_amount, + 'name': f"YASAKLI ŞAHSİ KULLANIM - Maaştan Kesilecek: {charge_amount:.2f} TRY" + }) + + # 4. DİĞER GEREKÇELER + else: + self.log_id.write({ + 'is_resolved': True, + 'name': f"İŞ GÖREVİ (DİĞER): {self.description}" + }) + + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mymach_fleet_intelligence/models/security_log.py b/addons/mymach_fleet_intelligence/models/security_log.py new file mode 100644 index 0000000..371e9b9 --- /dev/null +++ b/addons/mymach_fleet_intelligence/models/security_log.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api +import math +import requests + +class MymachFleetSecurityLog(models.Model): + """ + Component 2 & 3: Security & OSRM Routing Engine + Stores telemetry and processes route matches and deviations. + """ + _name = 'mymach.fleet.security.log' + _description = 'Fleet Security and Route Logs' + _order = 'datetime desc' + + name = fields.Char(string="Log Reference", required=True, default="Autonomous System Log") + vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True) + driver_id = fields.Many2one('res.partner', string="Driver", related='vehicle_id.driver_id', store=True) + + datetime = fields.Datetime(string="Datetime", default=fields.Datetime.now) + latitude = fields.Float(string="Latitude", digits=(10, 7)) + longitude = fields.Float(string="Longitude", digits=(10, 7)) + + status = fields.Selection([ + ('in_office_moving', 'WARNING: Employee in Office but Vehicle Moving'), + ('project_match', 'CONFIRMED: Project Route Match'), + ('exception', 'ATTENTION: Surprise Route Exception') + ], string="Security Status", required=True) + + is_resolved = fields.Boolean(string="Is Resolved?", default=False) + deduction_amount = fields.Monetary(string="Payroll Deduction Amount", currency_field='currency_id') + currency_id = fields.Many2one('res.currency', string="Currency", related='vehicle_id.currency_id') + + # Yolculuk mesafesi ve B noktasındaki kontak tespiti + distance = fields.Float(string="Yolculuk Mesafesi (KM)", default=0.0) + partner_id = fields.Many2one('res.partner', string="Ziyaret Edilen Kontak") + + def _calculate_haversine_distance(self, lat1, lon1, lat2, lon2): + """ Calculates straight-line distance in km """ + R = 6371.0 + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = math.sin(dlat / 2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + return R * c + + def _calculate_osrm_road_distance(self, lat1, lon1, lat2, lon2): + """ Calculates real driving distance using public OSRM API (km) """ + try: + url = f"http://router.project-osrm.org/route/v1/driving/{lon1},{lat1};{lon2},{lat2}?overview=false" + res = requests.get(url, timeout=3) + if res.status_code == 200: + data = res.json() + if data.get('code') == 'Ok' and data.get('routes'): + return data['routes'][0]['distance'] / 1000.0 + except Exception: + pass + return None + + def _find_nearest_partner(self, lat, lon): + """ B noktasının 200m yakınında kayıtlı res.partner arar """ + if not lat or not lon: + return False + max_dist = 0.2 # 200 metre + partners = self.env['res.partner'].search([ + ('partner_latitude', '!=', 0.0), + ('partner_longitude', '!=', 0.0) + ]) + for p in partners: + dist = self._calculate_haversine_distance(lat, lon, p.partner_latitude, p.partner_longitude) + if dist <= max_dist: + return p.id + return False + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + # OSRM Rota Sapma Analizi + vehicle_id = vals.get('vehicle_id') + lat = vals.get('latitude') + lon = vals.get('longitude') + + if vehicle_id and lat and lon: + # 1. Rota/Mesafe Hesaplama (Eğer dışarıdan mesafe verilmemişse) + if not vals.get('distance'): + last_log = self.search([ + ('vehicle_id', '=', vehicle_id) + ], order='datetime desc', limit=1) + + if last_log and last_log.latitude and last_log.longitude: + haversine_dist = self._calculate_haversine_distance( + last_log.latitude, last_log.longitude, lat, lon + ) + osrm_dist = self._calculate_osrm_road_distance( + last_log.latitude, last_log.longitude, lat, lon + ) + + # Eğer karayolu sürüş mesafesi kuş uçuşunun %40'ından fazla sapmışsa ve mesafe 1.5 km'den büyükse istisna olarak işaretle + if osrm_dist and osrm_dist > 1.4 * haversine_dist and osrm_dist > 1.5: + vals['status'] = 'exception' + vals['name'] = f"OSRM Rota Sapması Sezildi (OSRM: {osrm_dist:.2f} km / Kuş Uçuşu: {haversine_dist:.2f} km)" + vals['distance'] = osrm_dist + elif osrm_dist: + vals['distance'] = osrm_dist + else: + vals['distance'] = haversine_dist + else: + vals['distance'] = 0.0 + + # 2. Yakındaki kontağı otomatik tespit et (Mesafe dışarıdan verilmiş olsa dahi) + nearest_partner = self._find_nearest_partner(lat, lon) + if nearest_partner: + vals['partner_id'] = nearest_partner + + records = super(MymachFleetSecurityLog, self).create(vals_list) + for record in records: + if record.status in ['exception', 'in_office_moving'] and record.vehicle_id: + manager_id = record.vehicle_id.manager_id.id if record.vehicle_id.manager_id else self.env.uid + record.vehicle_id.activity_schedule( + 'mail.mail_activity_data_warning', + summary=f'🚨 Otonom Güvenlik İhlali: {record.status}', + note=f'Sürücü: {record.driver_id.name} rotadan saptı veya yetkisiz araç kullandı. Lütfen kontrol edin!', + user_id=manager_id + ) + + if record.driver_id: + contract = self.env['mymach.fleet.contract'].search([ + ('driver_id', '=', record.driver_id.id), + ], limit=1) + if contract: + deduct = 15 if record.status == 'in_office_moving' else 10 + contract.write({'driver_score': contract.driver_score - deduct}) + return records + + @api.model + def get_dashboard_data(self): + """ + OWL Harita Arayüzünün veri okuması için optimize edilmiş ORM RPC metodu. + """ + vehicles = self.env['fleet.vehicle'].search([]) + vehicle_list = [] + for v in vehicles: + score = 100 + commute_allowed = 'forbidden' + if v.driver_id: + contract = self.env['mymach.fleet.contract'].search([ + ('driver_id', '=', v.driver_id.id) + ], limit=1) + if contract: + score = contract.driver_score + commute_allowed = contract.commute_allowed + + vehicle_list.append({ + 'id': v.id, + 'name': v.name, + 'license_plate': v.license_plate, + 'driver_name': v.driver_id.name or 'Sürücü Atanmamış', + 'fuel_level': round(v.current_fuel_level, 1), + 'tank_capacity': v.tank_capacity, + 'needs_predictive_maintenance': v.needs_predictive_maintenance, + 'is_maintenance_critical': v.is_maintenance_critical, + 'score': score, + 'commute_allowed': commute_allowed + }) + + logs = self.search([], limit=50) + log_list = [] + for l in logs: + log_list.append({ + 'id': l.id, + 'vehicle_name': l.vehicle_id.license_plate, + 'driver_name': l.driver_id.name or 'Bilinmiyor', + 'latitude': l.latitude, + 'longitude': l.longitude, + 'status': l.status, + 'status_desc': dict(l._fields['status'].selection).get(l.status, ''), + 'datetime': l.datetime.strftime('%Y-%m-%d %H:%M:%S') if l.datetime else '' + }) + + google_maps_api_key = self.env['ir.config_parameter'].sudo().get_param('mymach.google_maps_api_key', default='') + + return { + 'vehicles': vehicle_list, + 'logs': log_list, + 'google_maps_api_key': google_maps_api_key + } + + def action_open_exception_wizard(self): + self.ensure_one() + return { + 'name': 'İstisna Rota Karar Motoru', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.fleet.route.exception.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_log_id': self.id, + 'default_vehicle_id': self.vehicle_id.id, + 'default_partner_id': self.partner_id.id, + 'default_distance': self.distance, + 'default_estimated_cost': self.distance * (self.vehicle_id.km_cost or 5.0), + } + } diff --git a/addons/mymach_fleet_intelligence/populate_5_drivers.py b/addons/mymach_fleet_intelligence/populate_5_drivers.py new file mode 100644 index 0000000..28f56d7 --- /dev/null +++ b/addons/mymach_fleet_intelligence/populate_5_drivers.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +db_name = 'dev' +registry = Registry.new(db_name) +cr = registry.cursor() +env = api.Environment(cr, SUPERUSER_ID, {}) + +print("--- REpopulating Demo Drivers and Vehicles ---") + +drivers_data = [ + { + 'name': 'Ahmet Yılmaz', + 'plate': '34-AHM-101', + 'model_name': 'Renault Clio 1.5 dCi', + 'policy': 'limit', + 'planned_km': 15.2, + 'driven_km': 15.4, + 'fuel': 42.0, + 'score': 95, + 'lat_offset': 0.0 + }, + { + 'name': 'Ayşe Kaya', + 'plate': '34-AYS-202', + 'model_name': 'Fiat Egea 1.6 Multijet', + 'policy': 'forbidden', + 'planned_km': 12.0, + 'driven_km': 12.3, + 'fuel': 38.0, + 'score': 92, + 'lat_offset': -0.05 + }, + { + 'name': 'Mehmet Demir', + 'plate': '34-MHM-303', + 'model_name': 'Ford Focus 1.5 EcoBlue', + 'policy': 'limit', + 'planned_km': 14.0, + 'driven_km': 17.5, + 'fuel': 28.0, + 'score': 68, + 'lat_offset': 0.03 + }, + { + 'name': 'Canan Öztürk', + 'plate': '34-CNN-404', + 'model_name': 'Volkswagen Golf 1.0 TSI', + 'policy': 'forbidden', + 'planned_km': 25.0, + 'driven_km': 28.8, + 'fuel': 19.0, + 'score': 60, + 'lat_offset': -0.10 + }, + { + 'name': 'Burak Arslan', + 'plate': '34-BRK-505', + 'model_name': 'BMW 320i Sedan', + 'policy': 'unlimited', + 'planned_km': 10.0, + 'driven_km': 14.0, + 'fuel': 45.0, + 'score': 88, + 'lat_offset': 0.02 + } +] + +model_obj = env['fleet.vehicle.model'].search([], limit=1) +if not model_obj: + brand = env['fleet.vehicle.model.brand'].create({'name': 'Generic Fleet'}) + model_obj = env['fleet.vehicle.model'].create({'name': 'Filo Modeli', 'brand_id': brand.id}) + +for item in drivers_data: + partner = env['res.partner'].search([('name', '=', item['name'])], limit=1) + if not partner: + partner = env['res.partner'].create({ + 'name': item['name'], + 'email': f"{item['name'].lower().replace(' ', '.')}@mymach.com", + 'phone': '0532 000 00 00' + }) + + vehicle = env['fleet.vehicle'].search([('license_plate', '=', item['plate'])], limit=1) + if not vehicle: + vehicle = env['fleet.vehicle'].create({ + 'model_id': model_obj.id, + 'license_plate': item['plate'], + 'driver_id': partner.id, + 'current_fuel_level': item['fuel'], + 'tank_capacity': 50.0, + 'odometer': 15000.0 + (item['driven_km'] * 10) + }) + else: + vehicle.write({ + 'driver_id': partner.id, + 'current_fuel_level': item['fuel'], + 'odometer': 15000.0 + (item['driven_km'] * 10) + }) + + env['mymach.fleet.contract'].create({ + 'name': f"Tahsis Sözleşmesi - {item['name']}", + 'driver_id': partner.id, + 'personal_use_policy': item['policy'], + 'driver_score': item['score'], + 'max_km_limit': 300.0, + 'personal_km_used': 50.0 + }) + + deviation = item['driven_km'] - item['planned_km'] + has_free_use = item['policy'] in ('allowed', 'unlimited', 'rental') + is_violation = (deviation > 1.0) and (not has_free_use) + + status = 'exception' if is_violation else 'project_match' + log_name = f"ROTA SAPMA İHLALİ: {item['name']} rotadan {deviation:.1f} KM sapmıştır!" if is_violation else f"NORMAL SÜRÜŞ: {item['name']} rotaya uygun seyrediyor." + + env['mymach.fleet.security.log'].create({ + 'vehicle_id': vehicle.id, + 'status': status, + 'name': log_name, + 'latitude': 41.0766 + item['lat_offset'], + 'longitude': 29.0124 + (item['lat_offset'] * 0.5), + 'is_resolved': not is_violation + }) + +env.cr.commit() +print("--- DEMO DATA RE-POPULATED SUCCESSFULLY! ---") diff --git a/addons/mymach_fleet_intelligence/populate_script_v2.py b/addons/mymach_fleet_intelligence/populate_script_v2.py new file mode 100644 index 0000000..2fa3ad7 --- /dev/null +++ b/addons/mymach_fleet_intelligence/populate_script_v2.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +from odoo.modules.loading import load_modules + +db_name = 'dev' +registry = Registry(db_name) +load_modules(registry) + +cr = registry.cursor() +env = api.Environment(cr, SUPERUSER_ID, {}) + +print("--- EXECUTING DEMO REPOPULATION ---") +env['fleet.vehicle'].action_repopulate_demo_data() +cr.commit() +cr.close() +print("--- SUCCESS: DEMO DATA REPOPULATED! ---") diff --git a/addons/mymach_fleet_intelligence/regenerate_js_assets.py b/addons/mymach_fleet_intelligence/regenerate_js_assets.py new file mode 100644 index 0000000..e891591 --- /dev/null +++ b/addons/mymach_fleet_intelligence/regenerate_js_assets.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() +cr.execute("DELETE FROM ir_attachment WHERE name LIKE '%.assets_%';") +conn.commit() +conn.close() +print("SUCCESS: Invalidated JS/CSS asset bundle attachments!") diff --git a/addons/mymach_fleet_intelligence/reset_admin_password_and_session.py b/addons/mymach_fleet_intelligence/reset_admin_password_and_session.py new file mode 100644 index 0000000..2fea07f --- /dev/null +++ b/addons/mymach_fleet_intelligence/reset_admin_password_and_session.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import psycopg2 + +conn = psycopg2.connect('dbname=dev user=odoo password=odoo host=db') +cr = conn.cursor() + +# Ensure admin (id=1 or login='admin') is active and has correct password hash if needed +cr.execute("SELECT id, login, active, company_id FROM res_users WHERE id = 1 or login = 'admin';") +users = cr.fetchall() +print("ADMIN USERS IN DB:", users) + +cr.execute("UPDATE res_users SET active = true WHERE login = 'admin' OR id = 1;") +conn.commit() +conn.close() diff --git a/addons/mymach_fleet_intelligence/restore_system_and_demo.py b/addons/mymach_fleet_intelligence/restore_system_and_demo.py new file mode 100644 index 0000000..e030379 --- /dev/null +++ b/addons/mymach_fleet_intelligence/restore_system_and_demo.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +cr = env.cr + +# 1. Reset stuck modules to installed +cr.execute("UPDATE ir_module_module SET state = 'installed' WHERE state IN ('to install', 'to upgrade', 'to remove');") +cr.commit() + +# 2. Ensure base users are active +cr.execute("UPDATE res_users SET active = true;") +cr.commit() + +print("Modules and users un-stuck!") diff --git a/addons/mymach_fleet_intelligence/scan_db_broken_chars.py b/addons/mymach_fleet_intelligence/scan_db_broken_chars.py new file mode 100644 index 0000000..b41c0fc --- /dev/null +++ b/addons/mymach_fleet_intelligence/scan_db_broken_chars.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +# Scan Odoo models for string fields containing "??" +all_models = env['ir.model'].search([]) +suspicious_records = [] + +for ir_model in all_models: + model_name = ir_model.model + # Skip models that are not actual database tables or are technical log tables + if model_name.startswith(('ir.', 'base.', 'bus.', 'report.', 'mail.message', 'mail.tracking')): + continue + + try: + model_obj = env[model_name] + except KeyError: + continue + + # Get string fields + string_fields = [f_name for f_name, f_obj in model_obj._fields.items() if f_obj.type in ('char', 'text', 'html') and not f_obj.compute] + if not string_fields: + continue + + for field in string_fields: + try: + # Search for records containing "??" + records = model_obj.search([(field, 'like', '??')]) + if records: + for rec in records: + val = rec[field] + suspicious_records.append({ + 'model': model_name, + 'id': rec.id, + 'field': field, + 'val': val + }) + except Exception: + pass + +if suspicious_records: + print(f"FOUND {len(suspicious_records)} SUSPICIOUS RECORDS:") + for item in suspicious_records[:50]: + print(f"Model: {item['model']}, ID: {item['id']}, Field: {item['field']} -> Value: {item['val']}") +else: + print("NO CORRUPTED RECORDS WITH '??' FOUND IN ODOO DATABASE.") diff --git a/addons/mymach_fleet_intelligence/scratch_fix.py b/addons/mymach_fleet_intelligence/scratch_fix.py new file mode 100644 index 0000000..6132999 --- /dev/null +++ b/addons/mymach_fleet_intelligence/scratch_fix.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +# Delete old demo records to prevent duplicates +old_contracts = env['mymach.fleet.contract'].search([]) +old_contracts.unlink() + +old_handovers = env['mymach.fleet.handover'].search([]) +old_handovers.unlink() + +old_fines = env['mymach.fleet.fine'].search([]) +old_fines.unlink() + +old_logs = env['mymach.fleet.security.log'].search([]) +old_logs.unlink() + +old_employees = env['hr.employee'].search([('name', 'ilike', 'Sürücü')]) +old_employees.unlink() + +old_partners = env['res.partner'].search([('name', 'ilike', 'Sürücü')]) +old_partners.unlink() + +# Recreate drivers with correct Turkish names +drivers_data = [ + {"name": "Ahmet Yılmaz (Sürücü)", "email": "ahmet@mymach.com"}, + {"name": "Mehmet Demir (Sürücü)", "email": "mehmet@mymach.com"}, + {"name": "Ayşe Kaya (Sürücü)", "email": "ayse@mymach.com"}, +] +drivers = [] +for d_data in drivers_data: + partner = env['res.partner'].create(d_data) + employee = env['hr.employee'].create({ + 'name': d_data['name'], + 'work_contact_id': partner.id, + 'work_email': d_data['email'] + }) + drivers.append(partner) + +# Link vehicles +vehicles = env['fleet.vehicle'].search([('license_plate', 'in', ['34 AL 100', '34 AL 200', '34 AL 300'])]) +for i, vehicle in enumerate(vehicles): + vehicle.write({ + 'driver_id': drivers[i % len(drivers)].id + }) + +# Recreate contracts +env['mymach.fleet.contract'].create({ + 'name': 'CON-AHMET-100', + 'driver_id': drivers[0].id, + 'personal_use_policy': 'forbidden', + 'commute_allowed': 'allowed', + 'driver_score': 100 +}) +env['mymach.fleet.contract'].create({ + 'name': 'CON-MEHMET-65', + 'driver_id': drivers[1].id, + 'personal_use_policy': 'limit', + 'commute_allowed': 'forbidden', + 'driver_score': 65 +}) +env['mymach.fleet.contract'].create({ + 'name': 'CON-AYSE-85', + 'driver_id': drivers[2].id, + 'personal_use_policy': 'allowed', + 'commute_allowed': 'allowed', + 'driver_score': 85 +}) + +# Initial logs for coordinates +coords = [ + {"lat": 40.9901, "lng": 29.0290, "status": "project_match", "name": "Autonomous System Log"}, + {"lat": 41.0422, "lng": 29.0083, "status": "project_match", "name": "Autonomous System Log"}, + {"lat": 41.0782, "lng": 29.0104, "status": "exception", "name": "OSRM Rota Sapması"}, +] +for i, coord in enumerate(coords): + if i < len(vehicles): + env['mymach.fleet.security.log'].create({ + 'vehicle_id': vehicles[i].id, + 'driver_id': drivers[i].id, + 'latitude': coord['lat'], + 'longitude': coord['lng'], + 'status': coord['status'], + 'name': coord['name'] + }) + +env.cr.commit() +print("DEMO DATA RECREATED SUCCESSFULLY WITH UTF-8 ENCODING!") diff --git a/addons/mymach_fleet_intelligence/scratch_fix_chars.py b/addons/mymach_fleet_intelligence/scratch_fix_chars.py new file mode 100644 index 0000000..bb8e85d --- /dev/null +++ b/addons/mymach_fleet_intelligence/scratch_fix_chars.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +def fix_text(text): + if not text: + return text + replacements = { + 'S??r??c??': 'Sürücü', + 'Y??lmaz': 'Yılmaz', + 'Ay??e': 'Ayşe', + '??hlal': 'İhlal', + 'S??r??': 'Sürücü', + 'Y??l': 'Yılmaz', + 'Ay??': 'Ayşe', + '??hl': 'İhlal' + } + new_text = text + for old, new in replacements.items(): + new_text = new_text.replace(old, new) + return new_text + +partners = env['res.partner'].search([]) +for p in partners: + fixed_name = fix_text(p.name) + if fixed_name != p.name: + print(f"Partner: {p.name} -> {fixed_name}") + p.name = fixed_name + +employees = env['hr.employee'].search([]) +for e in employees: + fixed_name = fix_text(e.name) + if fixed_name != e.name: + print(f"Employee: {e.name} -> {fixed_name}") + e.name = fixed_name + +contracts = env['mymach.fleet.contract'].search([]) +for c in contracts: + fixed_name = fix_text(c.name) + if fixed_name != c.name: + print(f"Contract: {c.name} -> {fixed_name}") + c.name = fixed_name + +logs = env['mymach.fleet.security.log'].search([]) +for l in logs: + fixed_name = fix_text(l.name) + if fixed_name != l.name: + print(f"Log Name: {l.name} -> {fixed_name}") + l.name = fixed_name + +env.cr.commit() +print("TURKISH CHARACTERS REPAIRED IN DATABASE!") diff --git a/addons/mymach_fleet_intelligence/scratch_fix_chars_clean.py b/addons/mymach_fleet_intelligence/scratch_fix_chars_clean.py new file mode 100644 index 0000000..8dcb563 --- /dev/null +++ b/addons/mymach_fleet_intelligence/scratch_fix_chars_clean.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +def clean_text(text): + if not text: + return text + mappings = { + 'Ahmet Y??lmaz': 'Ahmet Yılmaz', + 'Ahmet Y??lmazmaz': 'Ahmet Yılmaz', + 'Ahmet Y??lmaz (S??r??c??)': 'Ahmet Yılmaz (Sürücü)', + 'Ahmet Y??lmazmaz (S??r??c??c??)': 'Ahmet Yılmaz (Sürücü)', + 'Mehmet Demir (S??r??c??)': 'Mehmet Demir (Sürücü)', + 'Mehmet Demir (S??r??c??c??)': 'Mehmet Demir (Sürücü)', + 'Ay??e Kaya (S??r??c??)': 'Ayşe Kaya (Sürücü)', + 'Ay??ee Kaya (S??r??c??c??)': 'Ayşe Kaya (Sürücü)', + 'TEST_S??r??c??_X': 'TEST_Sürücü_X', + 'TEST_S??r??c??c??_X': 'TEST_Sürücü_X', + 'TEST_S??r??c??_Yeni': 'TEST_Sürücü_Yeni', + 'TEST_S??r??c??c??_Yeni': 'TEST_Sürücü_Yeni', + 'Veli (??hlal Testi)': 'Veli (İhlal Testi)', + 'Veli (??hlalal Testi)': 'Veli (İhlal Testi)', + } + + for old, new in mappings.items(): + if old in text: + text = text.replace(old, new) + + text = text.replace('??', 'ı').replace('Sürücücünü', 'Sürücü').replace('Yılmazmaz', 'Yılmaz').replace('Sürücücü', 'Sürücü').replace('Ayşee', 'Ayşe') + return text + +partners = env['res.partner'].search([]) +for p in partners: + fixed = clean_text(p.name) + if fixed != p.name: + print(f"Partner: {p.name} -> {fixed}") + p.name = fixed + +employees = env['hr.employee'].search([]) +for e in employees: + fixed = clean_text(e.name) + if fixed != e.name: + print(f"Employee: {e.name} -> {fixed}") + e.name = fixed + +contracts = env['mymach.fleet.contract'].search([]) +for c in contracts: + fixed = clean_text(c.name) + if fixed != c.name: + print(f"Contract: {c.name} -> {fixed}") + c.name = fixed + +logs = env['mymach.fleet.security.log'].search([]) +for l in logs: + fixed = clean_text(l.name) + if fixed != l.name: + print(f"Log Name: {l.name} -> {fixed}") + l.name = fixed + +env.cr.commit() +print("DATABASE TURKISH CHARACTERS REPAIRED CLEANLY!") diff --git a/addons/mymach_fleet_intelligence/security/ir.model.access.csv b/addons/mymach_fleet_intelligence/security/ir.model.access.csv new file mode 100644 index 0000000..3ec9509 --- /dev/null +++ b/addons/mymach_fleet_intelligence/security/ir.model.access.csv @@ -0,0 +1,16 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mymach_fleet_contract_user,contract_user,model_mymach_fleet_contract,group_mymach_fleet_user,1,0,0,0 +access_mymach_fleet_contract_manager,contract_manager,model_mymach_fleet_contract,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_maintenance_cost_manager,maintenance_cost_manager,model_mymach_fleet_maintenance_cost,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_security_log_manager,log_manager,model_mymach_fleet_security_log,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_route_wizard_user,route_wizard_user,model_mymach_fleet_route_exception_wizard,group_mymach_fleet_user,1,1,1,1 +access_mymach_fleet_reservation_user,reservation_user,model_mymach_fleet_reservation,group_mymach_fleet_user,1,1,1,0 +access_mymach_fleet_reservation_manager,reservation_manager,model_mymach_fleet_reservation,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_home_user,home_user,model_mymach_fleet_home,group_mymach_fleet_user,1,1,1,1 +access_mymach_fleet_fine_user,fine_user,model_mymach_fleet_fine,group_mymach_fleet_user,1,0,0,0 +access_mymach_fleet_fine_manager,fine_manager,model_mymach_fleet_fine,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_handover_user,handover_user,model_mymach_fleet_handover,group_mymach_fleet_user,1,1,1,0 +access_mymach_fleet_handover_manager,handover_manager,model_mymach_fleet_handover,group_mymach_fleet_manager,1,1,1,1 +access_mymach_fleet_periodic_maintenance_user,periodic_maintenance_user,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_user,1,1,0,0 +access_mymach_fleet_periodic_maintenance_manager,periodic_maintenance_manager,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_manager,1,1,1,1 + diff --git a/addons/mymach_fleet_intelligence/security/security_groups.xml b/addons/mymach_fleet_intelligence/security/security_groups.xml new file mode 100644 index 0000000..cb9b192 --- /dev/null +++ b/addons/mymach_fleet_intelligence/security/security_groups.xml @@ -0,0 +1,59 @@ + + + + + + Sürücü / Personel (MyMach) + + + + + + Yönetici / Admin (MyMach) + + + + + + + Sürücüler kendi sözleşmesini görür + + [('driver_id', '=', user.partner_id.id)] + + + + Yöneticiler tüm sözleşmeleri görür + + [(1, '=', 1)] + + + + + + Sürücüler kendi taleplerini görür + + [('create_uid', '=', user.id)] + + + + Yöneticiler tüm talepleri görür + + [(1, '=', 1)] + + + + + + Sürücüler kendi teslimatlarını görür + + [('driver_id', '=', user.partner_id.id)] + + + + Yöneticiler tüm teslimatları görür + + [(1, '=', 1)] + + + + diff --git a/addons/mymach_fleet_intelligence/set_admin_password.py b/addons/mymach_fleet_intelligence/set_admin_password.py new file mode 100644 index 0000000..196522d --- /dev/null +++ b/addons/mymach_fleet_intelligence/set_admin_password.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +user = env['res.users'].sudo().search([('login', '=', 'admin')], limit=1) +if user: + user.write({'password': 'admin'}) + env.cr.commit() + print("SUCCESS: Admin password set to 'admin'") diff --git a/addons/mymach_fleet_intelligence/set_new_admin_password.py b/addons/mymach_fleet_intelligence/set_new_admin_password.py new file mode 100644 index 0000000..f1af21a --- /dev/null +++ b/addons/mymach_fleet_intelligence/set_new_admin_password.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +user = env['res.users'].sudo().search([('login', '=', 'admin')], limit=1) +if user: + user.write({'password': 'MyMach2026!'}) + env.cr.commit() + print("SUCCESS: Admin password updated to MyMach2026!") diff --git a/addons/mymach_fleet_intelligence/static/description/car_hero.png b/addons/mymach_fleet_intelligence/static/description/car_hero.png new file mode 100644 index 0000000..d3b447e Binary files /dev/null and b/addons/mymach_fleet_intelligence/static/description/car_hero.png differ diff --git a/addons/mymach_fleet_intelligence/static/src/css/aesthetics.css b/addons/mymach_fleet_intelligence/static/src/css/aesthetics.css new file mode 100644 index 0000000..1c69d02 --- /dev/null +++ b/addons/mymach_fleet_intelligence/static/src/css/aesthetics.css @@ -0,0 +1,317 @@ +/* MyMach Fleet Intelligence - Minimalist Apple/Glassmorphism Aesthetic */ +:root { + --mymach-bg: #f8fafc; + --mymach-card: rgba(255, 255, 255, 0.7); + --mymach-border: rgba(255, 255, 255, 0.3); + --mymach-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); + --mymach-primary: #0ea5e9; + --mymach-hover: #38bdf8; + --mymach-text: #1e293b; + --mymach-danger: #ef4444; + --mymach-warning: #f59e0b; +} + +/* Make the form view fully edge-to-edge, removing all Odoo paddings/margins/backgrounds */ +.o_content:has(.mymach_zara_form), +.o_form_view:has(.mymach_zara_form), +.mymach_zara_form { + background-color: #0f172a !important; + padding: 0 !important; + margin: 0 !important; + height: 100vh !important; + width: 100% !important; + max-width: 100% !important; + display: flex !important; + flex-direction: column !important; +} + +.mymach_zara_form .o_form_sheet_bg { + background: #0f172a !important; + padding: 0 !important; + margin: 0 !important; + width: 100% !important; + height: 100vh !important; + max-width: 100% !important; +} + +.mymach_zara_form .o_form_sheet { + background-color: #0f172a !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + box-shadow: none !important; + width: 100% !important; + max-width: 100% !important; + height: 100vh !important; + min-height: 100vh !important; +} + +/* Zara/Fashion-inspired Minimalist Editorial Styles */ +.mymach_zara_container { + background-image: url('/mymach_fleet_intelligence/static/description/car_hero.png'); + background-size: cover; + background-position: center; + border-radius: 0px !important; /* Edge-to-edge: no rounded corners */ + position: relative; + overflow: hidden; + width: 100% !important; + height: 100vh !important; + min-height: 100vh !important; + margin: 0 !important; + padding: 0 !important; +} + +.mymach_zara_overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.45) !important; /* Dark overlay only, NO BLUR! */ + z-index: 1; +} + + +.mymach_zara_content { + position: relative; + z-index: 2; + padding: 35px; + display: flex; + flex-direction: column; + gap: 25px; +} + +.mymach_zara_title { + font-family: 'Outfit', 'Helvetica Neue', Arial, sans-serif; + font-weight: 300 !important; + letter-spacing: 4px; + text-transform: uppercase; + color: #ffffff !important; + font-size: 26px !important; + margin: 0; + text-shadow: 0 2px 10px rgba(0,0,0,0.5); +} + +.mymach_zara_subtitle { + font-family: 'Outfit', sans-serif; + font-size: 11px !important; + letter-spacing: 2px; + text-transform: uppercase; + color: #cbd5e1 !important; + margin-top: 8px; + text-shadow: 0 1px 5px rgba(0,0,0,0.5); +} + +/* Premium Light-Glassmorphism Floating White Cards (NOT DARK!) */ +.mymach_zara_btn { + background: rgba(255, 255, 255, 0.90) !important; + backdrop-filter: blur(5px) !important; + -webkit-backdrop-filter: blur(5px) !important; + border: 1px solid rgba(255, 255, 255, 0.5) !important; + border-radius: 10px !important; + color: #0f172a !important; + font-family: 'Outfit', sans-serif; + font-weight: 800 !important; + font-size: 12px !important; + letter-spacing: 1px; + text-transform: uppercase; + padding: 22px 12px !important; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2) !important; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important; + display: inline-block; + width: 100%; + text-align: center; +} + +.mymach_zara_btn:hover { + background: #ffffff !important; + transform: translateY(-4px) scale(1.02); + box-shadow: 0 15px 30px rgba(255, 255, 255, 0.3) !important; + border-color: #ffffff !important; + color: #000000 !important; +} + +.mymach_zara_btn i { + color: #0f172a !important; + font-size: 20px; + margin-bottom: 6px; + transition: transform 0.3s ease; +} + +.mymach_zara_btn:hover i { + transform: scale(1.2); +} + +/* Warning hover variants */ +.mymach_zara_btn_warning { + background: rgba(254, 243, 199, 0.95) !important; + border-color: rgba(245, 158, 11, 0.5) !important; +} + +.mymach_zara_btn_warning:hover { + background: #fef3c7 !important; + border-color: #f59e0b !important; + box-shadow: 0 15px 30px rgba(245, 158, 11, 0.3) !important; +} + +/* Group headings inside background */ +.mymach_zara_group_title { + font-size: 11px; + font-weight: 700; + color: #ffffff; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding-bottom: 6px; + margin-bottom: 15px; + text-transform: uppercase; + letter-spacing: 2px; +} + +/* Araç Kartlarındaki Uyarılar için Titreme Animasyonu (Prediktif Bakım) */ +@keyframes pulse_red { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.05); opacity: 0.8; color: #ef4444; } + 100% { transform: scale(1); opacity: 1; } +} + +.mymach_predictive_alert { + animation: pulse_red 2s infinite; + font-weight: bold; + border-left: 4px solid var(--mymach-danger) !important; + background-color: #fee2e2 !important; +} + +/* Leaflet Canlı Telemetri Pulse İşaretçileri */ +.custom-div-icon { + background: transparent !important; + border: none !important; +} + +.pulse { + width: 14px; + height: 14px; + border-radius: 50%; + position: absolute; + top: -7px; + left: -7px; + border: 2px solid #ffffff; +} + +.pulse-green { + background: #10b981; + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.8); + animation: pulse_green_anim 1.8s infinite; +} + +.pulse-red { + background: #ef4444; + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.8); + animation: pulse_red_anim 1.8s infinite; +} + +@keyframes pulse_green_anim { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + } + 70% { + transform: scale(1.1); + box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); + } + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); + } +} + +@keyframes pulse_red_anim { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); + } + 70% { + transform: scale(1.1); + box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); + } + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); + } +} + +/* Glassmorphic Dashboard Table for Upcoming Maintenances */ +.mymach_zara_table_container { + background: rgba(15, 23, 42, 0.6) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + border-radius: 12px; + padding: 20px; + margin-top: 20px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); +} + +.mymach_zara_dashboard_table { + width: 100%; + border-collapse: collapse; + font-family: 'Outfit', sans-serif; + color: #f8fafc; +} + +.mymach_zara_dashboard_table th { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 2px; + color: #94a3b8; + border-bottom: 2px solid rgba(255, 255, 255, 0.1); + padding: 12px 10px; + text-align: left; +} + +.mymach_zara_dashboard_table td { + padding: 14px 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + font-size: 13px; + vertical-align: middle; +} + +.mymach_zara_dashboard_table tbody tr:hover { + background: rgba(255, 255, 255, 0.03); +} + +/* Badge & Messages styling */ +.mymach_badge { + display: inline-block; + padding: 3px 8px; + border-radius: 20px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} + +.mymach_badge_danger { + background: rgba(239, 68, 68, 0.15) !important; + color: #f87171 !important; + border: 1px solid rgba(239, 68, 68, 0.3) !important; +} + +.mymach_badge_warning { + background: rgba(245, 158, 11, 0.15) !important; + color: #fbbf24 !important; + border: 1px solid rgba(245, 158, 11, 0.3) !important; +} + +.mymach_table_msg { + font-size: 11px; + color: #cbd5e1; + display: block; + margin-top: 3px; + font-style: italic; +} + +.mymach_m_type { + font-weight: bold; + color: #e2e8f0; +} + diff --git a/addons/mymach_fleet_intelligence/static/src/js/dashboard.js b/addons/mymach_fleet_intelligence/static/src/js/dashboard.js new file mode 100644 index 0000000..b086b09 --- /dev/null +++ b/addons/mymach_fleet_intelligence/static/src/js/dashboard.js @@ -0,0 +1,399 @@ +document.addEventListener("DOMContentLoaded", function() { + var telemetryEl = document.getElementById('mymach_telemetry_data'); + if (!telemetryEl) return; + + var vehiclesData = JSON.parse(telemetryEl.getAttribute('data-vehicles') || '[]'); + var logsData = JSON.parse(telemetryEl.getAttribute('data-logs') || '[]'); + var googleMapsApiKey = telemetryEl.getAttribute('data-gmaps-key') || ''; + + var map; + var activePolylines = []; + var activeMarkers = []; + + var isGoogleLoaded = false; + if (googleMapsApiKey) { + if (typeof google !== 'undefined' && google.maps) { + isGoogleLoaded = true; + } + } + + if (isGoogleLoaded) { + map = new google.maps.Map(document.getElementById('map'), { + center: {lat: 41.0766, lng: 29.0124}, + zoom: 12, + mapTypeId: 'hybrid', // Google Uydu + Yol Haritası + zoomControl: true, + zoomControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM }, + mapTypeControl: false + }); + } else { + map = L.map('map', { zoomControl: false }).setView([41.0766, 29.0124], 12); + L.control.zoom({ position: 'bottomright' }).addTo(map); + + // Yüksek Detaylı Esri World Imagery Uydu Haritası Katmanı + L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community', + maxZoom: 19 + }).addTo(map); + + // Şehir ve Yol Etiketleri Katmanı (Uydu üzerine) + L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png', { + subdomains: 'abcd', + maxZoom: 19 + }).addTo(map); + } + + // Mavi Başlangıç İğnesi (Google Maps Stilinde) + function createBlueStartIcon() { + return L.divIcon({ + className: 'custom-blue-start-marker', + html: '
' + + '
' + + '
' + + '
', + iconSize: [26, 26], + iconAnchor: [13, 26] + }); + } + + // Kırmızı Bitiş Hedef İğnesi (Google Maps Stilinde) + function createRedEndIcon() { + return L.divIcon({ + className: 'custom-red-end-marker', + html: '
' + + '
' + + '
' + + '
', + iconSize: [26, 26], + iconAnchor: [13, 26] + }); + } + + // Yol Üzeri Türkçe Çağrı Kutucuğu (Callout Badge) + function createCalloutIcon(text, bgHex) { + var bg = bgHex || '#2563eb'; + return L.divIcon({ + className: 'custom-callout-marker', + html: '
' + text + '
', + iconSize: [130, 26], + iconAnchor: [65, 13] + }); + } + + var vehicleAnimationTimer = null; + + // Canlı Hareket Eden Araç İkonu (SVG 🚘 + Anlık Hız Göstergeli) + function createLiveCarIcon(driverTag, speedText) { + return L.divIcon({ + className: 'custom-live-car-marker', + html: '
' + + '
' + + '🚘 ' + (driverTag || 'Sürücü') + ' • ' + (speedText || '55 km/s') + '' + + '
' + + '
' + + '🚗' + + '
' + + '
', + iconSize: [140, 55], + iconAnchor: [70, 52] + }); + } + + function animateMovingVehicle(routePoints, vehicle) { + if (vehicleAnimationTimer) { + clearInterval(vehicleAnimationTimer); + vehicleAnimationTimer = null; + } + if (!routePoints || routePoints.length < 2) return; + + var currentIdx = 0; + var progress = 0; + var stepSize = 0.08; // Akıcı hareket adımı + + var carMarker = L.marker(routePoints[0], { + icon: createLiveCarIcon(vehicle.driver_tag, "52 km/s") + }).addTo(map); + activeMarkers.push(carMarker); + + vehicleAnimationTimer = setInterval(function() { + if (!routePoints || routePoints.length < 2) return; + progress += stepSize; + if (progress >= 1) { + progress = 0; + currentIdx++; + if (currentIdx >= routePoints.length - 1) { + currentIdx = 0; // Rotayı sürekli akıcı döngüde yenile + } + } + + var p1 = routePoints[currentIdx]; + var p2 = routePoints[currentIdx + 1]; + var lat = p1[0] + (p2[0] - p1[0]) * progress; + var lng = p1[1] + (p2[1] - p1[1]) * progress; + + // Anlık simüle edilen canlı hız (48 - 65 km/s arası dinamik değişim) + var currentSpeed = Math.floor(48 + Math.abs(Math.sin(currentIdx + progress)) * 17) + " km/s"; + + carMarker.setLatLng([lat, lng]); + carMarker.setIcon(createLiveCarIcon(vehicle.driver_tag, currentSpeed)); + }, 120); + } + + function selectDriver(vehicle) { + if (vehicleAnimationTimer) { + clearInterval(vehicleAnimationTimer); + vehicleAnimationTimer = null; + } + + if (isGoogleLoaded) { + activePolylines.forEach(function(p) { p.setMap(null); }); + activeMarkers.forEach(function(m) { m.setMap(null); }); + } else { + activePolylines.forEach(function(p) { map.removeLayer(p); }); + activeMarkers.forEach(function(m) { map.removeLayer(m); }); + } + activePolylines = []; + activeMarkers = []; + + var greenPts = vehicle.green_route || []; + var redPts = vehicle.red_route || []; + var alt1Pts = vehicle.alt1 || []; + var alt2Pts = vehicle.alt2 || []; + + if (isGoogleLoaded) { + if (greenPts.length > 0) { + var polyGreen = new google.maps.Polyline({ + path: greenPts.map(function(p) { return {lat: p[0], lng: p[1]}; }), + strokeColor: '#10b981', + strokeOpacity: 0.95, + strokeWeight: 7, + map: map + }); + activePolylines.push(polyGreen); + } + if (redPts.length > 0) { + var polyRed = new google.maps.Polyline({ + path: redPts.map(function(p) { return {lat: p[0], lng: p[1]}; }), + strokeColor: '#ef4444', + strokeOpacity: 0.95, + strokeWeight: 7, + map: map + }); + activePolylines.push(polyRed); + } + var polyAlt1 = new google.maps.Polyline({ + path: alt1Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }), + strokeColor: '#64748b', + strokeOpacity: 0.55, + strokeWeight: 4, + map: map + }); + var polyAlt2 = new google.maps.Polyline({ + path: alt2Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }), + strokeColor: '#64748b', + strokeOpacity: 0.55, + strokeWeight: 4, + map: map + }); + activePolylines.push(polyAlt1, polyAlt2); + + var allPts = greenPts.concat(redPts).concat(alt1Pts).concat(alt2Pts); + if (allPts.length > 0) { + var startPt = greenPts[0] || allPts[0]; + var endPt = redPts.length > 0 ? redPts[redPts.length - 1] : (greenPts[greenPts.length - 1] || allPts[allPts.length - 1]); + var mStart = new google.maps.Marker({ position: {lat: startPt[0], lng: startPt[1]}, map: map, title: "Başlangıç Noktası" }); + var mEnd = new google.maps.Marker({ position: {lat: endPt[0], lng: endPt[1]}, map: map, title: vehicle.destination }); + activeMarkers.push(mStart, mEnd); + + var bounds = new google.maps.LatLngBounds(); + allPts.forEach(function(p) { bounds.extend({lat: p[0], lng: p[1]}); }); + map.fitBounds(bounds); + } + + } else { + // Leaflet.js Görsel Çizimler (Uydu Temalı) + + // 1. Alternatif Gri Yollar + var polyAlt1 = L.polyline(alt1Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map); + var polyAlt2 = L.polyline(alt2Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map); + activePolylines.push(polyAlt1, polyAlt2); + + // 2. Yeşil Ana Rota Çizgisi + if (greenPts.length > 0) { + var polyGreen = L.polyline(greenPts, { color: '#10b981', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map); + activePolylines.push(polyGreen); + } + + // 3. Kırmızı 1 km+ Sapma Çizgisi + if (redPts.length > 0) { + var polyRed = L.polyline(redPts, { color: '#ef4444', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map); + activePolylines.push(polyRed); + } + + var allPts = greenPts.concat(redPts); + if (allPts.length > 0) { + var startPt = greenPts[0] || allPts[0]; + var endPt = redPts.length > 0 ? redPts[redPts.length - 1] : (greenPts[greenPts.length - 1] || allPts[allPts.length - 1]); + + // Mavi Başlangıç İğnesi (İş Yeri / Genel Merkez) + var mStart = L.marker(startPt, { icon: createBlueStartIcon() }).addTo(map).bindPopup("Başlangıç Noktası (İş Yeri): " + (vehicle.start_location || "Genel Merkez Ofis")); + // Kırmızı Hedef Bitiş İğnesi (Personel Evi / Proje Sahası / Şantiye) + var mEnd = L.marker(endPt, { icon: createRedEndIcon() }).addTo(map).bindPopup("Ulaşım Noktası (Hedef / Ev / Proje): " + vehicle.destination); + + // Yeşil Yol Üzerindeki "35 dk (Planlanan Rota)" Çağrı Kutucuğu + if (greenPts.length > 1) { + var midGreenIdx = Math.floor(greenPts.length / 2); + var mGreenCallout = L.marker(greenPts[midGreenIdx], { icon: createCalloutIcon(vehicle.planned_time, '#2563eb') }).addTo(map); + activeMarkers.push(mGreenCallout); + } + + // Kırmızı Yol Üzerindeki "1.5 km Sapma" Çağrı Kutucuğu + if (redPts.length > 0) { + var midRedIdx = Math.floor(redPts.length / 2); + var mRedCallout = L.marker(redPts[midRedIdx], { icon: createCalloutIcon(vehicle.deviation_label, '#ef4444') }).addTo(map); + activeMarkers.push(mRedCallout); + } + + activeMarkers.push(mStart, mEnd); + + // Rotada Gerçek Zamanlı Canlı Hareket Eden Araç Animasyonunu Başlat + animateMovingVehicle(allPts, vehicle); + + var group = new L.featureGroup([polyAlt1, polyAlt2].concat(activePolylines)); + map.fitBounds(group.getBounds(), { padding: [50, 50] }); + } + } + + // Yüzen Detay Kartını Güncelle + var overlayEl = document.getElementById('driver_info_overlay'); + if (overlayEl) { + var badgeBg = vehicle.is_violation ? 'rgba(239, 68, 68, 0.25)' : (vehicle.is_manager_approved ? 'rgba(56, 189, 248, 0.25)' : 'rgba(16, 185, 129, 0.25)'); + var badgeBorder = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981'); + var badgeText = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981'); + var badgeTitle = vehicle.is_violation + ? '🚨 UYUMSUZ ROTA (YÖNETİCİYE ALARM BİLDİRİLDİ)' + : (vehicle.is_manager_approved ? '✅ YÖNETİCİ ONAYLI PROJE GÖREVİ' : '✅ UYUMLU ANA ROTA'); + + overlayEl.style.display = 'block'; + overlayEl.innerHTML = + '
' + + '
' + + '

' + (vehicle.driver_tag || '') + ' - ' + vehicle.driver_name + '

' + + '' + vehicle.license_plate + ' (' + vehicle.name + ')' + + '
' + + '' + + 'Skor: ' + vehicle.score + + '' + + '
' + + '
' + + badgeTitle + + '
' + + '
' + + '
' + + 'Personel Ev Adresi:' + + '' + (vehicle.driver_home_address || 'Kadıköy/Moda') + '' + + '
' + + '
' + + 'Başlangıç Noktası (İş Yeri):' + + '' + (vehicle.start_location || 'Genel Merkez Ofis') + '' + + '
' + + '
' + + 'Ulaşım Noktası (Hedef / Proje):' + + '' + vehicle.destination + '' + + '
' + + '
' + + 'Planlanan / Kat Edilen:' + + '' + vehicle.planned_km + ' KM / ' + vehicle.driven_km + ' KM' + + '
' + + '
' + + 'Sapma & Durum:' + + '' + vehicle.deviation_label + '' + + '
' + + '
' + + 'Sözleşme Poliçesi:' + + '' + vehicle.policy_desc + '' + + '
' + + '
'; + } + } + + var tableBody = document.getElementById('vehicleTableBody'); + if (tableBody) { + tableBody.innerHTML = ''; + vehiclesData.forEach(function(v, idx) { + var tr = document.createElement('tr'); + tr.style.cursor = 'pointer'; + tr.style.transition = 'all 0.2s ease'; + + var isRev = v.is_violation; + var badgeStyle = isRev + ? 'background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.4);' + : (v.is_manager_approved + ? 'background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4);' + : 'background: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.4);'); + var statusLabel = isRev ? ('🚨 UYUMSUZ (' + v.deviation + ' km)') : (v.is_manager_approved ? '✅ Onaylı Rota' : '✅ Uyumlu'); + + tr.innerHTML = + '' + + '' + (v.driver_tag || '') + ' - ' + v.driver_name + '
' + + '' + v.license_plate + '' + + '' + + '' + + '' + v.destination + '
' + + '' + v.driven_km + ' KM' + + '' + + '' + + '' + v.score + '' + + '' + + '' + + '' + + statusLabel + + '' + + ''; + + tr.addEventListener('click', function() { + var children = tableBody.children; + for (var i = 0; i < children.length; i++) { + children[i].style.background = 'transparent'; + } + tr.style.background = 'rgba(56, 189, 248, 0.2)'; + selectDriver(v); + }); + + tableBody.appendChild(tr); + + if (idx === 0) { + tr.style.background = 'rgba(56, 189, 248, 0.2)'; + setTimeout(function() { selectDriver(v); }, 300); + } + }); + } + + var miniChartEl = document.getElementById('miniChart'); + if (miniChartEl) { + var ctx = miniChartEl.getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { + labels: ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz'], + datasets: [{ + label: 'KM Trend', + data: [2200, 2500, 2100, 2800, 3100, 2700], + borderColor: '#00f2fe', + backgroundColor: 'rgba(0, 242, 254, 0.05)', + borderWidth: 2, + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + plugins: { legend: { display: false } }, + scales: { + x: { grid: { display: false }, ticks: { color: '#94a3b8' } }, + y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } } + } + } + }); + } +}); diff --git a/addons/mymach_fleet_intelligence/static/src/js/dashboard_action.js b/addons/mymach_fleet_intelligence/static/src/js/dashboard_action.js new file mode 100644 index 0000000..036ead6 --- /dev/null +++ b/addons/mymach_fleet_intelligence/static/src/js/dashboard_action.js @@ -0,0 +1,164 @@ +/** @odoo-module **/ + +import { Component, onWillStart, onMounted, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +export class MymachFleetDashboard extends Component { + static template = "mymach_fleet_intelligence.DashboardAction"; + + setup() { + this.orm = useService("orm"); + this.state = useState({ + vehicles: [], + logs: [], + googleMapsApiKey: "", + loading: true + }); + + onWillStart(async () => { + await this.loadDependencies(); + await this.loadData(); + }); + + onMounted(() => { + this.initMapAndCharts(); + }); + } + + async loadDependencies() { + return new Promise((resolve) => { + if (window.L && window.Chart) { + resolve(); + return; + } + + let loadedCount = 0; + const total = 2; // Leaflet JS and Chart JS + + const onDependencyLoaded = () => { + loadedCount++; + if (loadedCount === total) { + resolve(); + } + }; + + // Leaflet CSS + if (!document.getElementById('leaflet_css')) { + const link = document.createElement('link'); + link.id = 'leaflet_css'; + link.rel = 'stylesheet'; + link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; + document.head.appendChild(link); + } + + // Leaflet JS + if (!window.L) { + const script = document.createElement('script'); + script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; + script.onload = onDependencyLoaded; + document.head.appendChild(script); + } else { + onDependencyLoaded(); + } + + // Chart.js + if (!window.Chart) { + const scriptChart = document.createElement('script'); + scriptChart.src = 'https://cdn.jsdelivr.net/npm/chart.js'; + scriptChart.onload = onDependencyLoaded; + document.head.appendChild(scriptChart); + } else { + onDependencyLoaded(); + } + }); + } + + async loadData() { + try { + const data = await this.orm.call("mymach.fleet.security.log", "get_dashboard_data", []); + this.state.vehicles = data.vehicles || []; + this.state.logs = data.logs || []; + this.state.googleMapsApiKey = data.google_maps_api_key || ""; + } catch (error) { + console.error("Failed to load dashboard data", error); + } finally { + this.state.loading = false; + } + } + + initMapAndCharts() { + setTimeout(() => { + const mapEl = document.getElementById('mymach_owl_map'); + if (!mapEl || !window.L) return; + + // Load Leaflet map + const map = window.L.map('mymach_owl_map', { + zoomControl: false + }).setView([41.0082, 28.9784], 11); + + window.L.control.zoom({ position: 'bottomright' }).addTo(map); + + window.L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + subdomains: 'abcd', + maxZoom: 20 + }).addTo(map); + + // Add Leaflet markers + this.state.logs.forEach(function(log) { + if (log.latitude && log.longitude) { + const isAlarm = log.status === 'in_office_moving' || log.status === 'exception'; + const pulseClass = isAlarm ? 'pulse pulse-red' : 'pulse pulse-green'; + const markerHtml = '
'; + + const customIcon = window.L.divIcon({ + html: markerHtml, + className: 'custom-div-icon', + iconSize: [12, 12] + }); + + const marker = window.L.marker([log.latitude, log.longitude], { icon: customIcon }).addTo(map); + + const popupContent = '
' + + '' + log.vehicle_name + ' (' + log.driver_name + ')
' + + '' + log.datetime + '
' + + '' + log.status_desc + '' + + '
'; + marker.bindPopup(popupContent); + } + }); + + // Mini Chart.js + const chartEl = document.getElementById('mymach_owl_chart'); + if (chartEl && window.Chart) { + const ctx = chartEl.getContext('2d'); + new window.Chart(ctx, { + type: 'line', + data: { + labels: ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz'], + datasets: [{ + label: 'KM Trend', + data: [2200, 2500, 2100, 2800, 3100, 2700], + borderColor: '#00f2fe', + backgroundColor: 'rgba(0, 242, 254, 0.05)', + borderWidth: 2, + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + plugins: { legend: { display: false } }, + scales: { + x: { grid: { display: false }, ticks: { color: '#94a3b8' } }, + y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } } + } + } + }); + } + }, 200); + } +} + +registry.category("actions").add("mymach_fleet_dashboard_client_action", MymachFleetDashboard); diff --git a/addons/mymach_fleet_intelligence/static/src/xml/dashboard_action.xml b/addons/mymach_fleet_intelligence/static/src/xml/dashboard_action.xml new file mode 100644 index 0000000..d7eb948 --- /dev/null +++ b/addons/mymach_fleet_intelligence/static/src/xml/dashboard_action.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
diff --git a/addons/mymach_fleet_intelligence/sync_and_launch_server.py b/addons/mymach_fleet_intelligence/sync_and_launch_server.py new file mode 100644 index 0000000..dea2610 --- /dev/null +++ b/addons/mymach_fleet_intelligence/sync_and_launch_server.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +import os +import shutil + +src_dir = r"C:\Users\Ortak\Desktop\MyMach_Fleet_Intelligence_Yedek\mymach_fleet_intelligence" +dest_dir = r"C:\Users\Ortak\Desktop\Odoo-Ceren-MyMach\addons\mymach_fleet_intelligence" + +if os.path.exists(src_dir): + # Copy fresh files from Masaüstü yedek folder to active addons folder + for item in os.listdir(src_dir): + s = os.path.join(src_dir, item) + d = os.path.join(dest_dir, item) + if os.path.isdir(s): + if os.path.exists(d): + shutil.rmtree(d) + shutil.copytree(s, d) + else: + shutil.copy2(s, d) + print("SUCCESS: Synced Desktop files to active Odoo addons path!") +else: + print("ERROR: Source folder not found!") diff --git a/addons/mymach_fleet_intelligence/task.md b/addons/mymach_fleet_intelligence/task.md new file mode 100644 index 0000000..c76c550 --- /dev/null +++ b/addons/mymach_fleet_intelligence/task.md @@ -0,0 +1,12 @@ +# Task List - KM Bakım Hatırlatıcısı & Çoklu Döviz Amortisman Motoru + +- [x] `fleet_vehicle.py` modeline son bakım alanları, 3 para birimli amortisman alanları ve hesaplanan ortalama eklenmeli, varsayılan endeks para birimi TL yapılmalı. +- [x] `periodic_maintenance.py` modeli (mymach.fleet.periodic.maintenance) oluşturulmalı. +- [x] `models/__init__.py` dosyasına yeni model import edilmeli. +- [x] `views/periodic_maintenance_views.xml` (Bakım hatırlatıcıları form, ağaç ve menü tanımları) oluşturulmalı. +- [x] `views/fleet_vehicle_views.xml` (Araç formundaki amortisman alanları, para birimleri ve smart buton) güncellenmeli. +- [x] `views/home_views.xml` (Ana panoya bakım hatırlatıcısı butonu eklenmesi) güncellenmeli. +- [x] `__manifest__.py` dosyasına yeni XML dosyası kaydedilmeli. +- [x] `i18n/tr_TR.po` çeviri dosyasına yeni terimler eklenmeli. +- [x] Odoo modülü güncellenmeli ve docker container yeniden başlatılmalı. +- [x] Odoo kabuğunda ve arayüzde doğrulama testleri yapılmalı. diff --git a/addons/mymach_fleet_intelligence/test_user_session.py b/addons/mymach_fleet_intelligence/test_user_session.py new file mode 100644 index 0000000..47914c0 --- /dev/null +++ b/addons/mymach_fleet_intelligence/test_user_session.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +import urllib.request +import urllib.parse +import http.cookiejar +import re + +cookie_jar = http.cookiejar.CookieJar() +opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) + +# 1. Fetch login page +resp = opener.open('http://localhost:8069/web/login') +html = resp.read().decode('utf-8') +print("1. GET /web/login -> STATUS:", resp.status, "CSRF IN HTML:", 'csrf_token' in html) + +match = re.search(r'name="csrf_token"\s+value="([^"]+)"', html) +csrf_token = match.group(1) if match else "" +print("2. Extracted CSRF Token:", csrf_token[:10] + "...") + +# 2. Post login +data = urllib.parse.urlencode({ + 'login': 'admin', + 'password': 'admin', + 'csrf_token': csrf_token +}).encode('utf-8') + +req = urllib.request.Request('http://localhost:8069/web/login', data=data) +resp_post = opener.open(req) +print("3. POST /web/login -> STATUS:", resp_post.status, "FINAL URL:", resp_post.geturl()) + +# 3. Fetch /odoo +resp_odoo = opener.open('http://localhost:8069/odoo') +html_odoo = resp_odoo.read().decode('utf-8') +print("4. GET /odoo -> STATUS:", resp_odoo.status, "HTML LENGTH:", len(html_odoo), "MENU IN HTML:", 'ir.ui.menu' in html_odoo or 'odoo' in html_odoo) diff --git a/addons/mymach_fleet_intelligence/test_view_groups.py b/addons/mymach_fleet_intelligence/test_view_groups.py new file mode 100644 index 0000000..e896c85 --- /dev/null +++ b/addons/mymach_fleet_intelligence/test_view_groups.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +driver_partner = env['res.partner'].search([('name', 'ilike', 'Ahmet Yılmaz')], limit=1) +driver_user = env['res.users'].search([('partner_id', '=', driver_partner.id)], limit=1) +if not driver_user: + driver_user = env['res.users'].create({ + 'name': 'Ahmet Sürücü Test', + 'login': 'ahmet_test', + 'partner_id': driver_partner.id, + 'groups_id': [(6, 0, [env.ref('mymach_fleet_intelligence.group_mymach_fleet_user').id])] + }) + +print("Test User Groups:", driver_user.groups_id.mapped('name')) + +form_view = env['mymach.fleet.home'].with_user(driver_user).get_views( + [(env.ref('mymach_fleet_intelligence.view_mymach_fleet_home_form').id, 'form')] +) +arch = form_view['views']['form']['arch'] + +print("Is Map button in arch for driver?", "action_mymach_fleet_dashboard_client" in arch) +print("Is Security Log button in arch for driver?", "action_mymach_fleet_security_log" in arch) diff --git a/addons/mymach_fleet_intelligence/test_view_groups_safe.py b/addons/mymach_fleet_intelligence/test_view_groups_safe.py new file mode 100644 index 0000000..9eb7aef --- /dev/null +++ b/addons/mymach_fleet_intelligence/test_view_groups_safe.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +driver_group = env.ref('mymach_fleet_intelligence.group_mymach_fleet_user') +manager_group = env.ref('mymach_fleet_intelligence.group_mymach_fleet_manager') + +users = env['res.users'].search([]) +print("System Users:") +for u in users: + has_user_group = driver_group in u.group_ids + has_mgr_group = manager_group in u.group_ids + print(f"- User: {u.name} (login: {u.login}), Is Driver: {has_user_group}, Is Manager: {has_mgr_group}") + + if has_user_group and not has_mgr_group: + print(f"Testing view for user: {u.name}") + form_view = env['mymach.fleet.home'].with_user(u).get_views( + [(env.ref('mymach_fleet_intelligence.view_mymach_fleet_home_form').id, 'form')] + ) + arch = form_view['views']['form']['arch'] + print("Is Map button in arch for driver?", "action_mymach_fleet_dashboard_client" in arch) + print("Is Security Log button in arch for driver?", "action_mymach_fleet_security_log" in arch) diff --git a/addons/mymach_fleet_intelligence/verify_dashboard_table.py b/addons/mymach_fleet_intelligence/verify_dashboard_table.py new file mode 100644 index 0000000..92e75ba --- /dev/null +++ b/addons/mymach_fleet_intelligence/verify_dashboard_table.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +env = env(user=1) +home = env['mymach.fleet.home'].create({'name': 'Ana Sayfa Test'}) +print("upcoming_maintenance_html field content when empty:") +print(home.upcoming_maintenance_html) + +# Create a test vehicle and maintenance +model_id = env['fleet.vehicle.model'].search([], limit=1).id +vehicle = env['fleet.vehicle'].create({ + 'model_id': model_id, + 'license_plate': 'TEST-BAKIM-34', + 'fuel_type': 'diesel', + 'odometer': 50.0, +}) +# Create a maintenance record +maint = env['mymach.fleet.periodic.maintenance'].create({ + 'vehicle_id': vehicle.id, + 'maintenance_type': 'small', + 'last_odometer': 0.0, + 'target_odometer': 10000, + 'target_date': '2026-09-01' +}) +# Re-compute and print upcoming html field +home._compute_upcoming_maintenance_html() +print("upcoming_maintenance_html field content with pending record:") +print(home.upcoming_maintenance_html) + +# Clean up +maint.unlink() +# Delete vehicle (also delete its odometer logs) +odometers = env['fleet.vehicle.odometer'].search([('vehicle_id', '=', vehicle.id)]) +odometers.unlink() +vehicle.unlink() +home.unlink() +print("VERIFICATION COMPLETED!") diff --git a/addons/mymach_fleet_intelligence/verify_maintenance_and_currency.py b/addons/mymach_fleet_intelligence/verify_maintenance_and_currency.py new file mode 100644 index 0000000..f7f836f --- /dev/null +++ b/addons/mymach_fleet_intelligence/verify_maintenance_and_currency.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +env = env(user=1) + +# Find or create a test vehicle +model_id = env['fleet.vehicle.model'].search([], limit=1).id +vehicle = env['fleet.vehicle'].create({ + 'model_id': model_id, + 'license_plate': 'TEST-BAKIM-34', + 'fuel_type': 'diesel', + 'odometer': 0.0, + # Set multi-currency depreciation + 'depreciation_cost_try': 100000.0, + 'depreciation_cost_usd': 3000.0, + 'depreciation_cost_eur': 2800.0, +}) + +print(f"TEST VEHICLE CREATED: {vehicle.name}") +print(f"Initial Last Maintenance Odometer: {vehicle.last_maintenance_odometer}") +print(f"Initial Last Maintenance Date: {vehicle.last_maintenance_date}") + +# Check average depreciation cost (TRY) +print(f"TRY cost: {vehicle.depreciation_cost_try}") +print(f"USD cost: {vehicle.depreciation_cost_usd}") +print(f"EUR cost: {vehicle.depreciation_cost_eur}") +print(f"Computed Average Depreciation (TRY): {vehicle.depreciation_cost_avg:.2f}") + +# Check if initial maintenance was created +maintenance = env['mymach.fleet.periodic.maintenance'].search([('vehicle_id', '=', vehicle.id)], limit=1) +if maintenance: + print(f"Otonom Bakım Planlandı: {maintenance.name}") + print(f"Bakım Türü: {maintenance.maintenance_type}") + print(f"Hedef KM: {maintenance.target_odometer}") + print(f"Hedef Tarih: {maintenance.target_date}") + print(f"Durum: {maintenance.state}") + + # Simulating vehicle odometer update to 12,000 KM (exceeding target 10,000 KM) + vehicle.write({'odometer': 12000.0}) + # Re-compute state + maintenance._compute_state() + print(f"Yeni KM (12000) Sonrası Durum: {maintenance.state} (Beklenen: overdue)") + + # Complete maintenance + maintenance.action_complete() + print(f"Bakım Tamamlandı! Yeni Araç Son Bakım KM: {vehicle.last_maintenance_odometer}") + print(f"Yeni Araç Son Bakım Tarihi: {vehicle.last_maintenance_date}") + + # Check if NEXT maintenance was created + next_maint = env['mymach.fleet.periodic.maintenance'].search([ + ('vehicle_id', '=', vehicle.id), + ('state', '=', 'pending') + ], limit=1) + if next_maint: + print(f"Bir Sonraki Otonom Bakım Planlandı: {next_maint.name}") + print(f"Yeni Bakım Türü: {next_maint.maintenance_type}") + print(f"Yeni Hedef KM: {next_maint.target_odometer}") + print(f"Yeni Hedef Tarih: {next_maint.target_date}") +else: + print("HATA: İlk periyodik bakım planlanamadı!") + +# Clean up test records +maintenance.unlink() +if 'next_maint' in locals() and next_maint: + next_maint.unlink() +vehicle.unlink() +print("VERIFICATION COMPLETED!") diff --git a/addons/mymach_fleet_intelligence/views/contract_views.xml b/addons/mymach_fleet_intelligence/views/contract_views.xml new file mode 100644 index 0000000..6742112 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/contract_views.xml @@ -0,0 +1,94 @@ + + + + + mymach.fleet.contract.list + mymach.fleet.contract + + + + + + + + + + + + + + + mymach.fleet.contract.form + mymach.fleet.contract + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + mymach.fleet.contract.search + mymach.fleet.contract + + + + + + + + + + + + + Tahsis Sözleşmeleri + mymach.fleet.contract + list,form + + + + + + + +
diff --git a/addons/mymach_fleet_intelligence/views/dashboard_template.xml b/addons/mymach_fleet_intelligence/views/dashboard_template.xml new file mode 100644 index 0000000..0a60c08 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/dashboard_template.xml @@ -0,0 +1,235 @@ + + + + + + + Canlı Harita + mymach_fleet_dashboard_client_action + + + + + diff --git a/addons/mymach_fleet_intelligence/views/fleet_fine_views.xml b/addons/mymach_fleet_intelligence/views/fleet_fine_views.xml new file mode 100644 index 0000000..6ecaa52 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/fleet_fine_views.xml @@ -0,0 +1,87 @@ + + + + + mymach.fleet.fine.list + mymach.fleet.fine + + + + + + + + + + + + + + + + + mymach.fleet.fine.form + mymach.fleet.fine + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + +
+ + +
+
+ + + + mymach.fleet.fine.search + mymach.fleet.fine + + + + + + + + + + + + + + + Cezalar ve HGS Geçişleri + mymach.fleet.fine + list,form + + + + + +
diff --git a/addons/mymach_fleet_intelligence/views/fleet_handover_views.xml b/addons/mymach_fleet_intelligence/views/fleet_handover_views.xml new file mode 100644 index 0000000..d7b0a7d --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/fleet_handover_views.xml @@ -0,0 +1,175 @@ + + + + + mymach.fleet.handover.list + mymach.fleet.handover + + + + + + + + + + + + + + + + mymach.fleet.handover.form + mymach.fleet.handover + +
+
+ +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Teslim Öncesi vs Sonrası Durumu:
+ + + + + + + + + + + + + + + + + + + + + + + +
MetrikAlış (Önce)İade (Sonra)Fark / Değişim
Kilometre (KM)+ KM
Depo Oranı%% + % + +% +
+
+ +
+
+
Alış Fotoğrafı
+ +
+
+
+
İade Fotoğrafı
+ +
+
+
+
+ + + + +
+
+
+ + +
+
+ + + + mymach.fleet.handover.search + mymach.fleet.handover + + + + + + + + + + + + + + + + + + + Teslim Alım & İade Otomasyonu + mymach.fleet.handover + list,form + + + + + +
diff --git a/addons/mymach_fleet_intelligence/views/fleet_reservation_views.xml b/addons/mymach_fleet_intelligence/views/fleet_reservation_views.xml new file mode 100644 index 0000000..30fdb40 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/fleet_reservation_views.xml @@ -0,0 +1,105 @@ + + + + + + Araç Rezervasyon Sequence + mymach.fleet.reservation + RNDV-%(year)s- + 4 + + + + + + + mymach.fleet.reservation.list + mymach.fleet.reservation + + + + + + + + + + + + + + + mymach.fleet.reservation.form + mymach.fleet.reservation + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + +
+ + + +
+
+ + + + mymach.fleet.reservation.calendar + mymach.fleet.reservation + + + + + + + + + + + + Araç Randevuları + mymach.fleet.reservation + calendar,list,form + +

+ İlk araç talebinizi oluşturun! +

+

+ Araçların doluluk oranlarını takvim üzerinden takip edebilirsiniz. +

+
+
+ + + +
diff --git a/addons/mymach_fleet_intelligence/views/fleet_vehicle_views.xml b/addons/mymach_fleet_intelligence/views/fleet_vehicle_views.xml new file mode 100644 index 0000000..57f3f5b --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/fleet_vehicle_views.xml @@ -0,0 +1,160 @@ + + + + + mymach.fleet.vehicle.form.inherit + fleet.vehicle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mymach.fleet.vehicle.log.services.form.inherit + fleet.vehicle.log.services + + + + + + + + + + + + Araç Kartları + fleet.vehicle + kanban,list,form + +

+ Yönetilecek ilk aracınızı oluşturun! +

+
+
+ + + + +
diff --git a/addons/mymach_fleet_intelligence/views/home_views.xml b/addons/mymach_fleet_intelligence/views/home_views.xml new file mode 100644 index 0000000..64921f2 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/home_views.xml @@ -0,0 +1,289 @@ + + + + + Yapay Zeka Servis Bekleyen Araçlar + fleet.vehicle + list,form + [('needs_predictive_maintenance', '=', True)] + + + + + Kritik Maliyet Alarmı Veren Araçlar + fleet.vehicle + list,form + [('is_maintenance_critical', '=', True)] + + + + + Canlı Harita Analizi + + code + +record = env['mymach.fleet.home'].search([], limit=1) +if not record: + record = env['mymach.fleet.home'].create({'name': 'Ana Sayfa'}) + +action = { + 'type': 'ir.actions.act_window', + 'name': 'Canlı Harita Analizi', + 'res_model': 'mymach.fleet.home', + 'res_id': record.id, + 'view_mode': 'form', + 'views': [(env.ref('mymach_fleet_intelligence.view_mymach_fleet_home_chart_only_form').id, 'form')], + 'target': 'current', + 'context': {'clear_breadcrumbs': True}, +} + + + + + mymach.fleet.home.form + mymach.fleet.home + +
+
+
+ +
+ +
+ + +
+ +
+

MYMACH FLEET INTELLIGENCE

+ +
+ + +
+

Temel Operasyonlar

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

Güvenlik & Rota Uyum Motoru

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

Analitik Paneller & Gider Yönetimi

+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

+ 💰 FİNANS GİDER MATRİSİ VE ARAÇ MALİYET ANALİTİĞİ +

+
+
+ + +
+
+
+
+
+ +
+
+ + +
+ +
+
+
+
+
+
+
+ + + + Ana Pano + + code + +record = env['mymach.fleet.home'].search([], limit=1) +if not record: + record = env['mymach.fleet.home'].create({'name': 'Ana Sayfa'}) + +action = { + 'type': 'ir.actions.act_window', + 'name': 'Filo Zekası', + 'res_model': 'mymach.fleet.home', + 'res_id': record.id, + 'view_mode': 'form', + 'views': [(env.ref('mymach_fleet_intelligence.view_mymach_fleet_home_form').id, 'form')], + 'target': 'current', + 'context': {'clear_breadcrumbs': True}, +} + + + + + + + + + mymach.fleet.home.chart.only.form + mymach.fleet.home + +
+
+
+ +
+ +
+ + +
+ +
+

KİLOMETRE & YAKIT ANALİZİ

+

FİLO KİLOMETRE GELİŞİMİ VE YAKIT TÜKETİM ANALİTİKLERİ

+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ + +
+

+ 🗺️ OTONOM ROTA PLANLAMA VE SAPMA DENETİMİ (1 KM TOLERANS) +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + + +
diff --git a/addons/mymach_fleet_intelligence/views/maintenance_cost_views.xml b/addons/mymach_fleet_intelligence/views/maintenance_cost_views.xml new file mode 100644 index 0000000..d92015b --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/maintenance_cost_views.xml @@ -0,0 +1,77 @@ + + + + + mymach.fleet.maintenance.cost.list + mymach.fleet.maintenance.cost + + + + + + + + + + + + + + + mymach.fleet.maintenance.cost.form + mymach.fleet.maintenance.cost + +
+ +
+

+ +

+
+ + + + + + + + + + + +
+
+
+
+ + + + mymach.fleet.maintenance.cost.search + mymach.fleet.maintenance.cost + + + + + + + + + + + + + + Gider Matrisi (Araç Bazlı Analiz) + mymach.fleet.maintenance.cost + list,form + {'search_default_group_by_vehicle': 1} + + + + +
diff --git a/addons/mymach_fleet_intelligence/views/periodic_maintenance_views.xml b/addons/mymach_fleet_intelligence/views/periodic_maintenance_views.xml new file mode 100644 index 0000000..cf23529 --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/periodic_maintenance_views.xml @@ -0,0 +1,104 @@ + + + + + mymach.fleet.periodic.maintenance.tree + mymach.fleet.periodic.maintenance + + + + + + + + + + + + + + + + mymach.fleet.periodic.maintenance.form + mymach.fleet.periodic.maintenance + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + mymach.fleet.periodic.maintenance.search + mymach.fleet.periodic.maintenance + + + + + + + + + + + + + + + + + + Periyodik Bakım Takibi + mymach.fleet.periodic.maintenance + list,form + + {'search_default_overdue': 1, 'search_default_pending': 1} + +

+ İlk periyodik bakım hatırlatıcısını ekleyin! +

+

+ Sistem, araçların yakıt türü ve kilometresine göre periyodik bakımları otomatik olarak takip eder. +

+
+
+ + + +
diff --git a/addons/mymach_fleet_intelligence/views/route_exception_wizard_views.xml b/addons/mymach_fleet_intelligence/views/route_exception_wizard_views.xml new file mode 100644 index 0000000..648bf8f --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/route_exception_wizard_views.xml @@ -0,0 +1,31 @@ + + + + mymach.fleet.route.exception.wizard.form + mymach.fleet.route.exception.wizard + +
+ + + + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/addons/mymach_fleet_intelligence/views/security_log_views.xml b/addons/mymach_fleet_intelligence/views/security_log_views.xml new file mode 100644 index 0000000..b3a62ac --- /dev/null +++ b/addons/mymach_fleet_intelligence/views/security_log_views.xml @@ -0,0 +1,81 @@ + + + + mymach.fleet.security.log.list + mymach.fleet.security.log + + + + + + + + + + +