İlk versiyon: MyMach Fleet Intelligence ve yapılandırmalar
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -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/
|
||||||
2
addons/mymach_fleet_intelligence/__init__.py
Normal file
2
addons/mymach_fleet_intelligence/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
37
addons/mymach_fleet_intelligence/__manifest__.py
Normal file
37
addons/mymach_fleet_intelligence/__manifest__.py
Normal file
@@ -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',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
16
addons/mymach_fleet_intelligence/check_database_modules.py
Normal file
16
addons/mymach_fleet_intelligence/check_database_modules.py
Normal file
@@ -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()
|
||||||
6
addons/mymach_fleet_intelligence/check_fields.py
Normal file
6
addons/mymach_fleet_intelligence/check_fields.py
Normal file
@@ -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}")
|
||||||
5
addons/mymach_fleet_intelligence/check_handovers.py
Normal file
5
addons/mymach_fleet_intelligence/check_handovers.py
Normal file
@@ -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}")
|
||||||
8
addons/mymach_fleet_intelligence/check_modules.py
Normal file
8
addons/mymach_fleet_intelligence/check_modules.py
Normal file
@@ -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()
|
||||||
12
addons/mymach_fleet_intelligence/cleanup_test.py
Normal file
12
addons/mymach_fleet_intelligence/cleanup_test.py
Normal file
@@ -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.")
|
||||||
26
addons/mymach_fleet_intelligence/clear_all_demo_data.py
Normal file
26
addons/mymach_fleet_intelligence/clear_all_demo_data.py
Normal file
@@ -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!")
|
||||||
1
addons/mymach_fleet_intelligence/controllers/__init__.py
Normal file
1
addons/mymach_fleet_intelligence/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import main
|
||||||
336
addons/mymach_fleet_intelligence/controllers/main.py
Normal file
336
addons/mymach_fleet_intelligence/controllers/main.py
Normal file
@@ -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
|
||||||
|
})
|
||||||
82
addons/mymach_fleet_intelligence/create_demo_maintenances.py
Normal file
82
addons/mymach_fleet_intelligence/create_demo_maintenances.py
Normal file
@@ -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ı)")
|
||||||
27
addons/mymach_fleet_intelligence/data/cron_jobs.xml
Normal file
27
addons/mymach_fleet_intelligence/data/cron_jobs.xml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Faz 8: Akaryakıt API Senkronizasyonu ve Prediktif Bakım Zekası (Her Gece Çalışır) -->
|
||||||
|
<record id="ir_cron_sync_fuel_data" model="ir.cron">
|
||||||
|
<field name="name">MyMach: Akaryakıt Senkronizasyonu & Prediktif Bakım (AI)</field>
|
||||||
|
<field name="model_id" ref="mymach_fleet_intelligence.model_mymach_fleet_maintenance_cost"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.action_sync_fuel_data_cron()</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="active" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<!-- Otonom Depo Temizliği: 10 Günlük Fotoğrafları Silme (Her Gece Çalışır) -->
|
||||||
|
<record id="ir_cron_purge_handover_photos" model="ir.cron">
|
||||||
|
<field name="name">MyMach: 10 Günlük Teslimat Fotoğraflarını Temizleme</field>
|
||||||
|
<field name="model_id" ref="mymach_fleet_intelligence.model_mymach_fleet_handover"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.action_purge_expired_photos()</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="active" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -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)
|
||||||
25
addons/mymach_fleet_intelligence/fix_base_groups.py
Normal file
25
addons/mymach_fleet_intelligence/fix_base_groups.py
Normal file
@@ -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!")
|
||||||
18
addons/mymach_fleet_intelligence/fix_company_and_modules.py
Normal file
18
addons/mymach_fleet_intelligence/fix_company_and_modules.py
Normal file
@@ -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!")
|
||||||
93
addons/mymach_fleet_intelligence/fix_db_sql.py
Normal file
93
addons/mymach_fleet_intelligence/fix_db_sql.py
Normal file
@@ -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.")
|
||||||
88
addons/mymach_fleet_intelligence/fix_db_turkish_chars.py
Normal file
88
addons/mymach_fleet_intelligence/fix_db_turkish_chars.py
Normal file
@@ -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.")
|
||||||
40
addons/mymach_fleet_intelligence/fix_external_ids.py
Normal file
40
addons/mymach_fleet_intelligence/fix_external_ids.py
Normal file
@@ -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!")
|
||||||
16
addons/mymach_fleet_intelligence/fix_lang_xmlid.py
Normal file
16
addons/mymach_fleet_intelligence/fix_lang_xmlid.py
Normal file
@@ -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!")
|
||||||
16
addons/mymach_fleet_intelligence/fix_odoo_conf.py
Normal file
16
addons/mymach_fleet_intelligence/fix_odoo_conf.py
Normal file
@@ -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!")
|
||||||
24
addons/mymach_fleet_intelligence/fix_users.py
Normal file
24
addons/mymach_fleet_intelligence/fix_users.py
Normal file
@@ -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!")
|
||||||
547
addons/mymach_fleet_intelligence/i18n/tr_TR.po
Normal file
547
addons/mymach_fleet_intelligence/i18n/tr_TR.po
Normal file
@@ -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)"
|
||||||
|
|
||||||
56
addons/mymach_fleet_intelligence/inspect_cols.py
Normal file
56
addons/mymach_fleet_intelligence/inspect_cols.py
Normal file
@@ -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!")
|
||||||
5
addons/mymach_fleet_intelligence/inspect_users.py
Normal file
5
addons/mymach_fleet_intelligence/inspect_users.py
Normal file
@@ -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)
|
||||||
9
addons/mymach_fleet_intelligence/install_all_modules.py
Normal file
9
addons/mymach_fleet_intelligence/install_all_modules.py
Normal file
@@ -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!")
|
||||||
@@ -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!")
|
||||||
10
addons/mymach_fleet_intelligence/models/__init__.py
Normal file
10
addons/mymach_fleet_intelligence/models/__init__.py
Normal file
@@ -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
|
||||||
88
addons/mymach_fleet_intelligence/models/fleet_contract.py
Normal file
88
addons/mymach_fleet_intelligence/models/fleet_contract.py
Normal file
@@ -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"🚨 <b>KRİTİK GÜVENLİK ALARMI:</b> 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
|
||||||
97
addons/mymach_fleet_intelligence/models/fleet_fine.py
Normal file
97
addons/mymach_fleet_intelligence/models/fleet_fine.py
Normal file
@@ -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"🚨 <b>Şahsi Kullanım İhlal Cezası:</b> {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'})
|
||||||
148
addons/mymach_fleet_intelligence/models/fleet_handover.py
Normal file
148
addons/mymach_fleet_intelligence/models/fleet_handover.py
Normal file
@@ -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="🗑️ <b>Otonom Depo Temizliği:</b> Veritabanı boyutunu korumak amacıyla 10 günden eski teslimat fotoğrafları otonom olarak silinmiştir."
|
||||||
|
)
|
||||||
|
return count
|
||||||
84
addons/mymach_fleet_intelligence/models/fleet_reservation.py
Normal file
84
addons/mymach_fleet_intelligence/models/fleet_reservation.py
Normal file
@@ -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"📩 <b>YÖNETİCİYE GÖREV BİLDİRİMİ:</b> {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'})
|
||||||
321
addons/mymach_fleet_intelligence/models/fleet_vehicle.py
Normal file
321
addons/mymach_fleet_intelligence/models/fleet_vehicle.py
Normal file
@@ -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.")
|
||||||
|
|
||||||
735
addons/mymach_fleet_intelligence/models/home.py
Normal file
735
addons/mymach_fleet_intelligence/models/home.py
Normal file
@@ -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 = "<div style='color:#cbd5e1; text-align:center; padding:20px;'>Araç seçilmedi.</div>"
|
||||||
|
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"""
|
||||||
|
<div class="card border-0 shadow-sm mt-3 mb-3" style="border-radius: 12px; border: 1px solid #e2e8f0; background: #ffffff; padding: 20px;">
|
||||||
|
<h4 style="color: #1e293b; font-size: 16px; font-weight: 700; margin-top: 0; margin-bottom: 20px;">
|
||||||
|
🚘 {vehicle.license_plate} ({vehicle.name or ""}) - FİNANSAL GİDER MATRİSİ ANALİZİ
|
||||||
|
</h4>
|
||||||
|
<div class="row text-center mb-4">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: #f0fdf4; border: 1.5px solid #10b981; border-radius: 12px; padding: 18px;">
|
||||||
|
<div style="color: #047857; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">BİR KM BAŞINA MALİYET</div>
|
||||||
|
<div style="color: #059669; font-size: 24px; font-weight: 800; margin-top: 6px;">{cpk_tl:,.2f} ₺ / KM</div>
|
||||||
|
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{cpk_avg:,.2f} ₺</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: #f0f9ff; border: 1.5px solid #38bdf8; border-radius: 12px; padding: 18px;">
|
||||||
|
<div style="color: #0369a1; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">YILLIK TOPLAM MALİYET</div>
|
||||||
|
<div style="color: #0284c7; font-size: 24px; font-weight: 800; margin-top: 6px;">{annual_tl:,.2f} ₺ / YIL</div>
|
||||||
|
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{annual_avg:,.2f} ₺</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: #fffbeb; border: 1.5px solid #f59e0b; border-radius: 12px; padding: 18px;">
|
||||||
|
<div style="color: #b45309; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">AYLIK ORTALAMA MALİYET</div>
|
||||||
|
<div style="color: #d97706; font-size: 24px; font-weight: 800; margin-top: 6px;">{monthly_tl:,.2f} ₺ / AY</div>
|
||||||
|
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{monthly_avg:,.2f} ₺</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card border-0 shadow-sm" style="border-radius: 8px; border: 1px solid #e2e8f0; background: #ffffff;">
|
||||||
|
<div class="card-header bg-light border-bottom d-flex align-items-center justify-content-between" style="padding: 10px 15px;">
|
||||||
|
<h6 class="mb-0 font-weight-bold text-dark" style="font-size: 13px;">
|
||||||
|
💱 3 Para Birimli Karşılaştırmalı Gider Listbox Tablosu
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-success text-white" style="font-size: 11px;">Canlı Kur Dönüşümü</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-sm table-striped table-hover mb-0" style="font-size: 13px; width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8fafc; color: #475569; border-bottom: 2px solid #e2e8f0;">
|
||||||
|
<th style="padding: 10px 15px; width: 25%;">Metrik Başlığı</th>
|
||||||
|
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">🇹🇷 TL (Ana Para Birimi)</th>
|
||||||
|
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">💵 {c1_name} (Döviz 1)</th>
|
||||||
|
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">💶 {c2_name} (Döviz 2)</th>
|
||||||
|
<th style="padding: 10px 15px; width: 25%; font-weight: 700; color: #047857; background-color: #ecfdf5;">📊 3 Dövizli Ortalama Tahminî Değer (TL)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid #f1f5f9;">
|
||||||
|
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">BİR KM BAŞINA MALİYET</td>
|
||||||
|
<td style="padding: 10px 15px; color: #059669; font-weight: 600;">{cpk_tl:,.2f} ₺</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{cpk_c1:,.2f} {c1_name}</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{cpk_c2:,.2f} {c2_name}</td>
|
||||||
|
<td style="padding: 10px 15px; font-weight: 700; color: #047857; background-color: #f0fdf4;">{cpk_avg:,.2f} ₺</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid #f1f5f9;">
|
||||||
|
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">YILLIK TOPLAM MALİYET</td>
|
||||||
|
<td style="padding: 10px 15px; color: #0284c7; font-weight: 600;">{annual_tl:,.2f} ₺</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{annual_c1:,.2f} {c1_name}</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{annual_c2:,.2f} {c2_name}</td>
|
||||||
|
<td style="padding: 10px 15px; font-weight: 700; color: #0369a1; background-color: #f0f9ff;">{annual_avg:,.2f} ₺</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">AYLIK ORTALAMA MALİYET</td>
|
||||||
|
<td style="padding: 10px 15px; color: #d97706; font-weight: 600;">{monthly_tl:,.2f} ₺</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{monthly_c1:,.2f} {c1_name}</td>
|
||||||
|
<td style="padding: 10px 15px; color: #334155;">{monthly_c2:,.2f} {c2_name}</td>
|
||||||
|
<td style="padding: 10px 15px; font-weight: 700; color: #b45309; background-color: #fffbeb;">{monthly_avg:,.2f} ₺</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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 = """
|
||||||
|
<div style="text-align: center; color: #94a3b8; padding: 30px; font-size: 14px; border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px; background: rgba(15, 23, 42, 0.4);">
|
||||||
|
<i class="fa fa-check-circle" style="font-size: 28px; color: #10b981; margin-bottom: 10px; display: block;"></i>
|
||||||
|
Yaklaşan veya süresi geçmiş periyodik bakım hatırlatıcısı bulunmamaktadır.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
record.upcoming_maintenance_html = html
|
||||||
|
return
|
||||||
|
|
||||||
|
html_lines = []
|
||||||
|
html_lines.append('<div class="mymach_zara_table_container">')
|
||||||
|
html_lines.append('<h3 class="mymach_zara_group_title" style="color: #fbbf24; border-color: rgba(245, 158, 11, 0.25); margin-bottom: 15px;">⏳ Yaklaşan ve Geciken Periyodik Bakım Listesi</h3>')
|
||||||
|
html_lines.append('<table class="mymach_zara_dashboard_table">')
|
||||||
|
html_lines.append('<thead>')
|
||||||
|
html_lines.append('<tr>')
|
||||||
|
html_lines.append('<th>Araç (Plaka)</th>')
|
||||||
|
html_lines.append('<th>Bakım Türü</th>')
|
||||||
|
html_lines.append('<th>Hedef Kilometre</th>')
|
||||||
|
html_lines.append('<th>Hedef Tarih</th>')
|
||||||
|
html_lines.append('<th>Öncelik & Bildirim Durumu</th>')
|
||||||
|
html_lines.append('</tr>')
|
||||||
|
html_lines.append('</thead>')
|
||||||
|
html_lines.append('<tbody>')
|
||||||
|
|
||||||
|
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'<tr style="{row_style}">')
|
||||||
|
html_lines.append(f'<td><strong style="color: #ffffff; font-size: 14px;">{m.vehicle_id.license_plate or ""}</strong><br/><span style="font-size: 11px; color: #94a3b8;">{m.vehicle_id.name or ""}</span></td>')
|
||||||
|
html_lines.append(f'<td><span class="mymach_m_type">{m_label}</span></td>')
|
||||||
|
html_lines.append(f'<td><strong>{m.target_odometer:.0f} KM</strong><br/><span style="font-size: 11px; color: #94a3b8;">Mevcut: {m.odometer:.0f} KM</span></td>')
|
||||||
|
html_lines.append(f'<td><strong style="color: #f8fafc;">{date_str}</strong></td>')
|
||||||
|
html_lines.append(f'<td><span class="{badge_class}">{status_text}</span><br/><span class="mymach_table_msg">{warning_msg}</span></td>')
|
||||||
|
html_lines.append('</tr>')
|
||||||
|
|
||||||
|
html_lines.append('</tbody>')
|
||||||
|
html_lines.append('</table>')
|
||||||
|
html_lines.append('</div>')
|
||||||
|
|
||||||
|
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 = """
|
||||||
|
<div style="text-align: center; color: #94a3b8; padding: 30px; font-size: 14px; border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px; background: rgba(15, 23, 42, 0.4);">
|
||||||
|
<i class="fa fa-info-circle" style="font-size: 28px; color: #38bdf8; margin-bottom: 10px; display: block;"></i>
|
||||||
|
Grafik gösterimi için sistemde kayıtlı aktif bir araç bulunamadı.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: #cbd5e1;
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
.chart-box {{
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.35);
|
||||||
|
height: 300px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
h3 {{
|
||||||
|
color: #fbbf24;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="chart-box">
|
||||||
|
<h3>📈 {vehicle.license_plate or ""} Telemetri Analizi & Finansal Gider Analitiği</h3>
|
||||||
|
<div style="position: relative; height: 210px; width: 100%;">
|
||||||
|
<canvas id="canvasChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const labels = {json.dumps(labels)};
|
||||||
|
const eventData = {json.dumps({int(k): v for k, v in event_labels.items()})};
|
||||||
|
const datasets = {json.dumps(raw_datasets)};
|
||||||
|
const metric = '{metric}';
|
||||||
|
|
||||||
|
// Radikal olayları (bakım, onarım, lastik vb.) daha büyük işaretçilerle işaretleme
|
||||||
|
datasets.forEach(ds => {{
|
||||||
|
ds.pointRadius = labels.map((_, idx) => eventData[idx] ? 8 : 3);
|
||||||
|
ds.pointHoverRadius = labels.map((_, idx) => eventData[idx] ? 12 : 6);
|
||||||
|
ds.pointBackgroundColor = labels.map((_, idx) => eventData[idx] ? '#ef4444' : ds.borderColor); // Kırmızı nokta
|
||||||
|
ds.pointBorderColor = labels.map((_, idx) => eventData[idx] ? '#ffffff' : ds.borderColor);
|
||||||
|
ds.pointBorderWidth = labels.map((_, idx) => eventData[idx] ? 2.5 : 1);
|
||||||
|
}});
|
||||||
|
|
||||||
|
window.onload = function() {{
|
||||||
|
const ctx = document.getElementById('canvasChart');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
new Chart(ctx, {{
|
||||||
|
type: 'line',
|
||||||
|
data: {{
|
||||||
|
labels: labels,
|
||||||
|
datasets: datasets
|
||||||
|
}},
|
||||||
|
options: {{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {{
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}},
|
||||||
|
plugins: {{
|
||||||
|
legend: {{
|
||||||
|
labels: {{
|
||||||
|
color: '#cbd5e1',
|
||||||
|
font: {{ size: 11 }}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
tooltip: {{
|
||||||
|
backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
||||||
|
titleColor: '#ffffff',
|
||||||
|
bodyColor: '#cbd5e1',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 10,
|
||||||
|
callbacks: {{
|
||||||
|
label: function(context) {{
|
||||||
|
let val = context.parsed.y;
|
||||||
|
let unit = '';
|
||||||
|
if (context.dataset.label.includes('Kilometre')) unit = ' KM';
|
||||||
|
else if (context.dataset.label.includes('Litre') || context.dataset.label.includes('Yakıt')) unit = ' Litre';
|
||||||
|
if (context.dataset.label.includes('Gideri') || context.dataset.label.includes('Maliyet')) unit = ' TL';
|
||||||
|
return context.dataset.label + ': ' + val.toLocaleString('tr-TR', {{minimumFractionDigits: 1, maximumFractionDigits: 1}}) + unit;
|
||||||
|
}},
|
||||||
|
footer: function(tooltipItems) {{
|
||||||
|
const index = tooltipItems[0].dataIndex;
|
||||||
|
const events = eventData[index];
|
||||||
|
if (events && events.length > 0) {{
|
||||||
|
return '\\n🚨 Radikal Değişimler / İşlemler:\\n' + events.join('\\n');
|
||||||
|
}}
|
||||||
|
return '';
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
footerColor: '#fbbf24',
|
||||||
|
footerFont: {{
|
||||||
|
weight: 'bold',
|
||||||
|
size: 11
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
scales: {{
|
||||||
|
yKM: {{
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
display: metric === 'combined',
|
||||||
|
grid: {{ color: 'rgba(255, 255, 255, 0.05)' }},
|
||||||
|
ticks: {{ color: '#94a3b8', font: {{ size: 10 }} }}
|
||||||
|
}},
|
||||||
|
yFuel: {{
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
display: metric === 'combined',
|
||||||
|
grid: {{ drawOnChartArea: false }},
|
||||||
|
ticks: {{ color: '#fb923c', font: {{ size: 10 }} }}
|
||||||
|
}},
|
||||||
|
yCost: {{
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
display: metric === 'combined',
|
||||||
|
grid: {{ drawOnChartArea: false }},
|
||||||
|
ticks: {{ color: '#10b981', font: {{ size: 10 }} }}
|
||||||
|
}},
|
||||||
|
yMain: {{
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
display: metric !== 'combined',
|
||||||
|
grid: {{ color: 'rgba(255, 255, 255, 0.05)' }},
|
||||||
|
ticks: {{ color: '#94a3b8', font: {{ size: 10 }} }}
|
||||||
|
}},
|
||||||
|
x: {{
|
||||||
|
grid: {{ color: 'rgba(255, 255, 255, 0.05)' }},
|
||||||
|
ticks: {{ color: '#94a3b8', font: {{ size: 10 }} }}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
}};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
# Escape double quotes for srcdoc attribute safety
|
||||||
|
escaped_html = inner_html.replace('"', '"')
|
||||||
|
record.chart_html = f'<iframe srcdoc="{escaped_html}" style="width: 100%; height: 320px; border: none; background: transparent; overflow: hidden;"></iframe>'
|
||||||
|
|
||||||
|
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"""
|
||||||
|
<div style="margin-top: 15px; padding: 20px; background: rgba(15, 23, 42, 0.4); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;">
|
||||||
|
<h4 style="color: #fbbf24; margin-top: 0; margin-bottom: 15px; font-weight: 600; font-size: 14px; text-align: center;">🗺️ {dest} Konumu İçin Alternatif 3 Rota Belirlendi</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: rgba(56, 189, 248, 0.05); border: 1px solid rgba(56, 189, 248, 0.15); border-radius: 8px; padding: 15px; text-align: center;">
|
||||||
|
<span style="color: #38bdf8; font-weight: bold; font-size: 13px; display: block; margin-bottom: 5px;">ROTA A (En Kısa)</span>
|
||||||
|
<h2 style="color: white; margin: 5px 0; font-size: 22px; font-weight: 700;">15.2 <span style="font-size: 13px; color: #94a3b8; font-weight: normal;">KM</span></h2>
|
||||||
|
<p style="color: #94a3b8; margin: 0; font-size: 11px;">Tahmini Yakıt: 1.1 L (Otoyol)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.15); border-radius: 8px; padding: 15px; text-align: center;">
|
||||||
|
<span style="color: #10b981; font-weight: bold; font-size: 13px; display: block; margin-bottom: 5px;">ROTA B (Ekonomik)</span>
|
||||||
|
<h2 style="color: white; margin: 5px 0; font-size: 22px; font-weight: 700;">16.5 <span style="font-size: 13px; color: #94a3b8; font-weight: normal;">KM</span></h2>
|
||||||
|
<p style="color: #94a3b8; margin: 0; font-size: 11px;">Tahmini Yakıt: 0.9 L (Şehir İçi)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div style="background: rgba(251, 146, 60, 0.05); border: 1px solid rgba(251, 146, 60, 0.15); border-radius: 8px; padding: 15px; text-align: center;">
|
||||||
|
<span style="color: #fb923c; font-weight: bold; font-size: 13px; display: block; margin-bottom: 5px;">ROTA C (Alternatif)</span>
|
||||||
|
<h2 style="color: white; margin: 5px 0; font-size: 22px; font-weight: 700;">18.0 <span style="font-size: 13px; color: #94a3b8; font-weight: normal;">KM</span></h2>
|
||||||
|
<p style="color: #94a3b8; margin: 0; font-size: 11px;">Tahmini Yakıt: 1.3 L (Akıcı)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
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"🚨 <b>ROTA SAPMA ALARMI (YÖNETİCİ UYARISI):</b> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
101
addons/mymach_fleet_intelligence/models/maintenance_cost.py
Normal file
101
addons/mymach_fleet_intelligence/models/maintenance_cost.py
Normal file
@@ -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
|
||||||
137
addons/mymach_fleet_intelligence/models/periodic_maintenance.py
Normal file
137
addons/mymach_fleet_intelligence/models/periodic_maintenance.py
Normal file
@@ -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"✅ <b>Periyodik Bakım Yapıldı:</b> {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
|
||||||
@@ -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'}
|
||||||
202
addons/mymach_fleet_intelligence/models/security_log.py
Normal file
202
addons/mymach_fleet_intelligence/models/security_log.py
Normal file
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
128
addons/mymach_fleet_intelligence/populate_5_drivers.py
Normal file
128
addons/mymach_fleet_intelligence/populate_5_drivers.py
Normal file
@@ -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! ---")
|
||||||
18
addons/mymach_fleet_intelligence/populate_script_v2.py
Normal file
18
addons/mymach_fleet_intelligence/populate_script_v2.py
Normal file
@@ -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! ---")
|
||||||
9
addons/mymach_fleet_intelligence/regenerate_js_assets.py
Normal file
9
addons/mymach_fleet_intelligence/regenerate_js_assets.py
Normal file
@@ -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!")
|
||||||
@@ -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()
|
||||||
13
addons/mymach_fleet_intelligence/restore_system_and_demo.py
Normal file
13
addons/mymach_fleet_intelligence/restore_system_and_demo.py
Normal file
@@ -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!")
|
||||||
45
addons/mymach_fleet_intelligence/scan_db_broken_chars.py
Normal file
45
addons/mymach_fleet_intelligence/scan_db_broken_chars.py
Normal file
@@ -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.")
|
||||||
87
addons/mymach_fleet_intelligence/scratch_fix.py
Normal file
87
addons/mymach_fleet_intelligence/scratch_fix.py
Normal file
@@ -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!")
|
||||||
51
addons/mymach_fleet_intelligence/scratch_fix_chars.py
Normal file
51
addons/mymach_fleet_intelligence/scratch_fix_chars.py
Normal file
@@ -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!")
|
||||||
60
addons/mymach_fleet_intelligence/scratch_fix_chars_clean.py
Normal file
60
addons/mymach_fleet_intelligence/scratch_fix_chars_clean.py
Normal file
@@ -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!")
|
||||||
@@ -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
|
||||||
|
|
||||||
|
@@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
<!-- Sürücü / Personel Grubu -->
|
||||||
|
<record id="group_mymach_fleet_user" model="res.groups">
|
||||||
|
<field name="name">Sürücü / Personel (MyMach)</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Yönetici (Admin) Grubu -->
|
||||||
|
<record id="group_mymach_fleet_manager" model="res.groups">
|
||||||
|
<field name="name">Yönetici / Admin (MyMach)</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_mymach_fleet_user'))]"/>
|
||||||
|
<field name="user_ids" eval="[(4, ref('base.user_admin')), (4, ref('base.user_root'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- RECORD RULES (Sözleşme) -->
|
||||||
|
<record id="rule_mymach_fleet_contract_user" model="ir.rule">
|
||||||
|
<field name="name">Sürücüler kendi sözleşmesini görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_contract"/>
|
||||||
|
<field name="domain_force">[('driver_id', '=', user.partner_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_mymach_fleet_contract_manager" model="ir.rule">
|
||||||
|
<field name="name">Yöneticiler tüm sözleşmeleri görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_contract"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- RECORD RULES (Rezervasyon) -->
|
||||||
|
<record id="rule_mymach_fleet_reservation_user" model="ir.rule">
|
||||||
|
<field name="name">Sürücüler kendi taleplerini görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_reservation"/>
|
||||||
|
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_mymach_fleet_reservation_manager" model="ir.rule">
|
||||||
|
<field name="name">Yöneticiler tüm talepleri görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_reservation"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- RECORD RULES (Teslimat / Handover) -->
|
||||||
|
<record id="rule_mymach_fleet_handover_user" model="ir.rule">
|
||||||
|
<field name="name">Sürücüler kendi teslimatlarını görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_handover"/>
|
||||||
|
<field name="domain_force">[('driver_id', '=', user.partner_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="rule_mymach_fleet_handover_manager" model="ir.rule">
|
||||||
|
<field name="name">Yöneticiler tüm teslimatları görür</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_handover"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('group_mymach_fleet_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
6
addons/mymach_fleet_intelligence/set_admin_password.py
Normal file
6
addons/mymach_fleet_intelligence/set_admin_password.py
Normal file
@@ -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'")
|
||||||
@@ -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!")
|
||||||
BIN
addons/mymach_fleet_intelligence/static/description/car_hero.png
Normal file
BIN
addons/mymach_fleet_intelligence/static/description/car_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 587 KiB |
317
addons/mymach_fleet_intelligence/static/src/css/aesthetics.css
Normal file
317
addons/mymach_fleet_intelligence/static/src/css/aesthetics.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
399
addons/mymach_fleet_intelligence/static/src/js/dashboard.js
Normal file
399
addons/mymach_fleet_intelligence/static/src/js/dashboard.js
Normal file
@@ -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: '<div style="position: relative;">' +
|
||||||
|
'<div style="background: #2563eb; border: 2px solid white; width: 26px; height: 26px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); box-shadow: 0 4px 14px rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center;">' +
|
||||||
|
'<div style="background: white; width: 9px; height: 9px; border-radius: 50%; transform: rotate(45deg);"></div>' +
|
||||||
|
'</div></div>',
|
||||||
|
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: '<div style="position: relative;">' +
|
||||||
|
'<div style="background: #ef4444; border: 2px solid white; width: 26px; height: 26px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); box-shadow: 0 4px 14px rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center;">' +
|
||||||
|
'<div style="background: white; width: 9px; height: 9px; border-radius: 50%; transform: rotate(45deg);"></div>' +
|
||||||
|
'</div></div>',
|
||||||
|
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: '<div style="background: ' + bg + '; color: white; padding: 6px 12px; border-radius: 16px; font-weight: 800; font-size: 11px; box-shadow: 0 4px 15px rgba(0,0,0,0.7); border: 2px solid white; font-family: sans-serif; white-space: nowrap;">' + text + '</div>',
|
||||||
|
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: '<div style="position: relative; text-align: center;">' +
|
||||||
|
'<div style="background: rgba(15, 23, 42, 0.9); color: #38bdf8; padding: 4px 9px; border-radius: 12px; font-size: 10px; font-weight: 800; border: 1.5px solid #38bdf8; box-shadow: 0 4px 14px rgba(0,0,0,0.8); white-space: nowrap; margin-bottom: 4px; display: inline-block;">' +
|
||||||
|
'🚘 ' + (driverTag || 'Sürücü') + ' • <span style="color: #10b981;">' + (speedText || '55 km/s') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="width: 30px; height: 30px; background: #0284c7; border: 2.5px solid white; border-radius: 50%; box-shadow: 0 0 18px #0284c7; margin: 0 auto; display: flex; align-items: center; justify-content: center;">' +
|
||||||
|
'<span style="font-size: 15px;">🚗</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>',
|
||||||
|
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("<b>Başlangıç Noktası (İş Yeri):</b> " + (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("<b>Ulaşım Noktası (Hedef / Ev / Proje):</b> " + 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 =
|
||||||
|
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">' +
|
||||||
|
'<div>' +
|
||||||
|
'<h3 style="color: white; font-size: 15px; font-weight: 700; margin: 0;">' + (vehicle.driver_tag || '') + ' - ' + vehicle.driver_name + '</h3>' +
|
||||||
|
'<span style="color: #94a3b8; font-size: 12px;">' + vehicle.license_plate + ' (' + vehicle.name + ')</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<span style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; color: ' + badgeText + '; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 12px;">' +
|
||||||
|
'Skor: ' + vehicle.score +
|
||||||
|
'</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; border-radius: 8px; padding: 8px; margin-bottom: 12px; font-size: 11px; font-weight: bold; color: ' + badgeText + '; text-align: center;">' +
|
||||||
|
badgeTitle +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Personel Ev Adresi:</span>' +
|
||||||
|
'<strong style="color: #f1f5f9; font-size: 11px;">' + (vehicle.driver_home_address || 'Kadıköy/Moda') + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Başlangıç Noktası (İş Yeri):</span>' +
|
||||||
|
'<strong style="color: white; font-size: 11px;">' + (vehicle.start_location || 'Genel Merkez Ofis') + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Ulaşım Noktası (Hedef / Proje):</span>' +
|
||||||
|
'<strong style="color: #38bdf8; font-size: 11px;">' + vehicle.destination + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Planlanan / Kat Edilen:</span>' +
|
||||||
|
'<span>' + vehicle.planned_km + ' KM / <strong>' + vehicle.driven_km + ' KM</strong></span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Sapma & Durum:</span>' +
|
||||||
|
'<strong style="color: ' + (vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981')) + ';">' + vehicle.deviation_label + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Sözleşme Poliçesi:</span>' +
|
||||||
|
'<span style="color: ' + (vehicle.has_free_use ? '#38bdf8' : '#f59e0b') + '; font-weight: 600;">' + vehicle.policy_desc + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
||||||
|
'<td>' +
|
||||||
|
'<strong style="color: white;">' + (v.driver_tag || '') + ' - ' + v.driver_name + '</strong><br/>' +
|
||||||
|
'<span style="color: #94a3b8; font-size: 11px;">' + v.license_plate + '</span>' +
|
||||||
|
'</td>' +
|
||||||
|
'<td>' +
|
||||||
|
'<span style="font-size: 11px; font-weight: 600; color: #38bdf8;">' + v.destination + '</span><br/>' +
|
||||||
|
'<span style="font-size: 10px; color: #94a3b8;">' + v.driven_km + ' KM</span>' +
|
||||||
|
'</td>' +
|
||||||
|
'<td>' +
|
||||||
|
'<span style="font-weight: bold; color: ' + (v.score >= 80 ? '#10b981' : (v.score >= 70 ? '#f59e0b' : '#ef4444')) + '">' + v.score + '</span>' +
|
||||||
|
'</td>' +
|
||||||
|
'<td>' +
|
||||||
|
'<span style="' + badgeStyle + ' padding: 3px 6px; border-radius: 10px; font-size: 10px; font-weight: bold; display: inline-block;">' +
|
||||||
|
statusLabel +
|
||||||
|
'</span>' +
|
||||||
|
'</td>';
|
||||||
|
|
||||||
|
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' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 = '<div style="position: relative;"><div class="' + pulseClass + '"></div></div>';
|
||||||
|
|
||||||
|
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 = '<div style="color: #000; font-family: sans-serif; font-size: 12px;">' +
|
||||||
|
'<b>' + log.vehicle_name + ' (' + log.driver_name + ')</b><br/>' +
|
||||||
|
'<span style="font-size: 10px; color: #555;">' + log.datetime + '</span><br/>' +
|
||||||
|
'<span style="font-weight: bold; color: ' + (isAlarm ? '#ef4444' : '#10b981') + ';">' + log.status_desc + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
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);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="mymach_fleet_intelligence.DashboardAction">
|
||||||
|
<div style="width: 100%; height: 100vh; overflow: hidden; margin: 0; padding: 0;">
|
||||||
|
<iframe src="/mymach/fleet/dashboard" style="width: 100%; height: 100%; border: none; display: block;"></iframe>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
21
addons/mymach_fleet_intelligence/sync_and_launch_server.py
Normal file
21
addons/mymach_fleet_intelligence/sync_and_launch_server.py
Normal file
@@ -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!")
|
||||||
12
addons/mymach_fleet_intelligence/task.md
Normal file
12
addons/mymach_fleet_intelligence/task.md
Normal file
@@ -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ı.
|
||||||
33
addons/mymach_fleet_intelligence/test_user_session.py
Normal file
33
addons/mymach_fleet_intelligence/test_user_session.py
Normal file
@@ -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)
|
||||||
22
addons/mymach_fleet_intelligence/test_view_groups.py
Normal file
22
addons/mymach_fleet_intelligence/test_view_groups.py
Normal file
@@ -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)
|
||||||
21
addons/mymach_fleet_intelligence/test_view_groups_safe.py
Normal file
21
addons/mymach_fleet_intelligence/test_view_groups_safe.py
Normal file
@@ -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)
|
||||||
35
addons/mymach_fleet_intelligence/verify_dashboard_table.py
Normal file
35
addons/mymach_fleet_intelligence/verify_dashboard_table.py
Normal file
@@ -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!")
|
||||||
@@ -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!")
|
||||||
94
addons/mymach_fleet_intelligence/views/contract_views.xml
Normal file
94
addons/mymach_fleet_intelligence/views/contract_views.xml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Ağaç (Liste) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_contract_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.contract.list</field>
|
||||||
|
<field name="model">mymach.fleet.contract</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Tahsis Sözleşmeleri">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="personal_use_policy"/>
|
||||||
|
<field name="commute_allowed"/>
|
||||||
|
<field name="driver_score"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_contract_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.contract.form</field>
|
||||||
|
<field name="model">mymach.fleet.contract</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Tahsis Sözleşmesi">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Sözleşme Adı (Örn: 2026 Ahmet Yılmaz)"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Personel Bilgileri">
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="employee_id" invisible="1"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
</group>
|
||||||
|
<group string="Politika ve İzinler">
|
||||||
|
<field name="personal_use_policy"/>
|
||||||
|
<!-- personal_use_policy 'allowed' veya 'limit' ise gösterilecek -->
|
||||||
|
<field name="max_km_limit" invisible="personal_use_policy == 'forbidden'"/>
|
||||||
|
<field name="personal_km_used" readonly="1" invisible="personal_use_policy == 'forbidden'"/>
|
||||||
|
<field name="commute_allowed"/>
|
||||||
|
<field name="driver_score"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Cihaz ve Güvenlik">
|
||||||
|
<group>
|
||||||
|
<field name="personal_device_gps_allowed"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="company_phone_device_id" placeholder="Örn: 550e8400-e29b-41d4-a716-446655440000"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Arama Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_contract_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.contract.search</field>
|
||||||
|
<field name="model">mymach.fleet.contract</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<filter string="Şahsi Kullanım İzinli" name="allowed_personal" domain="[('personal_use_policy', '=', 'allowed')]"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menü Aksiyonu -->
|
||||||
|
<record id="action_mymach_fleet_contract" model="ir.actions.act_window">
|
||||||
|
<field name="name">Tahsis Sözleşmeleri</field>
|
||||||
|
<field name="res_model">mymach.fleet.contract</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_mymach_fleet_contract_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Ana Menü ve Alt Menüler -->
|
||||||
|
<menuitem id="menu_mymach_fleet_intelligence_root"
|
||||||
|
name="MyMach Filo Zekası"
|
||||||
|
action="action_mymach_fleet_home_server"
|
||||||
|
web_icon="fleet,static/description/icon.png"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_mymach_fleet_contract"
|
||||||
|
name="Sözleşmeler"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_contract"
|
||||||
|
sequence="1"/>
|
||||||
|
</odoo>
|
||||||
235
addons/mymach_fleet_intelligence/views/dashboard_template.xml
Normal file
235
addons/mymach_fleet_intelligence/views/dashboard_template.xml
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="dashboard_template" name="MyMach Fleet Dashboard">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>MyMach Otonom Telemetri & Güvenlik Kontrol Merkezi</title>
|
||||||
|
<!-- Premium Fonts & Icons -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
|
||||||
|
|
||||||
|
<!-- Leaflet.js Harita Kütüphanesi -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
|
<!-- Google Maps API (Kullanıcı parametre girdiyse yüklenir) -->
|
||||||
|
<script t-if="google_maps_api_key" t-att-src="'https://maps.googleapis.com/maps/api/js?key=' + google_maps_api_key"/>
|
||||||
|
|
||||||
|
<!-- Chart.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
background: #0d0f12;
|
||||||
|
color: #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
/* Sol Telemetri Paneli */
|
||||||
|
.sidebar {
|
||||||
|
width: 38%;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(15, 22, 28, 0.9);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 10px 0 30px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
/* Sağ Canlı Harita */
|
||||||
|
.map-container {
|
||||||
|
width: 62%;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* Glassmorphism Kartları */
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(45deg, #00f2fe, #4facfe);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
/* Tablo Stilleri */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
/* Durum Rozetleri */
|
||||||
|
.badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.badge-success { background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
|
||||||
|
.badge-warning { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.2); }
|
||||||
|
.badge-danger { background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
|
||||||
|
|
||||||
|
/* Yakıt Barı */
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #10b981, #34d399);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.progress-bar.low {
|
||||||
|
background: linear-gradient(90deg, #ef4444, #f87171);
|
||||||
|
}
|
||||||
|
/* Neon Efektli Pulse Animasyonu */
|
||||||
|
.pulse {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
position: absolute;
|
||||||
|
left: -6px;
|
||||||
|
top: -6px;
|
||||||
|
animation: pulsate 1.5s ease-out infinite;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.pulse-red {
|
||||||
|
box-shadow: 0 0 12px 6px rgba(239, 68, 68, 0.8);
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
.pulse-green {
|
||||||
|
box-shadow: 0 0 12px 6px rgba(16, 185, 129, 0.8);
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
@keyframes pulsate {
|
||||||
|
0% { transform: scale(0.1, 0.1); opacity: 0.0; }
|
||||||
|
50% { opacity: 1.0; }
|
||||||
|
100% { transform: scale(1.2, 1.2); opacity: 0.0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sol Arayüz Paneli -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="card">
|
||||||
|
<div class="header-title">
|
||||||
|
<i class="fa-solid fa-users-viewfinder"></i> SÜRÜCÜ LİSTESİ
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 13px; color: #94a3b8; margin-top: 5px;">
|
||||||
|
Zero-Trust Canlı Rota Takip ve Otonom Sapma Denetim Paneli
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Canlı Sürücü ve Araç Listesi Tablosu -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="subtitle">Sürücü Seçimi & Rota Denetimi</div>
|
||||||
|
<div style="max-height: 260px; overflow-y: auto;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sürücü / Araç</th>
|
||||||
|
<th>Hedef</th>
|
||||||
|
<th>Skor</th>
|
||||||
|
<th>Durum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="vehicleTableBody">
|
||||||
|
<!-- JS ile doldurulacak -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Küçük Korelasyon Grafiği -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="subtitle">Maliyet ve Tüketim Korelasyonu</div>
|
||||||
|
<canvas id="miniChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sağ Canlı Harita Paneli -->
|
||||||
|
<div class="map-container">
|
||||||
|
<div id="map"></div>
|
||||||
|
<!-- Yüzen Personel Rota & Sapma Bilgi Kartı -->
|
||||||
|
<div id="driver_info_overlay" style="position: absolute; top: 20px; left: 20px; z-index: 1000; width: 340px; background: rgba(15, 23, 42, 0.9); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px; padding: 18px; box-shadow: 0 10px 30px rgba(0,0,0,0.6); display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verileri HTML Data Elementinde Depola (JS Kaçış Çakışmalarını Önler) -->
|
||||||
|
<div id="mymach_telemetry_data"
|
||||||
|
t-att-data-vehicles="vehicles_json"
|
||||||
|
t-att-data-logs="logs_json"
|
||||||
|
t-att-data-gmaps-key="google_maps_api_key"
|
||||||
|
style="display: none;"/>
|
||||||
|
|
||||||
|
<!-- Harita ve Tablo İşleme Kodları (Statik Dosyadan Yüklenir - QWeb Çakışmalarını Önler) -->
|
||||||
|
<script type="text/javascript" src="/mymach_fleet_intelligence/static/src/js/dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- OWL İstemci Aksiyonu (Client Action) -->
|
||||||
|
<record id="action_mymach_fleet_dashboard_client" model="ir.actions.client">
|
||||||
|
<field name="name">Canlı Harita</field>
|
||||||
|
<field name="tag">mymach_fleet_dashboard_client_action</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Ana Menü Altında Canlı Harita Menüsü -->
|
||||||
|
<menuitem id="menu_mymach_fleet_dashboard"
|
||||||
|
name="Canlı Harita"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_dashboard_client"
|
||||||
|
sequence="4"
|
||||||
|
groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
</odoo>
|
||||||
87
addons/mymach_fleet_intelligence/views/fleet_fine_views.xml
Normal file
87
addons/mymach_fleet_intelligence/views/fleet_fine_views.xml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Gider/Ceza List (Tree) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_fine_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.fine.list</field>
|
||||||
|
<field name="model">mymach.fleet.fine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Giderler ve Cezalar">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="charge_type"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="is_personal_violation" widget="boolean_toggle"/>
|
||||||
|
<field name="state" widget="badge" decoration-success="state == 'deducted'" decoration-info="state == 'confirmed'" decoration-muted="state == 'draft'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Gider/Ceza Form Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_fine_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.fine.form</field>
|
||||||
|
<field name="model">mymach.fleet.fine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Gider ve Ceza Kaydı">
|
||||||
|
<header>
|
||||||
|
<button name="action_confirm" string="Onayla" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_deduct_to_payroll" string="Bordroya/Cariye Yansıt" type="object" class="btn-danger" invisible="state != 'confirmed' or not is_personal_violation"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,deducted"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Geçiş/Ceza Fiş No (Örn: HGS-981298)"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Detaylar">
|
||||||
|
<field name="charge_type"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
</group>
|
||||||
|
<group string="Finansal Bilgiler">
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="is_personal_violation"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Gider/Ceza Arama Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_fine_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.fine.search</field>
|
||||||
|
<field name="model">mymach.fleet.fine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<filter string="Şahsi İhlaller" name="personal_violation" domain="[('is_personal_violation', '=', True)]"/>
|
||||||
|
<filter string="Gider Türü" name="group_by_type" context="{'group_by': 'charge_type'}"/>
|
||||||
|
<filter string="Durum" name="group_by_state" context="{'group_by': 'state'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Aksiyon -->
|
||||||
|
<record id="action_mymach_fleet_fine" model="ir.actions.act_window">
|
||||||
|
<field name="name">Cezalar ve HGS Geçişleri</field>
|
||||||
|
<field name="res_model">mymach.fleet.fine</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_mymach_fleet_fine_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menü Elemanı -->
|
||||||
|
<menuitem id="menu_mymach_fleet_fine"
|
||||||
|
name="Giderler & Cezalar"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_fine"
|
||||||
|
sequence="3"/>
|
||||||
|
</odoo>
|
||||||
175
addons/mymach_fleet_intelligence/views/fleet_handover_views.xml
Normal file
175
addons/mymach_fleet_intelligence/views/fleet_handover_views.xml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Teslim/İade List Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_handover_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.handover.list</field>
|
||||||
|
<field name="model">mymach.fleet.handover</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Teslim Alım ve İade Kayıtları">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="take_datetime"/>
|
||||||
|
<field name="return_datetime"/>
|
||||||
|
<field name="odometer_diff" sum="Toplam Yol"/>
|
||||||
|
<field name="state" widget="badge" decoration-success="state == 'compared'" decoration-info="state in ('taken', 'returned')" decoration-muted="state == 'draft'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Teslim/İade Form Görünümü (Hem Personel Girişi hem Yönetici Düzeni İçerir) -->
|
||||||
|
<record id="view_mymach_fleet_handover_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.handover.form</field>
|
||||||
|
<field name="model">mymach.fleet.handover</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Araç Teslim / İade">
|
||||||
|
<header>
|
||||||
|
<!-- Personel Aksiyonları -->
|
||||||
|
<button name="action_take" string="Aracı Teslim Al" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_return" string="Aracı İade Et" type="object" class="oe_highlight" invisible="state != 'taken'"/>
|
||||||
|
|
||||||
|
<!-- Yönetici Aksiyonu (Sadece group_mymach_fleet_manager görebilir) -->
|
||||||
|
<button name="action_compare" string="Uyuşmazlığı Denetle ve Onayla" type="object" class="btn-warning" invisible="state != 'returned'" groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,taken,returned,compared"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Genel Bilgiler">
|
||||||
|
<field name="vehicle_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="driver_id" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Özet İstatistikler" invisible="state in ('draft', 'taken')">
|
||||||
|
<field name="odometer_diff" readonly="1"/>
|
||||||
|
<field name="fuel_diff" readonly="1" decoration-danger="fuel_diff < 0"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<!-- Adım 1: Teslim Alım (Giriş) Sayfası -->
|
||||||
|
<page string="1. Teslim Alış (Giriş)" name="take_info">
|
||||||
|
<group>
|
||||||
|
<group string="Metrikler">
|
||||||
|
<field name="take_datetime" readonly="state != 'draft'"/>
|
||||||
|
<field name="start_odometer" placeholder="Kilometre Saati Değeri" readonly="state != 'draft'"/>
|
||||||
|
<field name="start_fuel_level" placeholder="Örn: 100 (Full)" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Görsel Kanıt">
|
||||||
|
<field name="start_photo" widget="image" class="oe_avatar" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- Adım 2: Teslim Etme (İade) Sayfası -->
|
||||||
|
<page string="2. İade (Çıkış)" name="return_info" invisible="state == 'draft'">
|
||||||
|
<group>
|
||||||
|
<group string="Metrikler">
|
||||||
|
<field name="return_datetime" readonly="state != 'taken'"/>
|
||||||
|
<field name="end_odometer" placeholder="Kilometre Saati Değeri" readonly="state != 'taken'"/>
|
||||||
|
<field name="end_fuel_level" placeholder="Örn: 80 (%80 Depo)" readonly="state != 'taken'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Görsel Kanıt">
|
||||||
|
<field name="end_photo" widget="image" class="oe_avatar" readonly="state != 'taken'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- Adım 3: Yönetici Karşılaştırma ve Denetleme Sayfası -->
|
||||||
|
<page string="3. Yönetici Denetim Paneli" name="manager_check" groups="mymach_fleet_intelligence.group_mymach_fleet_manager" invisible="state in ('draft', 'taken')">
|
||||||
|
<group>
|
||||||
|
<group string="Karşılaştırma Analizi">
|
||||||
|
<div class="o_horizontal_separator" style="font-weight: bold; margin-bottom: 10px;">Teslim Öncesi vs Sonrası Durumu:</div>
|
||||||
|
<table class="table table-bordered table-condensed" style="width: 100%; border: 1px solid #ccc; text-align: center;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f2f2f2;">
|
||||||
|
<th>Metrik</th>
|
||||||
|
<th>Alış (Önce)</th>
|
||||||
|
<th>İade (Sonra)</th>
|
||||||
|
<th>Fark / Değişim</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><b>Kilometre (KM)</b></td>
|
||||||
|
<td><field name="start_odometer" readonly="1"/></td>
|
||||||
|
<td><field name="end_odometer" readonly="1"/></td>
|
||||||
|
<td style="color: blue; font-weight: bold;">+<field name="odometer_diff" readonly="1"/> KM</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Depo Oranı</b></td>
|
||||||
|
<td>%<field name="start_fuel_level" readonly="1"/></td>
|
||||||
|
<td>%<field name="end_fuel_level" readonly="1"/></td>
|
||||||
|
<td>
|
||||||
|
<span invisible="fuel_diff >= 0" style="color: red; font-weight: bold;">%<field name="fuel_diff" readonly="1"/></span>
|
||||||
|
<span invisible="fuel_diff < 0" style="color: green; font-weight: bold;">+%<field name="fuel_diff" readonly="1"/></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</group>
|
||||||
|
<group string="Görsel Kıyaslama (Hasar Kontrol)">
|
||||||
|
<div style="display: flex; gap: 20px; justify-content: center; align-items: center;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-weight: 600; margin-bottom: 5px;">Alış Fotoğrafı</div>
|
||||||
|
<field name="start_photo" widget="image" style="max-width: 180px; max-height: 180px;" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 24px; color: #777;">➔</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-weight: 600; margin-bottom: 5px;">İade Fotoğrafı</div>
|
||||||
|
<field name="end_photo" widget="image" style="max-width: 180px; max-height: 180px;" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Karar ve Notlar">
|
||||||
|
<field name="is_approved" readonly="state == 'compared'"/>
|
||||||
|
<field name="manager_note" placeholder="Denetim esnasında tespit edilen farklar/çizikler veya yakıt açıkları..." readonly="state == 'compared'"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Teslim/İade Arama Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_handover_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.handover.search</field>
|
||||||
|
<field name="model">mymach.fleet.handover</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<filter string="Kullanımdakiler" name="taken_state" domain="[('state', '=', 'taken')]"/>
|
||||||
|
<filter string="Denetlenmeyi Bekleyenler" name="returned_state" domain="[('state', '=', 'returned')]"/>
|
||||||
|
<filter string="Uyuşmazlık (Yakıt Kaybı)" name="fuel_loss" domain="[('fuel_diff', '<', 0)]"/>
|
||||||
|
<filter string="Uyuşmazlık (Onaylanmayanlar)" name="not_approved" domain="[('state', '=', 'compared'), ('is_approved', '=', False)]"/>
|
||||||
|
<filter string="Durum" name="group_by_state" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Sürücü" name="group_by_driver" context="{'group_by': 'driver_id'}"/>
|
||||||
|
<filter string="Araç" name="group_by_vehicle" context="{'group_by': 'vehicle_id'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Aksiyon -->
|
||||||
|
<record id="action_mymach_fleet_handover" model="ir.actions.act_window">
|
||||||
|
<field name="name">Teslim Alım & İade Otomasyonu</field>
|
||||||
|
<field name="res_model">mymach.fleet.handover</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_mymach_fleet_handover_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menü Elemanı -->
|
||||||
|
<menuitem id="menu_mymach_fleet_handover"
|
||||||
|
name="Teslim & İade"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_handover"
|
||||||
|
sequence="2"/>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Sequence Tanımı -->
|
||||||
|
<record id="seq_mymach_fleet_reservation" model="ir.sequence">
|
||||||
|
<field name="name">Araç Rezervasyon Sequence</field>
|
||||||
|
<field name="code">mymach.fleet.reservation</field>
|
||||||
|
<field name="prefix">RNDV-%(year)s-</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<!-- Liste (Tree) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_reservation_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.reservation.list</field>
|
||||||
|
<field name="model">mymach.fleet.reservation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Araç Randevuları" decoration-info="state == 'submitted'" decoration-success="state == 'approved'" decoration-muted="state in ('done', 'rejected')">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="start_datetime"/>
|
||||||
|
<field name="end_datetime"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'submitted'" decoration-success="state == 'approved'" decoration-danger="state == 'rejected'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_reservation_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.reservation.form</field>
|
||||||
|
<field name="model">mymach.fleet.reservation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Araç Talebi">
|
||||||
|
<header>
|
||||||
|
<button name="action_submit" type="object" string="Onaya Gönder" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_approve" type="object" string="Talebi Onayla (Admin)" class="btn-success" invisible="state != 'submitted'" groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
<button name="action_reject" type="object" string="Talebi Reddet" class="btn-danger" invisible="state != 'submitted'" groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
<button name="action_done" type="object" string="Teslim Edildi / Tamamlandı" class="btn-primary" invisible="state != 'approved'" groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,submitted,approved,done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="vehicle_id" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="start_datetime" readonly="state != 'draft'"/>
|
||||||
|
<field name="end_datetime" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Kullanım Gerekçesi">
|
||||||
|
<field name="purpose" placeholder="Neden bu araca ihtiyacınız var?" readonly="state != 'draft'"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<!-- Odoo Chatter (Aktivite ve Mesajlar) -->
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Takvim (Calendar) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_reservation_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.reservation.calendar</field>
|
||||||
|
<field name="model">mymach.fleet.reservation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<calendar string="Araç Takvimi" date_start="start_datetime" date_stop="end_datetime" color="vehicle_id" event_open_popup="true" mode="week">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</calendar>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Aksiyon -->
|
||||||
|
<record id="action_mymach_fleet_reservation" model="ir.actions.act_window">
|
||||||
|
<field name="name">Araç Randevuları</field>
|
||||||
|
<field name="res_model">mymach.fleet.reservation</field>
|
||||||
|
<field name="view_mode">calendar,list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk araç talebinizi oluşturun!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Araçların doluluk oranlarını takvim üzerinden takip edebilirsiniz.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menü -->
|
||||||
|
<menuitem id="menu_mymach_fleet_reservation"
|
||||||
|
name="Araç Talep & Randevu"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_reservation"
|
||||||
|
sequence="2"/>
|
||||||
|
</odoo>
|
||||||
160
addons/mymach_fleet_intelligence/views/fleet_vehicle_views.xml
Normal file
160
addons/mymach_fleet_intelligence/views/fleet_vehicle_views.xml
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Araç Form Görünümüne Finansal Alanların Eklenmesi -->
|
||||||
|
<record id="view_mymach_fleet_vehicle_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.vehicle.form.inherit</field>
|
||||||
|
<field name="model">fleet.vehicle</field>
|
||||||
|
<field name="inherit_id" ref="fleet.fleet_vehicle_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Smart Button for Maintenance Warnings -->
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_periodic_maintenance)d"
|
||||||
|
type="action"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-wrench"
|
||||||
|
context="{'search_default_vehicle_id': id}">
|
||||||
|
<div class="o_stat_info">
|
||||||
|
<span class="o_stat_text">KM/Yıllık</span>
|
||||||
|
<span class="o_stat_text">Bakımlar</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Uyarı Kurdelesi (Ribbon) -->
|
||||||
|
<xpath expr="//sheet" position="inside">
|
||||||
|
<field name="is_maintenance_critical" invisible="1"/>
|
||||||
|
<widget name="web_ribbon" title="SATMAK DAHA KARLI!" bg_color="bg-danger" invisible="not is_maintenance_critical"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Finans & Gider Analizi (KM / Aylık / Yıllık)">
|
||||||
|
<group string="Para Birimi & Listbox Gösterim Ayarları">
|
||||||
|
<group>
|
||||||
|
<field name="cost_curr1_id" options="{'no_create': True, 'no_open': True}"/>
|
||||||
|
<field name="cost_curr2_id" options="{'no_create': True, 'no_open': True}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="cost_display_mode" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Seçilen Gösterim Moduna Göre Hesaplanan Maliyetler">
|
||||||
|
<group>
|
||||||
|
<field name="cost_per_km_avg_display" widget="monetary" options="{'currency_field': 'active_display_currency_id'}" string="BİR KM BAŞINA MALİYET" decoration-bf="1" style="font-weight: bold; color: #10b981; font-size: 15px;"/>
|
||||||
|
<field name="annual_cost_avg_display" widget="monetary" options="{'currency_field': 'active_display_currency_id'}" string="YILLIK TOPLAM MALİYET" decoration-bf="1" style="font-weight: bold; color: #38bdf8; font-size: 15px;"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="monthly_cost_avg_display" widget="monetary" options="{'currency_field': 'active_display_currency_id'}" string="AYLIK ORTALAMA MALİYET" decoration-bf="1" style="font-weight: bold; color: #f59e0b; font-size: 15px;"/>
|
||||||
|
<field name="total_expense_amount" widget="monetary" options="{'currency_field': 'try_currency_id'}" string="Toplam Birikmiş Gider (TL)"/>
|
||||||
|
<field name="try_currency_id" invisible="1"/>
|
||||||
|
<field name="active_display_currency_id" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Çoklu Para Birimi Karşılaştırması & 3 Dövizli Ortalama Tahminî Değer (Odoo Canlı Kur)">
|
||||||
|
<group string="BİR KM BAŞINA MALİYET">
|
||||||
|
<field name="cost_per_km_try" decoration-bf="1" style="color: #059669;"/>
|
||||||
|
<field name="cost_per_km_c1" />
|
||||||
|
<field name="cost_per_km_c2" />
|
||||||
|
<field name="cost_per_km_avg" decoration-bf="1" style="color: #047857; background-color: #ecfdf5;"/>
|
||||||
|
</group>
|
||||||
|
<group string="YILLIK TOPLAM MALİYET">
|
||||||
|
<field name="annual_cost_try" decoration-bf="1" style="color: #0284c7;"/>
|
||||||
|
<field name="annual_cost_c1" />
|
||||||
|
<field name="annual_cost_c2" />
|
||||||
|
<field name="annual_cost_avg" decoration-bf="1" style="color: #0369a1; background-color: #f0f9ff;"/>
|
||||||
|
</group>
|
||||||
|
<group string="AYLIK ORTALAMA MALİYET">
|
||||||
|
<field name="monthly_cost_try" decoration-bf="1" style="color: #d97706;"/>
|
||||||
|
<field name="monthly_cost_c1" />
|
||||||
|
<field name="monthly_cost_c2" />
|
||||||
|
<field name="monthly_cost_avg" decoration-bf="1" style="color: #b45309; background-color: #fffbeb;"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<separator string="Araç Gider Kalemleri Matrisi (Odoo Düzene Göre Listelenmiştir)"/>
|
||||||
|
<field name="maintenance_cost_ids">
|
||||||
|
<list string="Gider Kalemleri" editable="bottom">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="amount" widget="monetary"/>
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Finansal, Amortisman ve Teknik Motor">
|
||||||
|
<group>
|
||||||
|
<group string="Amortisman Bedelleri (3 Para Birimi & Ortalama)">
|
||||||
|
<field name="depreciation_cost_try" string="Amortisman (TRY)"/>
|
||||||
|
<field name="depreciation_cost_usd" string="Amortisman (USD)"/>
|
||||||
|
<field name="depreciation_cost_eur" string="Amortisman (EUR)"/>
|
||||||
|
<field name="depreciation_cost_avg" string="Ortalama Amortisman (TRY)" decoration-bf="1" style="font-weight: bold; color: #10b981;"/>
|
||||||
|
</group>
|
||||||
|
<group string="Son Bakım Durumu">
|
||||||
|
<field name="last_maintenance_odometer" readonly="1"/>
|
||||||
|
<field name="last_maintenance_date" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Akaryakıt & Yapay Zeka (Faz 8)">
|
||||||
|
<field name="tank_capacity"/>
|
||||||
|
<field name="factory_avg_consumption"/>
|
||||||
|
<field name="current_fuel_level" widget="progressbar"/>
|
||||||
|
<field name="needs_predictive_maintenance" widget="boolean_toggle" class="mymach_predictive_alert" invisible="needs_predictive_maintenance == False"/>
|
||||||
|
</group>
|
||||||
|
<group string="Döviz & Amortisman Endeksleme">
|
||||||
|
<field name="purchase_value"/>
|
||||||
|
<field name="depreciation_years"/>
|
||||||
|
<field name="annual_fixed_cost"/>
|
||||||
|
<field name="index_currency_id"/>
|
||||||
|
<field name="indexed_purchase_value" widget="monetary"/>
|
||||||
|
<field name="moving_average_maintenance_cost" widget="monetary"/>
|
||||||
|
<field name="km_cost"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Teknik ve Katalog Detayları (Birleşik Veriler)">
|
||||||
|
<group>
|
||||||
|
<field name="car_value" string="Katalog Fiyatı (Vergiler Dahil)"/>
|
||||||
|
<field name="residual_value" string="Kalan Değer"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="horsepower" string="Beygir Gücü"/>
|
||||||
|
<field name="horsepower_tax" string="Beygir Gücü Vergilendirme"/>
|
||||||
|
<field name="power" string="Güç (kW)"/>
|
||||||
|
<field name="seats" string="Oturma Kapasitesi"/>
|
||||||
|
<field name="doors" string="Kapı Sayısı"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Yakıt/Servis Kayıtlarına Yakıt Giriş Metodunun Eklenmesi -->
|
||||||
|
<record id="view_mymach_fleet_vehicle_log_services_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.vehicle.log.services.form.inherit</field>
|
||||||
|
<field name="model">fleet.vehicle.log.services</field>
|
||||||
|
<field name="inherit_id" ref="fleet.fleet_vehicle_log_services_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='amount']" position="after">
|
||||||
|
<!-- Alt Görev 2.3: Seçim Listesi Yakıt Fişi Giriş Modu -->
|
||||||
|
<field name="fuel_entry_method" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Araç Yönetimi Aksiyonu ve Menüsü -->
|
||||||
|
<record id="action_mymach_fleet_vehicle" model="ir.actions.act_window">
|
||||||
|
<field name="name">Araç Kartları</field>
|
||||||
|
<field name="res_model">fleet.vehicle</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Yönetilecek ilk aracınızı oluşturun!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mymach_fleet_vehicle"
|
||||||
|
name="Araçlar"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_vehicle"
|
||||||
|
sequence="1"/>
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
289
addons/mymach_fleet_intelligence/views/home_views.xml
Normal file
289
addons/mymach_fleet_intelligence/views/home_views.xml
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Yapay Zeka Kestirimci Bakım Filtreli Araç Listesi Aksiyonu -->
|
||||||
|
<record id="action_fleet_vehicle_predictive_ai" model="ir.actions.act_window">
|
||||||
|
<field name="name">Yapay Zeka Servis Bekleyen Araçlar</field>
|
||||||
|
<field name="res_model">fleet.vehicle</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('needs_predictive_maintenance', '=', True)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Kritik Maliyet Filtreli Araç Listesi Aksiyonu -->
|
||||||
|
<record id="action_fleet_vehicle_critical_cost" model="ir.actions.act_window">
|
||||||
|
<field name="name">Kritik Maliyet Alarmı Veren Araçlar</field>
|
||||||
|
<field name="res_model">fleet.vehicle</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('is_maintenance_critical', '=', True)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Sunucu Aksiyonu: Sadece Canlı Haritayı Gösteren Görünüm İçin -->
|
||||||
|
<record id="action_mymach_fleet_home_chart_server" model="ir.actions.server">
|
||||||
|
<field name="name">Canlı Harita Analizi</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_home"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="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},
|
||||||
|
}
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_mymach_fleet_home_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.home.form</field>
|
||||||
|
<field name="model">mymach.fleet.home</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="MyMach Filo Zekası" class="mymach_zara_form" create="false" delete="false">
|
||||||
|
<header>
|
||||||
|
<button name="action_save_settings" string="DEĞİŞİKLİKLERİ KAYDET" type="object" class="btn-primary" style="background-color: #10b981 !important; border-color: #10b981 !important; font-weight: bold; padding: 8px 16px; border-radius: 6px;"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="mymach_zara_container">
|
||||||
|
<!-- Dark Blurred Background Overlay -->
|
||||||
|
<div class="mymach_zara_overlay"></div>
|
||||||
|
|
||||||
|
<!-- Floating Content Wrapper -->
|
||||||
|
<div class="mymach_zara_content">
|
||||||
|
<!-- Zara-Style Minimalist Editorial Header -->
|
||||||
|
<div style="text-align: center; margin-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 25px;">
|
||||||
|
<h1 class="mymach_zara_title" style="color: #ef4444 !important; text-shadow: 0 0 15px rgba(239, 68, 68, 0.5); font-weight: 800;">MYMACH FLEET INTELLIGENCE</h1>
|
||||||
|
<field name="display_title" readonly="1" class="mymach_zara_subtitle" style="border: none; background: transparent; color: #cbd5e1 !important; text-align: center; display: block; width: 100%; pointer-events: none; text-shadow: 0 1px 5px rgba(0,0,0,0.5);"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grup 1: Temel Operasyonlar -->
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<h3 class="mymach_zara_group_title" style="color: #38bdf8; border-color: rgba(56, 189, 248, 0.25);">Temel Operasyonlar</h3>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(fleet.fleet_vehicle_action)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-car mb-2"></i><br/> Akıllı Araç Yönetimi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_reservation)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-calendar-check-o mb-2"></i><br/> Araç Talebi & Randevu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_handover)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-key mb-2"></i><br/> Araç Teslim & İade
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_periodic_maintenance)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-wrench mb-2"></i><br/> Periyodik Bakım Takibi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(hr.open_view_employee_list_my)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-users mb-2"></i><br/> Personel Kartları
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grup 2: Güvenlik & Rota Motoru -->
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<h3 class="mymach_zara_group_title" style="color: #f43f5e; border-color: rgba(244, 63, 94, 0.25);">Güvenlik & Rota Uyum Motoru</h3>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_contract)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-file-text-o mb-2"></i><br/> Tahsis Sözleşmeleri
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_fine)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-exclamation-triangle mb-2"></i><br/> Giderler & Cezalar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_security_log)d" type="action" class="btn mymach_zara_btn" style="width: 100%;" groups="mymach_fleet_intelligence.group_mymach_fleet_manager">
|
||||||
|
<i class="fa fa-shield mb-2"></i><br/> Güvenlik İhlal Logları
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_unresolved_exceptions)d" type="action" class="btn mymach_zara_btn mymach_zara_btn_warning" style="width: 100%;" groups="mymach_fleet_intelligence.group_mymach_fleet_manager">
|
||||||
|
<i class="fa fa-gavel mb-2"></i><br/> Çözüm Bekleyen Rotalar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grup 3: Analitik Paneller & Gider Yönetimi -->
|
||||||
|
<div style="margin-top: 15px;" groups="mymach_fleet_intelligence.group_mymach_fleet_manager">
|
||||||
|
<h3 class="mymach_zara_group_title" style="color: #10b981; border-color: rgba(16, 185, 129, 0.25);">Analitik Paneller & Gider Yönetimi</h3>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_dashboard_client)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-map-o mb-2"></i><br/> Canlı Harita
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_maintenance_cost)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-money mb-2"></i><br/> Gider Matrisi (Endeks)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_home_chart_server)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
|
||||||
|
<i class="fa fa-line-chart mb-2"></i><br/> Kilometre & Yakıt Grafiği
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Finans Gider Matrisi & Araç Maliyet Analitiği -->
|
||||||
|
<div style="margin-top: 20px; background: rgba(15, 23, 42, 0.4); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px;" groups="mymach_fleet_intelligence.group_mymach_fleet_manager">
|
||||||
|
<h3 style="color: #10b981; font-size: 15px; font-weight: 600; margin-top: 0; margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
💰 FİNANS GİDER MATRİSİ VE ARAÇ MALİYET ANALİTİĞİ
|
||||||
|
</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="cost_matrix_vehicle_id" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Maliyeti Hesaplanacak Aracı Seçin:</label>
|
||||||
|
<field name="cost_matrix_vehicle_id" options="{'no_create': True, 'no_open': True}" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3" style="display: flex; align-items: flex-end;">
|
||||||
|
<button name="action_save_settings" string="Seçilen Araç İçin Hesapla ve Güncelle" type="object" class="btn btn-success" style="width: 100%; background: #10b981; color: white; border: none; font-weight: bold; padding: 8px;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<field name="cost_matrix_cards_html" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Periyodik Bakım Canlı İzleme Tablosu -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<field name="upcoming_maintenance_html" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Sunucu Aksiyonu: Her girişte yeni bir boş kayıt yaratıp o sayfayı salt okunur açar -->
|
||||||
|
<record id="action_mymach_fleet_home_server" model="ir.actions.server">
|
||||||
|
<field name="name">Ana Pano</field>
|
||||||
|
<field name="model_id" ref="model_mymach_fleet_home"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="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},
|
||||||
|
}
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Odoo'nun Cache sorunlarını aşmak için Root Menu yerine Alt Menü (Sequence 0) ekliyoruz -->
|
||||||
|
<menuitem id="menu_mymach_fleet_home"
|
||||||
|
name="Ana Pano"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_home_server"
|
||||||
|
sequence="0"/>
|
||||||
|
|
||||||
|
<!-- SADECE GRAFİK İÇEREN FORM GÖRÜNÜMÜ -->
|
||||||
|
<record id="view_mymach_fleet_home_chart_only_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.home.chart.only.form</field>
|
||||||
|
<field name="model">mymach.fleet.home</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Kilometre & Yakıt Analizi" class="mymach_zara_form" create="false" delete="false">
|
||||||
|
<header>
|
||||||
|
<button name="action_save_settings" string="DEĞİŞİKLİKLERİ KAYDET" type="object" class="btn-primary" style="background-color: #10b981 !important; border-color: #10b981 !important; font-weight: bold; padding: 8px 16px; border-radius: 6px;"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="mymach_zara_container">
|
||||||
|
<!-- Dark Blurred Background Overlay -->
|
||||||
|
<div class="mymach_zara_overlay"></div>
|
||||||
|
|
||||||
|
<!-- Floating Content Wrapper -->
|
||||||
|
<div class="mymach_zara_content">
|
||||||
|
<!-- Zara-Style Minimalist Editorial Header -->
|
||||||
|
<div style="text-align: center; margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 20px;">
|
||||||
|
<h1 class="mymach_zara_title">KİLOMETRE & YAKIT ANALİZİ</h1>
|
||||||
|
<p class="mymach_zara_subtitle" style="color: #cbd5e1 !important; text-align: center; display: block; width: 100%;">FİLO KİLOMETRE GELİŞİMİ VE YAKIT TÜKETİM ANALİTİKLERİ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- İnteraktif Yakıt & KM Grafiği ve Seçiciler -->
|
||||||
|
<div style="background: rgba(15, 23, 42, 0.4); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="selected_vehicle_id" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Analiz Edilecek Araç:</label>
|
||||||
|
<field name="selected_vehicle_id" options="{'no_create': True, 'no_open': True}" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="time_range" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Zaman Aralığı:</label>
|
||||||
|
<field name="time_range" widget="radio" options="{'horizontal': true}" style="color: white !important;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="chart_metric" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Grafik Gösterim Modu:</label>
|
||||||
|
<field name="chart_metric" widget="radio" options="{'horizontal': true}" style="color: white !important;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<field name="chart_html" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Otonom Rota Planlama & Sapma Denetimi Bölümü -->
|
||||||
|
<div style="margin-top: 20px; background: rgba(15, 23, 42, 0.4); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px;">
|
||||||
|
<h3 style="color: #38bdf8; font-size: 15px; font-weight: 600; margin-top: 0; margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
🗺️ OTONOM ROTA PLANLAMA VE SAPMA DENETİMİ (1 KM TOLERANS)
|
||||||
|
</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="route_destination" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Hedef Konum:</label>
|
||||||
|
<field name="route_destination" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="route_selected" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Planlanan Rota:</label>
|
||||||
|
<field name="route_selected" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="route_simulated_distance" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Simüle Yolculuk (KM):</label>
|
||||||
|
<field name="route_simulated_distance" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3" style="display: flex; align-items: flex-end; gap: 10px;">
|
||||||
|
<button name="action_calculate_routes" string="Rotaları Hesapla" type="object" class="btn btn-secondary" style="width: 100%; background: #1e293b; color: white; border: 1px solid rgba(255,255,255,0.1); font-weight: bold;"/>
|
||||||
|
<button name="action_simulate_route_drive" string="Sürüşü Simüle Et" type="object" class="btn btn-primary" style="width: 100%; background: #3b82f6; color: white; border: none; font-weight: bold;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<field name="route_results_html" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Grafik İçin Sol Menü Kısayolu -->
|
||||||
|
<menuitem id="menu_mymach_fleet_telemetry_chart"
|
||||||
|
name="Canlı Harita Analizi"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_home_chart_server"
|
||||||
|
sequence="5"/>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Gider Matrisi Ağaç Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_maintenance_cost_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.maintenance.cost.list</field>
|
||||||
|
<field name="model">mymach.fleet.maintenance.cost</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Gider Matrisi">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Gider Matrisi Form Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_maintenance_cost_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.maintenance.cost.form</field>
|
||||||
|
<field name="model">mymach.fleet.maintenance.cost</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Gider Kaydı">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Gider Açıklaması (Örn: Motor Yağı Değişimi)"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="product_id" context="{'default_purchase_ok': True, 'default_type': 'service'}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="amount" widget="monetary"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Gider Matrisi Arama ve Gruplama Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_maintenance_cost_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.maintenance.cost.search</field>
|
||||||
|
<field name="model">mymach.fleet.maintenance.cost</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Gider Matrisi Arama">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<filter string="Araç Bazında Grupla" name="group_by_vehicle" context="{'group_by':'vehicle_id'}"/>
|
||||||
|
<filter string="Gider Kalemi Bazında Grupla" name="group_by_product" context="{'group_by':'product_id'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Gider Matrisi Aksiyonu -->
|
||||||
|
<record id="action_mymach_fleet_maintenance_cost" model="ir.actions.act_window">
|
||||||
|
<field name="name">Gider Matrisi (Araç Bazlı Analiz)</field>
|
||||||
|
<field name="res_model">mymach.fleet.maintenance.cost</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="context">{'search_default_group_by_vehicle': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Alt Menü -->
|
||||||
|
<menuitem id="menu_mymach_fleet_maintenance_cost"
|
||||||
|
name="Finans & Gider Matrisi"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_maintenance_cost"
|
||||||
|
sequence="2"
|
||||||
|
groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Periyodik Bakım Hatırlatıcı Liste (Ağaç) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_periodic_maintenance_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.periodic.maintenance.tree</field>
|
||||||
|
<field name="model">mymach.fleet.periodic.maintenance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Periyodik Bakım Hatırlatıcıları" decoration-danger="state == 'overdue'" decoration-info="state == 'pending'" decoration-success="state == 'done'">
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="fuel_type"/>
|
||||||
|
<field name="maintenance_type"/>
|
||||||
|
<field name="odometer"/>
|
||||||
|
<field name="target_odometer"/>
|
||||||
|
<field name="target_date"/>
|
||||||
|
<field name="state" widget="badge" decoration-danger="state == 'overdue'" decoration-success="state == 'done'" decoration-warning="state == 'pending'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Periyodik Bakım Hatırlatıcı Form Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_periodic_maintenance_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.periodic.maintenance.form</field>
|
||||||
|
<field name="model">mymach.fleet.periodic.maintenance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Periyodik Bakım Hatırlatıcısı">
|
||||||
|
<header>
|
||||||
|
<button name="action_complete" string="Bakımı Tamamlandı Olarak İşaretle" type="object" class="oe_highlight" invisible="state == 'done'"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="pending,overdue,done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" readonly="1"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Araç ve Durum Bilgileri">
|
||||||
|
<field name="vehicle_id" readonly="state == 'done'"/>
|
||||||
|
<field name="fuel_type"/>
|
||||||
|
<field name="odometer"/>
|
||||||
|
<field name="maintenance_type" readonly="state == 'done'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Hedef ve Tamamlanma Verileri">
|
||||||
|
<field name="last_odometer" readonly="1"/>
|
||||||
|
<field name="target_odometer" readonly="state == 'done'"/>
|
||||||
|
<field name="target_date" readonly="state == 'done'"/>
|
||||||
|
<field name="date_completed" readonly="1" invisible="state != 'done'"/>
|
||||||
|
<field name="odometer_completed" readonly="1" invisible="state != 'done'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Yapılacak Standart İşlemler">
|
||||||
|
<field name="description" readonly="1" style="font-family: monospace; font-size: 13px; background-color: #f8fafc; padding: 15px; border-radius: 8px;"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Periyodik Bakım Arama (Search) Görünümü -->
|
||||||
|
<record id="view_mymach_fleet_periodic_maintenance_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.periodic.maintenance.search</field>
|
||||||
|
<field name="model">mymach.fleet.periodic.maintenance</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Bakım Arama">
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="maintenance_type"/>
|
||||||
|
<filter string="Süresi/KM Geçenler" name="overdue" domain="[('state', '=', 'overdue')]"/>
|
||||||
|
<filter string="Bekleyenler" name="pending" domain="[('state', '=', 'pending')]"/>
|
||||||
|
<filter string="Tamamlananlar" name="done" domain="[('state', '=', 'done')]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Araç Bazında Grupla" name="group_by_vehicle" context="{'group_by': 'vehicle_id'}"/>
|
||||||
|
<filter string="Durum Bazında Grupla" name="group_by_state" context="{'group_by': 'state'}"/>
|
||||||
|
<filter string="Bakım Türü Bazında Grupla" name="group_by_type" context="{'group_by': 'maintenance_type'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Pencere Aksiyonu (Act Window) -->
|
||||||
|
<record id="action_mymach_fleet_periodic_maintenance" model="ir.actions.act_window">
|
||||||
|
<field name="name">Periyodik Bakım Takibi</field>
|
||||||
|
<field name="res_model">mymach.fleet.periodic.maintenance</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_mymach_fleet_periodic_maintenance_search"/>
|
||||||
|
<field name="context">{'search_default_overdue': 1, 'search_default_pending': 1}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk periyodik bakım hatırlatıcısını ekleyin!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Sistem, araçların yakıt türü ve kilometresine göre periyodik bakımları otomatik olarak takip eder.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menü Tanımı -->
|
||||||
|
<menuitem id="menu_mymach_fleet_periodic_maintenance"
|
||||||
|
name="Periyodik Bakımlar"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_periodic_maintenance"
|
||||||
|
sequence="4"/>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mymach_fleet_route_exception_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.route.exception.wizard.form</field>
|
||||||
|
<field name="model">mymach.fleet.route.exception.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Sürpriz Rota Karar Merkezi">
|
||||||
|
<group>
|
||||||
|
<group string="Yolculuk Detayları">
|
||||||
|
<field name="vehicle_id" readonly="1"/>
|
||||||
|
<field name="log_id" invisible="1"/>
|
||||||
|
<field name="exception_type" widget="radio"/>
|
||||||
|
<field name="distance" readonly="1"/>
|
||||||
|
<field name="estimated_cost" readonly="1" invisible="exception_type not in ('personal', 'project')"/>
|
||||||
|
</group>
|
||||||
|
<group string="İlişkili Bağlantılar">
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="project_id" invisible="exception_type != 'project'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Açıklama & Detaylar">
|
||||||
|
<field name="description" placeholder="Görüşme notları, proje gerekçesi veya diğer açıklamaları buraya yazın..." style="height: 100px;"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_confirm" type="object" string="Onayla ve İşle" class="btn-primary"/>
|
||||||
|
<button string="İptal" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mymach_fleet_security_log_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.security.log.list</field>
|
||||||
|
<field name="model">mymach.fleet.security.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Güvenlik Logları" decoration-danger="status == 'in_office_moving'" decoration-warning="status == 'exception' and not is_resolved" decoration-success="is_resolved or status == 'project_match'">
|
||||||
|
<field name="datetime"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<field name="is_resolved"/>
|
||||||
|
<field name="deduction_amount" widget="monetary" invisible="deduction_amount == 0"/>
|
||||||
|
<field name="currency_id" column_invisible="True"/>
|
||||||
|
<button name="action_open_exception_wizard" type="object" string="Sürpriz Rotayı Çözümle" class="btn-primary" invisible="is_resolved or status != 'exception'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_mymach_fleet_security_log_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.fleet.security.log.form</field>
|
||||||
|
<field name="model">mymach.fleet.security.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Güvenlik Logu">
|
||||||
|
<header>
|
||||||
|
<button name="action_open_exception_wizard" type="object" string="İstisna Rota Çözümle (Popup)" class="oe_highlight" invisible="is_resolved or status != 'exception'"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="vehicle_id"/>
|
||||||
|
<field name="driver_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="datetime"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<field name="is_resolved"/>
|
||||||
|
<field name="deduction_amount" widget="monetary" invisible="deduction_amount == 0"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mymach_fleet_security_log" model="ir.actions.act_window">
|
||||||
|
<field name="name">Güvenlik ve İstisna Logları</field>
|
||||||
|
<field name="res_model">mymach.fleet.security.log</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Çözüm Bekleyen Sürpriz Rotalar Aksiyonu (UI/UX İyileştirmesi) -->
|
||||||
|
<record id="action_mymach_fleet_unresolved_exceptions" model="ir.actions.act_window">
|
||||||
|
<field name="name">Çözüm Bekleyen Sürpriz Rotalar</field>
|
||||||
|
<field name="res_model">mymach.fleet.security.log</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('status', '=', 'exception'), ('is_resolved', '=', False)]</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Çözüm bekleyen herhangi bir sürpriz rota sapması bulunmuyor. Harikasınız!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mymach_fleet_security_log"
|
||||||
|
name="Otonom Güvenlik Logları"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_security_log"
|
||||||
|
sequence="3"
|
||||||
|
groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_mymach_fleet_unresolved_exceptions"
|
||||||
|
name="Çözüm Bekleyen Rotalar"
|
||||||
|
parent="menu_mymach_fleet_intelligence_root"
|
||||||
|
action="action_mymach_fleet_unresolved_exceptions"
|
||||||
|
sequence="4"
|
||||||
|
groups="mymach_fleet_intelligence.group_mymach_fleet_manager"/>
|
||||||
|
</odoo>
|
||||||
4
assign_group.py
Normal file
4
assign_group.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
env.user.write({'groups_id': [(4, env.ref('mymach_fleet_intelligence.group_mymach_fleet_manager').id)]})
|
||||||
|
env['res.users'].browse(2).write({'groups_id': [(4, env.ref('mymach_fleet_intelligence.group_mymach_fleet_manager').id)]})
|
||||||
|
env.cr.commit()
|
||||||
|
print("Group assigned to admin user successfully")
|
||||||
9
config/odoo.conf
Normal file
9
config/odoo.conf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[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
|
||||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:17
|
||||||
|
container_name: ceren_mymach_db
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
- POSTGRES_USER=odoo
|
||||||
|
- POSTGRES_PASSWORD=odoo
|
||||||
|
volumes:
|
||||||
|
- ceren_mymach_pg_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
odoo:
|
||||||
|
image: odoo:19
|
||||||
|
container_name: ceren_mymach_web
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "8070:8069"
|
||||||
|
- "8073:8072"
|
||||||
|
volumes:
|
||||||
|
- ceren_mymach_odoo_data:/var/lib/odoo
|
||||||
|
- ./addons:/mnt/extra-addons
|
||||||
|
- ./config/odoo.conf:/etc/odoo/odoo.conf
|
||||||
|
environment:
|
||||||
|
- HOST=db
|
||||||
|
- USER=odoo
|
||||||
|
- PASSWORD=odoo
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ceren_mymach_pg_data:
|
||||||
|
ceren_mymach_odoo_data:
|
||||||
7
ping.py
Normal file
7
ping.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
url = "http://localhost:8069/mymach/fleet/ping"
|
||||||
|
data = {"params": {"vehicle_id": 13, "latitude": 41.0, "longitude": 29.0, "api_token": "MYMACH_SECURE_TOKEN_2026"}}
|
||||||
|
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers={'Content-Type': 'application/json'})
|
||||||
|
resp = urllib.request.urlopen(req)
|
||||||
|
print(resp.read().decode('utf-8'))
|
||||||
19
test_fix.py
Normal file
19
test_fix.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from odoo import fields
|
||||||
|
env = env(user=1)
|
||||||
|
|
||||||
|
vehicle = env['fleet.vehicle'].search([('license_plate', '=', '34 YAPAY 2026')], limit=1)
|
||||||
|
|
||||||
|
partner = env['res.partner'].create({'name': 'Veli (İhlal Testi)'})
|
||||||
|
employee = env['hr.employee'].create({
|
||||||
|
'name': 'Veli',
|
||||||
|
'work_contact_id': partner.id
|
||||||
|
})
|
||||||
|
|
||||||
|
vehicle.driver_id = partner.id
|
||||||
|
|
||||||
|
env['hr.attendance'].create({
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'check_in': fields.Datetime.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
env.cr.commit()
|
||||||
53
test_phase2.py
Normal file
53
test_phase2.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
print("\n--- FAZ 2 TEST BAŞLIYOR ---")
|
||||||
|
product = env['product.template'].create({'name': 'Test Bakım Ürünü', 'type': 'service', 'purchase_ok': False})
|
||||||
|
|
||||||
|
# Odoo'nun demo datalarında USD olmayabilir veya inaktif olabilir, bulalım veya yaratalım
|
||||||
|
currency_usd = env['res.currency'].search([('name', '=', 'USD'), '|', ('active', '=', True), ('active', '=', False)], limit=1)
|
||||||
|
if not currency_usd:
|
||||||
|
currency_usd = env['res.currency'].create({'name': 'USD', 'symbol': '$', 'active': True})
|
||||||
|
else:
|
||||||
|
currency_usd.active = True
|
||||||
|
|
||||||
|
# Örnek kurlar ekleyelim (TL'den USD'ye çeviri için)
|
||||||
|
from datetime import date
|
||||||
|
env['res.currency.rate'].create({
|
||||||
|
'name': date.today(),
|
||||||
|
'currency_id': currency_usd.id,
|
||||||
|
'company_id': env.company.id,
|
||||||
|
'rate': 0.03 # Örn: 1 TL = 0.03 USD
|
||||||
|
})
|
||||||
|
|
||||||
|
model = env['fleet.vehicle.model'].search([], limit=1)
|
||||||
|
if not model:
|
||||||
|
brand = env['fleet.vehicle.model.brand'].create({'name': 'Test Brand'})
|
||||||
|
model = env['fleet.vehicle.model'].create({'name': 'Test Model', 'brand_id': brand.id})
|
||||||
|
|
||||||
|
vehicle = env['fleet.vehicle'].create({
|
||||||
|
'model_id': model.id,
|
||||||
|
'license_plate': '34 TEST 2026',
|
||||||
|
'purchase_value': 1000000.0,
|
||||||
|
'index_currency_id': currency_usd.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Maliyet Matrisi Kaydı
|
||||||
|
cost1 = env['mymach.fleet.maintenance.cost'].create({
|
||||||
|
'name': '10.000 Bakımı',
|
||||||
|
'product_id': product.id,
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'amount': 5000.0
|
||||||
|
})
|
||||||
|
cost2 = env['mymach.fleet.maintenance.cost'].create({
|
||||||
|
'name': '20.000 Bakımı',
|
||||||
|
'product_id': product.id,
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'amount': 7000.0
|
||||||
|
})
|
||||||
|
|
||||||
|
print("TEST_RESULT_START")
|
||||||
|
print("Ürün Masraf Bayrağı (can_be_expensed):", getattr(product, 'can_be_expensed', 'Masraf Modülü Yüklü Değil, Atlandı.'))
|
||||||
|
print("Ürün Satınalma Bayrağı (purchase_ok):", product.purchase_ok)
|
||||||
|
print("Araç Hareketli Ortalama Bakım Maliyeti (Beklenen 6000):", vehicle.moving_average_maintenance_cost)
|
||||||
|
print("Araç Endeksli Alım Bedeli (USD, Kura Göre Hesaplanır):", vehicle.indexed_purchase_value)
|
||||||
|
print("TEST_RESULT_END")
|
||||||
|
print("--- FAZ 2 TEST BİTTİ ---\n")
|
||||||
|
env.cr.rollback()
|
||||||
42
test_phase3.py
Normal file
42
test_phase3.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
print("\n--- FAZ 3 TEST BAŞLIYOR ---")
|
||||||
|
partner = env['res.partner'].create({'name': 'Sürpriz Sürücü'})
|
||||||
|
vehicle = env['fleet.vehicle'].search([], limit=1)
|
||||||
|
vehicle.driver_id = partner.id
|
||||||
|
|
||||||
|
# 1. Sözleşme Yasaklı İken
|
||||||
|
contract = env['mymach.fleet.contract'].create({
|
||||||
|
'name': 'TEST-FAZ3',
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'personal_use_policy': 'forbidden',
|
||||||
|
})
|
||||||
|
|
||||||
|
log = env['mymach.fleet.security.log'].create({
|
||||||
|
'name': 'Test GPS Log',
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'status': 'exception'
|
||||||
|
})
|
||||||
|
|
||||||
|
wizard = env['mymach.fleet.route.exception.wizard'].create({
|
||||||
|
'log_id': log.id,
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'exception_type': 'personal',
|
||||||
|
'description': 'Haftasonu Piknik',
|
||||||
|
'estimated_cost': 500.0
|
||||||
|
})
|
||||||
|
|
||||||
|
print("TEST_RESULT_START")
|
||||||
|
try:
|
||||||
|
wizard.action_confirm()
|
||||||
|
print("HATA: Yasaklı sözleşmeye rağmen onaylandı!")
|
||||||
|
except Exception as e:
|
||||||
|
print("BAŞARILI: Sözleşme yasağı devreye girdi ve engelledi. Hata Mesajı:", str(e))
|
||||||
|
|
||||||
|
# 2. Sözleşme İzinli İken
|
||||||
|
contract.personal_use_policy = 'allowed'
|
||||||
|
wizard.action_confirm()
|
||||||
|
print("BAŞARILI: İzinli sözleşme onaylandı.")
|
||||||
|
print("Log Kesinti Tutarı (Beklenen 500.0):", log.deduction_amount)
|
||||||
|
print("Log Çözüldü Mü (Beklenen True):", log.is_resolved)
|
||||||
|
print("TEST_RESULT_END")
|
||||||
|
print("--- FAZ 3 TEST BİTTİ ---\n")
|
||||||
|
env.cr.rollback()
|
||||||
62
test_scenario.py
Normal file
62
test_scenario.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from odoo import fields
|
||||||
|
env = env(user=1)
|
||||||
|
|
||||||
|
print("\n--- [TEST SENARYOSU BASLIYOR] ---")
|
||||||
|
|
||||||
|
# 1. Örnek Personel ve Partner (Sürücü) Yarat
|
||||||
|
partner = env['res.partner'].create({'name': 'Ahmet Yılmaz (Test Sürücüsü)'})
|
||||||
|
employee = env['hr.employee'].create({
|
||||||
|
'name': 'Ahmet Yılmaz'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Araç Yarat (Model yoksa ilk bulduğunu alır)
|
||||||
|
model_id = env['fleet.vehicle.model'].search([], limit=1)
|
||||||
|
vehicle = env['fleet.vehicle'].create({
|
||||||
|
'model_id': model_id.id,
|
||||||
|
'license_plate': '34 YAPAY 2026',
|
||||||
|
'purchase_value': 25000,
|
||||||
|
'depreciation_years': 5,
|
||||||
|
'annual_fixed_cost': 1500,
|
||||||
|
'tank_capacity': 60,
|
||||||
|
'factory_avg_consumption': 6.0,
|
||||||
|
'driver_id': partner.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Sözleşme Yarat (Sıfır Tolerans: Şahsi Kullanım Yasak)
|
||||||
|
contract = env['mymach.fleet.contract'].create({
|
||||||
|
'name': 'CON-TEST-001',
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'personal_use_policy': 'forbidden',
|
||||||
|
'commute_allowed': 'forbidden'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ürün (Hizmet/Masraf) bulalım
|
||||||
|
product = env['product.product'].search([], limit=1)
|
||||||
|
if not product:
|
||||||
|
product = env['product.product'].create({'name': 'Genel Bakım/Yakıt', 'type': 'service'})
|
||||||
|
|
||||||
|
# 4. Kestirimci Bakımı Tetikleyecek Veri Girişi (Çok Sayıda Yüksek Masraf)
|
||||||
|
for i in range(3):
|
||||||
|
env['mymach.fleet.maintenance.cost'].create({
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'product_id': product.id,
|
||||||
|
'amount': 5500.0,
|
||||||
|
'name': f'Yakıt/Bakım Alımı {i+1} (Yüksek Tüketim Simülasyonu)'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. Prediktif Bakım AI (Cron Job) Manuel Tetikleme
|
||||||
|
env['mymach.fleet.maintenance.cost'].action_sync_fuel_data_cron()
|
||||||
|
|
||||||
|
# 6. Güvenlik İhlali İçin Personeli Ofiste (İçeride) Gösterme (hr.attendance)
|
||||||
|
env['hr.attendance'].create({
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'check_in': fields.Datetime.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
env.cr.commit()
|
||||||
|
|
||||||
|
print(f"\n--- [BASARILI] ---")
|
||||||
|
print(f"Araç Plakası: {vehicle.license_plate}")
|
||||||
|
print(f"Araç ID: {vehicle.id}")
|
||||||
|
print(f"Personel: {employee.name}")
|
||||||
|
print("API'den Ping Atmaya Hazır!\n")
|
||||||
27
test_script.py
Normal file
27
test_script.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
print("\n--- TEST BAŞLIYOR ---")
|
||||||
|
partner = env['res.partner'].create({'name': 'Test Sürücü'})
|
||||||
|
dept = env['hr.department'].create({'name': 'Test Departmanı'})
|
||||||
|
employee = env['hr.employee'].create({'name': 'Test Çalışan', 'work_contact_id': partner.id, 'department_id': dept.id})
|
||||||
|
|
||||||
|
contract = env['mymach.fleet.contract'].create({
|
||||||
|
'name': 'TEST-2026',
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'personal_use_policy': 'limit',
|
||||||
|
'max_km_limit': 1000,
|
||||||
|
'commute_allowed': 'allowed',
|
||||||
|
'personal_device_gps_allowed': True,
|
||||||
|
'company_phone_device_id': str(uuid.uuid4())
|
||||||
|
})
|
||||||
|
|
||||||
|
print("TEST_RESULT_START")
|
||||||
|
print("Sözleşme ID:", contract.id)
|
||||||
|
print("Sözleşme Adı:", contract.name)
|
||||||
|
print("Sürücü:", contract.driver_id.name)
|
||||||
|
print("İlgili Çalışan:", contract.employee_id.name if contract.employee_id else 'Yok')
|
||||||
|
print("Departman:", contract.department_id.name if contract.department_id else 'Yok')
|
||||||
|
print("KM Limiti:", contract.max_km_limit)
|
||||||
|
print("TEST_RESULT_END")
|
||||||
|
print("--- TEST BİTTİ ---\n")
|
||||||
|
env.cr.rollback() # Test verilerini geri al
|
||||||
102
test_suite.py
Normal file
102
test_suite.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import datetime
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
from odoo import fields
|
||||||
|
|
||||||
|
env = env(user=1)
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("MYMACH AKILLI FILO - KAPSAMLI FOTOĞRAFLI TESLİM & İADE TEST SUİTİ")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
# --- HAZIRLIK ---
|
||||||
|
print("\n[HAZIRLIK] Test Verileri Oluşturuluyor...")
|
||||||
|
partner = env['res.partner'].create({'name': 'TEST_Sürücü_Teslim'})
|
||||||
|
employee = env['hr.employee'].create({'name': 'TEST_Personel_Teslim', 'work_contact_id': partner.id})
|
||||||
|
|
||||||
|
contract = env['mymach.fleet.contract'].create({
|
||||||
|
'name': 'CON-HANDOVER-TEST',
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'personal_use_policy': 'forbidden',
|
||||||
|
'commute_allowed': 'allowed',
|
||||||
|
'driver_score': 100
|
||||||
|
})
|
||||||
|
|
||||||
|
model_id = env['fleet.vehicle.model'].search([], limit=1)
|
||||||
|
vehicle = env['fleet.vehicle'].create({
|
||||||
|
'model_id': model_id.id,
|
||||||
|
'license_plate': '34 TESLIM 26',
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'purchase_value': 50000,
|
||||||
|
'tank_capacity': 60.0,
|
||||||
|
'factory_avg_consumption': 6.0
|
||||||
|
})
|
||||||
|
print(f" -> Sözleşme oluşturuldu: {contract.name}")
|
||||||
|
print(f" -> Araç oluşturuldu: {vehicle.license_plate}")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# BÖLÜM 1: ARAÇ TESLİM ALIM (GİRİŞ) İŞLEMİ
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("\n[TEST 1] ARAÇ TESLİM ALIM (GİRİŞ) YAZILIM TESTİ")
|
||||||
|
# Sahte bir fotoğraf (Binary string)
|
||||||
|
dummy_photo = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||||
|
|
||||||
|
handover = env['mymach.fleet.handover'].create({
|
||||||
|
'vehicle_id': vehicle.id,
|
||||||
|
'driver_id': partner.id,
|
||||||
|
'start_odometer': 12000.0,
|
||||||
|
'start_fuel_level': 80.0, # %80 depo ile aldı
|
||||||
|
'start_photo': dummy_photo
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f" -> Kayıt Oluşturuldu: {handover.name} (Durum: {handover.state})")
|
||||||
|
handover.action_take()
|
||||||
|
print(f" -> Teslim Alındı Sonrası Durum: {handover.state}")
|
||||||
|
print(f" -> Araç Depo Durumu (Litre): {vehicle.current_fuel_level} L (Kapasite: {vehicle.tank_capacity} L)")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# BÖLÜM 2: ARAÇ TESLİM ETME (İADE) İŞLEMİ
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("\n[TEST 2] ARAÇ İADE VE ODOMETER GÜNCELLEME TESTİ")
|
||||||
|
handover.write({
|
||||||
|
'end_odometer': 12350.0, # 350 KM yol yapmış
|
||||||
|
'end_fuel_level': 45.0, # %45 depo ile iade etti
|
||||||
|
'end_photo': dummy_photo
|
||||||
|
})
|
||||||
|
handover.action_return()
|
||||||
|
print(f" -> İade Edildi Sonrası Durum: {handover.state}")
|
||||||
|
print(f" -> Yapılan Yol (Hesaplanan): {handover.odometer_diff} KM")
|
||||||
|
print(f" -> Yakıt Değişimi: %{handover.fuel_diff}")
|
||||||
|
# En son odometer değerini kontrol edelim
|
||||||
|
last_odometer = env['fleet.vehicle.odometer'].search([('vehicle_id', '=', vehicle.id)], order='value desc', limit=1)
|
||||||
|
print(f" -> Araç Son Kilometresi (Odometer): {last_odometer.value} KM")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# BÖLÜM 3: YÖNETİCİ DENETİM VE ONAYI
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("\n[TEST 3] YÖNETİCİ KIYASLAMA VE ONAYLAMA")
|
||||||
|
handover.write({
|
||||||
|
'manager_note': 'Yakıt farkı makul seviyede, hasarsız teslim alındı.'
|
||||||
|
})
|
||||||
|
handover.action_compare()
|
||||||
|
print(f" -> Denetim Sonrası Durum: {handover.state} (Onaylandı mı?: {handover.is_approved})")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# BÖLÜM 4: OTONOM FOTOĞRAF PURGE (CRON) TESTİ
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
print("\n[TEST 4] 10 GÜNLÜK FOTOĞRAF OTONOM PURGE CRON TESTİ")
|
||||||
|
# İade tarihini 11 gün öncesine alalım
|
||||||
|
eleven_days_ago = fields.Datetime.now() - datetime.timedelta(days=11)
|
||||||
|
handover.write({'return_datetime': eleven_days_ago})
|
||||||
|
|
||||||
|
print(f" -> İade Tarihi Geriye Alındı: {handover.return_datetime}")
|
||||||
|
print(f" -> Temizlik Öncesi Fotoğraflar Mevcut mu?: (Alış: {bool(handover.start_photo)}, İade: {bool(handover.end_photo)})")
|
||||||
|
|
||||||
|
# Cron fonksiyonunu çalıştır
|
||||||
|
deleted_count = env['mymach.fleet.handover'].action_purge_expired_photos()
|
||||||
|
|
||||||
|
print(f" -> Cron Çalıştırıldı. Temizlenen Kayıt Sayısı: {deleted_count}")
|
||||||
|
print(f" -> Temizlik Sonrası Fotoğraflar Mevcut mu?: (Alış: {bool(handover.start_photo)}, İade: {bool(handover.end_photo)})")
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("FOTOĞRAFLI TESLİM/İADE VE CRON TESTLERİ BAŞARIYLA TAMAMLANDI!")
|
||||||
|
print("="*50 + "\n")
|
||||||
Reference in New Issue
Block a user