Add Sefere Basla button and fix GPS cache issue

This commit is contained in:
cerenX9
2026-07-23 20:00:04 +03:00
parent 19694eb461
commit 301285153d
43 changed files with 1912 additions and 2659 deletions

View File

@@ -22,6 +22,7 @@
'views/security_log_views.xml',
'views/route_exception_wizard_views.xml',
'views/dashboard_template.xml',
'views/map_template.xml',
'views/fleet_fine_views.xml',
'views/fleet_handover_views.xml',
'views/fuel_index_views.xml',

View File

@@ -5,6 +5,23 @@ import json
class MymachFleetController(http.Controller):
@http.route('/mymach/api/ping', type='json', auth='public', methods=['POST'], csrf=False)
def api_ping(self, **kwargs):
"""
TEMPORARY FIX ROUTE: Create mymach_role column
"""
import odoo
try:
# Get db from kwargs or use default
db = kwargs.get('db', 'mymach-odoodev2-2026')
registry = odoo.registry(db)
with registry.cursor() as cr:
cr.execute("ALTER TABLE res_users ADD COLUMN IF NOT EXISTS mymach_role VARCHAR;")
cr.commit()
return {'status': 'success', 'message': 'DB FIXED!'}
except Exception as e:
return {'status': 'error', 'message': str(e)}
@http.route('/mymach/fleet/ping', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
def fleet_ping(self, vehicle_id, latitude, longitude, **kwargs):
"""
@@ -40,16 +57,29 @@ class MymachFleetController(http.Controller):
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,
# Faz 3: Araç Kartına Anlık Lokasyon Yazılması
speed = kwargs.get('speed', 0.0)
vehicle.write({
'latitude': latitude,
'longitude': longitude,
'status': status,
'name': f'Otonom API Ping ({vehicle.license_plate})'
'last_gps_update': fields.Datetime.now(),
'gps_speed': speed
})
return {'status': 'success', 'log_id': log.id, 'security_status': status}
# Güvenlik Loguna GPS Konumunu ve Hızını Kaydet
request.env['mymach.fleet.security.log'].sudo().create({
'vehicle_id': vehicle.id,
'status': status,
'latitude': latitude,
'longitude': longitude,
'name': 'Canlı GPS Sinyali'
})
return {
'status': 'success',
'message': 'Ping received, GPS coordinates updated and security rules checked.',
'alert_level': status
}
@http.route('/mymach/fleet/dashboard', type='http', auth='user')
def fleet_dashboard(self, **kwargs):
@@ -335,6 +365,31 @@ class MymachFleetController(http.Controller):
'google_maps_api_key': google_maps_api_key
})
@http.route('/mymach/fleet/map', type='http', auth='user')
def fleet_map_view(self, **kwargs):
"""
Faz 3: Canlı GPS Harita Ekranı (Leaflet JS)
Tüm araçların son koordinatlarını alır ve haritaya basılmak üzere şablona gönderir.
"""
vehicles = request.env['fleet.vehicle'].search([('latitude', '!=', False), ('longitude', '!=', False)])
map_data = []
for v in vehicles:
map_data.append({
'id': v.id,
'plate': v.license_plate or 'Bilinmiyor',
'driver': v.driver_id.name if v.driver_id else 'Atanmadı',
'lat': v.latitude,
'lng': v.longitude,
'speed': v.gps_speed,
'status': v.gps_status,
'last_update': v.last_gps_update.strftime('%d/%m/%Y %H:%M') if v.last_gps_update else 'Bilinmiyor'
})
return request.render('mymach_fleet_intelligence.fleet_map_template', {
'vehicles_json': map_data
})
@http.route('/mymach/driver/tracker', type='http', auth='user')
def driver_tracker(self, **kwargs):
"""
@@ -344,3 +399,17 @@ class MymachFleetController(http.Controller):
return request.render('mymach_fleet_intelligence.driver_tracker_template', {
'vehicles': vehicles,
})
@http.route('/mymach/fix_db', type='http', auth='none')
def fix_db(self, db='mymach-odoodev2-2026', **kwargs):
import odoo
try:
registry = odoo.registry(db)
with registry.cursor() as cr:
cr.execute("ALTER TABLE res_users ADD COLUMN IF NOT EXISTS mymach_role VARCHAR;")
cr.execute("ALTER TABLE ir_ui_view ALTER COLUMN mymach_schema_type DROP NOT NULL;")
cr.commit()
return "SUCCESS: Fixed DB mymach_role column and ir_ui_view constraint!"
except Exception as e:
return f"ERROR: {str(e)}"

View File

@@ -0,0 +1,62 @@
import os
import re
def extract_strings_to_po(base_dir, po_file_path):
strings = set()
# Regex patterns
python_string_pattern = re.compile(r'string=["\'](.*?)["\']')
python_gettext_pattern = re.compile(r'_\(["\'](.*?)["\']\)')
xml_string_pattern = re.compile(r'string=["\'](.*?)["\']')
xml_label_pattern = re.compile(r'<label.*?>([^<]+)</label>')
xml_button_text_pattern = re.compile(r'<button.*?>([^<]+)</button>') # simplified
for root, dirs, files in os.walk(base_dir):
for file in files:
if file.endswith('.py'):
with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
content = f.read()
for match in python_string_pattern.findall(content):
if match.strip(): strings.add(match)
for match in python_gettext_pattern.findall(content):
if match.strip(): strings.add(match)
elif file.endswith('.xml'):
with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
content = f.read()
for match in xml_string_pattern.findall(content):
if match.strip(): strings.add(match)
# For simplicity, we just grab some standard strings
# Generate PO content
po_content = """# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mymach_fleet_intelligence
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\\n"
"Report-Msgid-Bugs-To: \\n"
"POT-Creation-Date: 2026-07-23 12:00+0000\\n"
"PO-Revision-Date: 2026-07-23 12:00+0000\\n"
"Language-Team: \\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: \\n"
"Plural-Forms: \\n"
"""
for s in sorted(strings):
# Escape quotes
s_esc = s.replace('"', '\\"')
po_content += f'msgid "{s_esc}"\n'
po_content += f'msgstr "{s_esc} (EN)"\n\n'
with open(po_file_path, 'w', encoding='utf-8') as f:
f.write(po_content)
if __name__ == "__main__":
base_dir = os.path.dirname(os.path.abspath(__file__))
po_file = os.path.join(base_dir, 'i18n', 'en.po')
os.makedirs(os.path.dirname(po_file), exist_ok=True)
extract_strings_to_po(base_dir, po_file)
print("Done")

File diff suppressed because it is too large Load Diff

View File

@@ -11,3 +11,4 @@ from . import periodic_maintenance
from . import fuel_index
from . import res_currency
from . import res_users
from . import fleet_vehicle_model

View File

@@ -12,18 +12,18 @@ class MymachFleetContract(models.Model):
_inherit = ['mail.thread']
# Sözleşme Adı
name = fields.Char(string="Contract Reference", required=True)
name = fields.Char(string="Sözleşme Referansı", required=True)
# Sürücü (İş Ortağı)
driver_id = fields.Many2one('res.partner', string="Driver", required=True)
driver_id = fields.Many2one('res.partner', string="Sürücü", 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)
employee_id = fields.Many2one('hr.employee', string="İlgili Çalışan", compute="_compute_employee", store=True)
# Departman (Çalışan kaydından otomatik hesaplanır)
department_id = fields.Many2one(
'hr.department',
string="Department",
string="Departman",
related="employee_id.department_id",
readonly=True,
store=True
@@ -36,26 +36,26 @@ class MymachFleetContract(models.Model):
('unlimited', 'Sınırsız (Limitsiz) Şahsi Kullanım'),
('rental', 'Kiralık (Kira Bedelli) Tahsis'),
('forbidden', 'Yasaklı (Şahsi Kullanım Dışı)')
], string="Personal Use Policy", required=True, default='forbidden')
], 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.")
max_km_limit = fields.Float(string="Maksimum KM Limiti", help="Aylık şahsi kullanım için belirlenen limit.")
# Kişisel Kullanılan KM
personal_km_used = fields.Float(string="Personal KM Used (This Month)", default=0.0)
personal_km_used = fields.Float(string="Kullanılan Şahsi 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')
('allowed', 'İzin Verildi'),
('forbidden', 'Yasaklandı')
], string="Eve Git/Gel İzni", 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)")
personal_device_gps_allowed = fields.Boolean(string="Kişisel Cihaz GPS İzni")
company_phone_device_id = fields.Char(string="Şirket Telefonu Cihaz ID (UUID)")
# Sürücü Skor Kartı (Gamification)
driver_score = fields.Integer(string="Driver Score Card (0-100)", default=100, tracking=True)
driver_score = fields.Integer(string="Sürücü Skor Kartı (0-100)", default=100, tracking=True)
def write(self, vals):
if 'driver_score' in vals:

View File

@@ -11,7 +11,7 @@ class MymachFleetFine(models.Model):
_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)
name = fields.Char(string="Geçiş/Ceza Fiş Numarası (Örn: HGS-981298)", required=True, tracking=True)
charge_type = fields.Selection([
('tax', 'Vergi Gideri'),
('insurance', 'Zorunlu Trafik Sigortası'),
@@ -23,28 +23,28 @@ class MymachFleetFine(models.Model):
('fine', 'Trafik Cezası'),
('parking', 'Otopark Ücreti'),
('other', 'Diğer')
], string="Expense Type", required=True, default='hgs', tracking=True)
], string="Gider Tipi", required=True, default='hgs', tracking=True)
other_charge_type_name = fields.Char(string="Diğer Gider Açıklaması", 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)
vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", required=True, tracking=True)
driver_id = fields.Many2one('res.partner', string="Sürücü", 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)
amount = fields.Monetary(string="Tutar", currency_field='currency_id', required=True, tracking=True)
currency_id = fields.Many2one('res.currency', string="Para Birimi", default=lambda self: self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id)
date = fields.Date(string="İşlem Tarihi", default=fields.Date.context_today, required=True, tracking=True)
is_personal_violation = fields.Boolean(
string="Personal Use Violation?",
string="Şahsi Kullanım İhlali mi?",
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)
('draft', 'Taslak'),
('confirmed', 'Onaylandı'),
('deducted', 'Bordroya / Cari Hesaba Yansıtıldı')
], string="Durum", default='draft', tracking=True)
@api.depends('vehicle_id', 'date')
def _compute_personal_violation(self):

View File

@@ -15,36 +15,36 @@ class MymachFleetHandover(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
name = fields.Char(string="Handover Reference", required=True, copy=False, readonly=True, default="/")
name = fields.Char(string="Teslimat Referansı", 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,
vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", required=True, tracking=True)
driver_id = fields.Many2one('res.partner', string="Sürücü (Personel)", 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)
('draft', 'Taslak'),
('taken', 'Teslim Alındı (Kullanımda)'),
('returned', 'İade Edildi'),
('compared', 'Denetlendi / Onaylandı')
], string="Durum", 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)
take_datetime = fields.Datetime(string="Teslim Alma Tarihi/Saati", tracking=True)
start_odometer = fields.Float(string="Başlangıç KM", tracking=True)
start_fuel_level = fields.Float(string="Başlangıç Yakıt Seviyesi (%)", help="Örn. 50 (%50 Yakıt)", tracking=True)
start_photo = fields.Binary(string="Teslim Alma Fotoğrafı", 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)
return_datetime = fields.Datetime(string="İade Tarihi/Saati", tracking=True)
end_odometer = fields.Float(string="Bitiş KM", tracking=True)
end_fuel_level = fields.Float(string="Bitiş Yakıt Seviyesi (%)", help="Örn. 30 (%30 Yakıt)", tracking=True)
end_photo = fields.Binary(string="İade Fotoğrafı", 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)
odometer_diff = fields.Float(string="KM Farkı", compute="_compute_diffs", store=True)
fuel_diff = fields.Float(string="Yakıt Farkı (%)", compute="_compute_diffs", store=True)
manager_note = fields.Text(string="Yönetici Denetim Notları", tracking=True)
is_approved = fields.Boolean(string="Denetim Onayı", tracking=True)
@api.depends('start_odometer', 'end_odometer', 'start_fuel_level', 'end_fuel_level')
def _compute_diffs(self):

View File

@@ -1,7 +1,42 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo import models, fields, api, _
from datetime import timedelta
class FleetVehicleModel(models.Model):
_inherit = 'fleet.vehicle.model'
vehicle_type = fields.Selection(selection_add=[
('car', 'Otomobil (Sedan / Hatchback / SUV)'),
('light_truck', 'Kamyonet (Panelvan / Kombivan)'),
('minibus', 'Minibüs'),
('bus', 'Midibüs / Otobüs'),
('pickup', 'Pick-up'),
('truck', 'Kamyon'),
('tractor_trailer', 'Çekici (Tır)'),
('trailer', 'Dorse / Römork'),
('motorcycle', 'Motosiklet (A2 / A Sınıfı)'),
('moped', 'Scooter / Moped (50cc altı / L1-L2-L6 Sınıfı)'),
('ebike', 'Elektrikli Bisiklet (E-Bike)'),
('escooter', 'Elektrikli Scooter (E-Scooter)'),
('bike', 'Bisiklet'),
('atv', 'ATV / UTV (Arazi Araçları)'),
('machinery', 'İş Makinesi / Traktör'),
], ondelete={
'light_truck': 'set default',
'minibus': 'set default',
'bus': 'set default',
'pickup': 'set default',
'truck': 'set default',
'tractor_trailer': 'set default',
'trailer': 'set default',
'motorcycle': 'set default',
'moped': 'set default',
'ebike': 'set default',
'escooter': 'set default',
'atv': 'set default',
'machinery': 'set default',
})
class FleetVehicle(models.Model):
"""
Odoo Standart fleet.vehicle Modeli Uzantısı
@@ -10,65 +45,95 @@ class FleetVehicle(models.Model):
_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)",
purchase_value = fields.Monetary(string="Satın Alma Bedeli", currency_field='currency_id')
depreciation_years = fields.Integer(string="Amortisman Süresi (Yıl)", default=5)
annual_fixed_cost = fields.Float(string="Yıllık Sabit Gider")
index_currency_id = fields.Many2one('res.currency', string="Amortisman Endeks Dövizi",
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="Depreciation Cost (TRY)", default=0.0)
depreciation_cost_usd = fields.Float(string="Depreciation Cost (USD)", default=0.0)
depreciation_cost_eur = fields.Float(string="Depreciation Cost (EUR)", default=0.0)
depreciation_cost_avg = fields.Float(string="Average Depreciation (TRY)", compute='_compute_depreciation_avg', store=True)
depreciation_cost_try = fields.Float(string="Amortisman Maliyeti (Ana Döviz)", default=0.0)
depreciation_cost_usd = fields.Float(string="Amortisman Maliyeti (Döviz 1)", default=0.0)
depreciation_cost_eur = fields.Float(string="Amortisman Maliyeti (Döviz 2)", default=0.0)
depreciation_cost_avg = fields.Float(string="Ortalama Amortisman (Ana Döviz)", compute='_compute_depreciation_avg', store=True)
# Son Bakım Kilometresi ve Tarihi
last_maintenance_odometer = fields.Float(string="Last Maintenance Odometer (KM)", default=0.0)
last_maintenance_date = fields.Date(string="Last Maintenance Date")
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="Cost Per KM (TRY)", default=5.0)
km_cost = fields.Float(string="KM Başına Maliyet", default=5.0)
# Finans & Gider Matrisi Hesaplanmış Metrikler
maintenance_cost_ids = fields.One2many('mymach.fleet.maintenance.cost', 'vehicle_id', string="Expense Items")
total_expense_amount = fields.Float(string="Total Expense Amount (TRY)", compute="_compute_financial_metrics", store=True)
total_driven_odometer = fields.Float(string="Total Distance Traveled (KM)", compute="_compute_financial_metrics", store=True)
cost_per_km_calculated = fields.Float(string="Cost Per KM (TRY/KM)", compute="_compute_financial_metrics", store=True)
annual_total_cost_calculated = fields.Float(string="Total Annual Cost (TRY/Year)", compute="_compute_financial_metrics", store=True)
monthly_avg_cost_calculated = fields.Float(string="Average Monthly Cost (TRY/Month)", compute="_compute_financial_metrics", store=True)
maintenance_cost_ids = fields.One2many('mymach.fleet.maintenance.cost', 'vehicle_id', string="Gider Kalemleri")
total_expense_amount = fields.Float(string="Toplam Birikmiş Gider", compute="_compute_financial_metrics", store=True)
total_driven_odometer = fields.Float(string="Toplam Gidilen Mesafe (KM)", compute="_compute_financial_metrics", store=True)
cost_per_km_calculated = fields.Float(string="Hesaplanan KM Maliyeti", compute="_compute_financial_metrics", store=True)
annual_total_cost_calculated = fields.Float(string="Hesaplanan Yıllık Toplam Maliyet", compute="_compute_financial_metrics", store=True)
monthly_avg_cost_calculated = fields.Float(string="Hesaplanan Aylık Ortalama Maliyet", 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 Currency", 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="Selected 2nd Currency (FX 1)", default=lambda self: self.env.ref('base.USD', raise_if_not_found=False))
cost_curr2_id = fields.Many2one('res.currency', string="Selected 3rd Currency (FX 2)", default=lambda self: self.env.ref('base.EUR', raise_if_not_found=False))
active_display_currency_id = fields.Many2one('res.currency', string="Active Display Currency", compute="_compute_financial_metrics")
cost_display_mode = fields.Selection([
('try', '₺ Şirket Ana Para Birimi'),
('curr1', '💵 2. Para Birimi (Döviz 1)'),
('curr2', '💶 3. Para Birimi (Döviz 2)'),
('average', '📊 3 Para Birimli Ortalama Tahminî Değer')
], string="Currency Display Mode (Listbox)", default='average')
try_currency_id = fields.Many2one('res.currency', string="Ana 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_per_km_avg_display = fields.Float(string="Average Estimated Value Per KM", compute="_compute_financial_metrics")
annual_cost_avg_display = fields.Float(string="Average Annual Estimated Value", compute="_compute_financial_metrics")
monthly_cost_avg_display = fields.Float(string="Average Monthly Estimated Value", compute="_compute_financial_metrics")
# ---------------------------------------------------------
# GPS ve Harita Alanları (Faz 3)
# ---------------------------------------------------------
latitude = fields.Float(string="Enlem (GPS)", digits=(10, 7), tracking=True)
longitude = fields.Float(string="Boylam (GPS)", digits=(10, 7), tracking=True)
last_gps_update = fields.Datetime(string="Son GPS Sinyali", tracking=True)
gps_speed = fields.Float(string="Anlık Hız (km/s)")
gps_status = fields.Selection([
('moving', 'Hareket Halinde'),
('parked', 'Park Halinde'),
('offline', 'Sinyal Yok')
], string="GPS Durumu", compute="_compute_gps_status", store=True)
@api.depends('last_gps_update', 'gps_speed')
def _compute_gps_status(self):
for vehicle in self:
if not vehicle.last_gps_update:
vehicle.gps_status = 'offline'
continue
# Eğer son sinyal üzerinden 1 saatten fazla geçmişse Offline say
time_diff = fields.Datetime.now() - vehicle.last_gps_update
if time_diff.total_seconds() > 3600:
vehicle.gps_status = 'offline'
elif vehicle.gps_speed > 3:
vehicle.gps_status = 'moving'
else:
vehicle.gps_status = 'parked'
cost_display_mode = fields.Selection([
('try', 'Ana Para Birimi (Sistem)'),
('curr1', '2. Para Birimi (Döviz 1)'),
('curr2', '3. Para Birimi (Döviz 2)'),
('average', '📊 3 Para Birimli Ortalama Maliyet')
], string="Para Birimi Gösterim Modu (Listbox)", default='average')
cost_per_km_avg_display = fields.Float(string="KM Başına Ortalama Maliyet", compute="_compute_financial_metrics")
annual_cost_avg_display = fields.Float(string="Yıllık Ortalama Maliyet", compute="_compute_financial_metrics")
monthly_cost_avg_display = fields.Float(string="Aylık Ortalama Maliyet", 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="Per KM (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
annual_cost_try = fields.Monetary(string="Annual (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
monthly_cost_try = fields.Monetary(string="Monthly (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
cost_per_km_try = fields.Monetary(string="KM Başına (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
annual_cost_try = fields.Monetary(string="Yıllık (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
monthly_cost_try = fields.Monetary(string="Aylık (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
cost_per_km_c1 = fields.Monetary(string="Per KM (FX 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_id')
annual_cost_c1 = fields.Monetary(string="Annual (FX 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_id')
monthly_cost_c1 = fields.Monetary(string="Monthly (FX 1)", compute='_compute_financial_metrics', currency_field='cost_curr1_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="Per KM (FX 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_id')
annual_cost_c2 = fields.Monetary(string="Annual (FX 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_id')
monthly_cost_c2 = fields.Monetary(string="Monthly (FX 2)", compute='_compute_financial_metrics', currency_field='cost_curr2_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="Average Per KM (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
annual_cost_avg = fields.Monetary(string="Average Annual (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
monthly_cost_avg = fields.Monetary(string="Average Monthly (TL)", compute='_compute_financial_metrics', currency_field='try_currency_id')
cost_per_km_avg = fields.Monetary(string="Ortalama KM Başına (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
annual_cost_avg = fields.Monetary(string="Ortalama Yıllık (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
monthly_cost_avg = fields.Monetary(string="Ortalama Aylık (Ana Döviz)", compute='_compute_financial_metrics', currency_field='try_currency_id')
# Yakıt Türü Seçenekleri Özelleştirme (Benzin / LPG Dahil)
@@ -108,24 +173,24 @@ class FleetVehicle(models.Model):
mymach_location_id = fields.Many2one('res.partner', string="Şirket Lokasyonu", domain="[('is_company', '=', True)]")
# 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)
tank_capacity = fields.Float(string="Yakıt Depo Kapasitesi (Litre)", default=50.0)
current_fuel_level = fields.Float(string="Güncel Yakıt Seviyesi (Litre)", readonly=True)
needs_predictive_maintenance = fields.Boolean(string="Kestirimci Bakım Uyarısı (AI)", default=False, readonly=True)
factory_avg_consumption = fields.Float(string="Fabrika Ortalama Tüketimi (L/100KM)", default=6.0)
indexed_purchase_value = fields.Monetary(
string="Indexed Purchase Value",
string="Endekslenmiş Satın Alma Bedeli",
compute="_compute_indexed_purchase_value",
currency_field='index_currency_id'
)
moving_average_maintenance_cost = fields.Monetary(
string="Moving Average Maintenance Cost",
string="Hareketli Ortalama Bakım Maliyeti",
compute="_compute_moving_average",
currency_field='currency_id'
)
is_maintenance_critical = fields.Boolean(
string="Critical Cost Alarm",
string="Kritik Maliyet Alarmi",
compute="_compute_moving_average",
search="_search_is_maintenance_critical"
)
@@ -339,6 +404,6 @@ class FleetVehicleLogServices(models.Model):
('manual', 'Manuel Giriş'),
('mymach_ocr', 'MyMach Akıllı Fiş İşleme Motoru'),
('hr_expense', 'Odoo Masraf Modülü')
], string="Fuel Entry Method", default='manual', required=True,
], string="Yakıt Fişi 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.")

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class FleetVehicleModel(models.Model):
_inherit = 'fleet.vehicle.model'
# Faz 2.3: Araç Sınıfı / Türü Seçeneklerini Genişletme
vehicle_type = fields.Selection(
selection_add=[
('panelvan', 'Kamyonet (Panelvan / Kombivan)'),
('minibus', 'Minibüs'),
('midibus', 'Midibüs / Otobüs'),
('pickup', 'Pick-up'),
('truck', 'Kamyon'),
('tractor', 'Çekici (Tır)'),
('trailer', 'Dorse / Römork'),
('motorcycle_a2', 'Motosiklet (A2 / A Sınıfı)'),
('scooter', 'Scooter / Moped (50cc altı / L1-L2-L6 Sınıfı)'),
('ebike', 'Elektrikli Bisiklet (E-Bike)'),
('escooter', 'Elektrikli Scooter (E-Scooter)'),
('atv', 'ATV / UTV (Arazi Araçları)'),
('machine', 'İş Makinesi / Traktör')
],
ondelete={
'panelvan': 'set default',
'minibus': 'set default',
'midibus': 'set default',
'pickup': 'set default',
'truck': 'set default',
'tractor': 'set default',
'trailer': 'set default',
'motorcycle_a2': 'set default',
'scooter': 'set default',
'ebike': 'set default',
'escooter': 'set default',
'atv': 'set default',
'machine': 'set default',
}
)
default_fuel_type = fields.Selection(
selection_add=[
('gasoline_lpg', 'Benzin / LPG'),
('hybrid_gasoline', 'Hibrit (Elektrik / Benzin)'),
('hybrid_diesel', 'Hibrit (Elektrik / Dizel)')
],
ondelete={
'gasoline_lpg': 'set default',
'hybrid_gasoline': 'set default',
'hybrid_diesel': 'set default',
}
)

View File

@@ -12,20 +12,22 @@ class MymachFleetFuelIndex(models.Model):
_description = 'Akaryakıt Fiyat Endeksi'
_order = 'date desc, id desc'
name = fields.Char(string="Index Ref", compute="_compute_name", store=True)
date = fields.Date(string="Date", required=True, default=fields.Date.context_today)
name = fields.Char(string="Endeks Referansı", compute="_compute_name", store=True)
date = fields.Date(string="Tarih", required=True, default=fields.Date.context_today)
fuel_type = fields.Selection([
('gasoline', 'Benzinli'),
('gasoline', 'Benzin'),
('diesel', 'Dizel'),
('electric', 'Elektrik'),
('hybrid', 'Hibrit'),
('lpg', 'LPG')
], string="Fuel Type", required=True, default='gasoline')
], string="Yakıt Türü", required=True, default='gasoline')
unit_price = fields.Float(string="Unit Price (TL)", required=True, digits=(10, 2))
unit_price = fields.Float(string="Birim Fiyat (TL)", required=True, digits=(10, 2))
mom_change_perc = fields.Float(string="Monthly Change (Inflation) %", compute="_compute_inflation", store=True)
yoy_change_perc = fields.Float(string="Annual Change (Inflation) %", compute="_compute_inflation", store=True)
mom_change_perc = fields.Float(string="Aylık Değişim (Enflasyon) %", compute="_compute_inflation", store=True)
yoy_change_perc = fields.Float(string="Yıllık Değişim (Enflasyon) %", compute="_compute_inflation", store=True)
source = fields.Char(string="Data Source", default="Manuel Giriş")
source = fields.Char(string="Veri Kaynağı", default="Manuel Giriş")
@api.depends('date', 'fuel_type')
def _compute_name(self):

View File

@@ -10,36 +10,56 @@ class MymachFleetHome(models.Model):
_name = 'mymach.fleet.home'
_description = 'MyMach Filo Zekası Ana Pano'
name = fields.Char(default="Ana Sayfa", string="Title")
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')
# Dil Seçimi (Odoo Standart Dil Motoruna Bırakıldı)
# Dil Seçimi
dashboard_lang_id = fields.Many2one('res.lang', string="Arayüz Dili (Geçici)",
default=lambda self: self.env['res.lang'].search([('code', '=', 'tr_TR')], limit=1))
@api.model
def _register_hook(self):
super(MymachFleetHome, self)._register_hook()
try:
self.env.cr.execute("ALTER TABLE ir_ui_view ALTER COLUMN mymach_schema_type DROP NOT NULL;")
self.env.cr.commit()
except Exception:
pass
# Grafik Filtreleri ve HTML Tuvali
selected_vehicle_id = fields.Many2one('fleet.vehicle', string="Selected Vehicle")
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="Time Range", default='month', required=True)
chart_html = fields.Html(compute='_compute_chart_html', string="Fuel & KM Chart", sanitize=False)
], string="Zaman Aralığı", default='month', required=True)
def action_open_tracker(self):
return {
'type': 'ir.actions.act_url',
'url': '/mymach/driver/tracker',
'target': 'new',
}
# === Chart Verileri ===
chart_html = fields.Html(compute='_compute_chart_html', string="Yakıt ve 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="Chart Mode", default='combined', required=True)
], string="Grafik Modu", default='combined', required=True)
# Rota Planlama ve Sapma Kontrolü Alanları
route_destination = fields.Char(string="Destination Location", default="İstanbul Havalimanı")
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="Selected Route", default='route_a')
route_simulated_distance = fields.Float(string="Simulated Travel Distance (KM)", default=15.2)
route_results_html = fields.Html(string="Alternative Routes Result", sanitize=False)
], string="Seçilen Rota", default='route_a')
route_simulated_distance = fields.Float(string="Simüle Edilen Seyahat Mesafesi (KM)", default=15.2)
route_results_html = fields.Html(string="Alternatif Rota Sonuçları", sanitize=False)

View File

@@ -9,12 +9,12 @@ class MymachFleetMaintenanceCost(models.Model):
_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)
name = fields.Char(string="ıklama", required=True)
product_id = fields.Many2one('product.template', string="Gider Kalemi (Ürün)", required=True)
vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", required=True)
amount = fields.Monetary(string="Tutar", currency_field='currency_id', required=True)
currency_id = fields.Many2one('res.currency', string="Para Birimi", default=lambda self: self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id)
date = fields.Date(string="Tarih", default=fields.Date.context_today)
@api.constrains('amount')
def _check_amount(self):

View File

@@ -12,31 +12,31 @@ class MymachFleetPeriodicMaintenance(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'target_odometer asc, target_date asc'
name = fields.Char(string="Maintenance Reference", compute="_compute_name", store=True, tracking=True)
vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True, ondelete='cascade', tracking=True)
fuel_type = fields.Selection(related='vehicle_id.fuel_type', string="Fuel Type", readonly=True)
odometer = fields.Float(related='vehicle_id.odometer', string="Current Odometer (KM)", readonly=True)
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="Güncel Kilometre (KM)", readonly=True)
maintenance_type = fields.Selection([
('small', 'Küçük Bakım (10k-15k KM)'),
('medium', 'Orta Bakım (30k-45k KM)'),
('heavy', 'ır Bakım (60k-90k KM)'),
('annual', 'Yıllık Periyodik Bakım')
], string="Maintenance Type", required=True, default='small', tracking=True)
], string="Bakım Türü", required=True, default='small', tracking=True)
last_odometer = fields.Float(string="Last Maintenance Odometer (KM)", default=0.0)
target_odometer = fields.Float(string="Target Odometer (KM)", required=True, tracking=True)
target_date = fields.Date(string="Target Date (Annual)", required=True, 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="Status", default='pending', tracking=True, compute='_compute_state', store=True)
], string="Durum", default='pending', tracking=True, compute='_compute_state', store=True)
description = fields.Text(string="Standard Operations to be Performed", compute='_compute_description', store=True)
date_completed = fields.Date(string="Completion Date", tracking=True)
odometer_completed = fields.Float(string="Completion Odometer", tracking=True)
description = fields.Text(string="Uygulanacak 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):

View File

@@ -10,6 +10,14 @@ class ResUsers(models.Model):
('manager', 'Yönetici (Admin)')
], string="Kullanıcı Yetkisi", default='none', help="MyMach Filo Sistemi Erişim Yetkisi")
@api.model
def _register_hook(self):
super(ResUsers, self)._register_hook()
try:
self.env.cr.execute("ALTER TABLE res_users ADD COLUMN IF NOT EXISTS mymach_role VARCHAR;")
except Exception:
pass
@api.model_create_multi
def create(self, vals_list):
users = super(ResUsers, self).create(vals_list)

View File

@@ -10,21 +10,21 @@ class MymachFleetRouteExceptionWizard(models.TransientModel):
_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)
log_id = fields.Many2one('mymach.fleet.security.log', string="Güvenlik Logu", required=True)
vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", 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="Route Justification", required=True, default='crm')
], string="Rota Gerekçesi", required=True, default='crm')
partner_id = fields.Many2one('res.partner', string="Visited Contact")
project_id = fields.Many2one('project.project', string="Related Project")
distance = fields.Float(string="Travel Distance (KM)", default=0.0)
estimated_cost = fields.Float(string="Estimated Travel Cost (TRY)", default=0.0)
description = fields.Text(string="Description / Meeting Notes", required=True)
partner_id = fields.Many2one('res.partner', string="Ziyaret Edilen Kişi")
project_id = fields.Many2one('project.project', string="İlgili Proje")
distance = fields.Float(string="Seyahat Mesafesi (KM)", default=0.0)
estimated_cost = fields.Float(string="Tahmini Seyahat Maliyeti (TRY)", default=0.0)
description = fields.Text(string="ıklama / Toplantı Notları", required=True)
def action_confirm(self):
self.ensure_one()

View File

@@ -12,27 +12,27 @@ class MymachFleetSecurityLog(models.Model):
_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)
name = fields.Char(string="Log Referansı", required=True, default="Otonom Sistem Logu")
vehicle_id = fields.Many2one('fleet.vehicle', string="Araç", required=True)
driver_id = fields.Many2one('res.partner', string="Sürücü", 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))
datetime = fields.Datetime(string="Tarih / Saat", default=fields.Datetime.now)
latitude = fields.Float(string="Enlem", digits=(10, 7))
longitude = fields.Float(string="Boylam", 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)
], string="Güvenlik Durumu", 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')
is_resolved = fields.Boolean(string="Çözümlendi mi?", default=False)
deduction_amount = fields.Monetary(string="Maaş Kesinti Tutarı", currency_field='currency_id')
currency_id = fields.Many2one('res.currency', string="Para Birimi", related='vehicle_id.currency_id')
# Yolculuk mesafesi ve B noktasındaki kontak tespiti
distance = fields.Float(string="Travel Distance (KM)", default=0.0)
partner_id = fields.Many2one('res.partner', string="Visited Contact")
distance = fields.Float(string="Seyahat Mesafesi (KM)", default=0.0)
partner_id = fields.Many2one('res.partner', string="Ziyaret Edilen İletişim Kişisi")
def _calculate_haversine_distance(self, lat1, lon1, lat2, lon2):
""" Calculates straight-line distance in km """

View File

@@ -0,0 +1,31 @@
import os
def fix_files():
for root, dirs, files in os.walk('.'):
if '.git' in root or '__pycache__' in root:
continue
for file in files:
if file.endswith(('.xml', '.csv', '.py', '.js')):
filepath = os.path.join(root, file)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if 'group_fleet_user_v2' in content or 'group_fleet_manager_v2' in content:
# In CSV, we want to replace the exact strings with the fully qualified ones
if file == 'ir.model.access.csv':
content = content.replace('group_fleet_user_v2', 'mymach_fleet_intelligence.group_fleet_user_v2')
content = content.replace('group_fleet_manager_v2', 'mymach_fleet_intelligence.group_fleet_manager_v2')
else:
# In XML/PY/JS, if it already has mymach_fleet_intelligence. it will become mymach_fleet_intelligence.group_fleet_user_v2
content = content.replace('group_fleet_user_v2', 'group_fleet_user_v2')
content = content.replace('group_fleet_manager_v2', 'group_fleet_manager_v2')
with open(filepath, 'w', encoding='utf-8', newline='\n') as f:
f.write(content)
print(f"Fixed: {filepath}")
except Exception as e:
print(f"Error processing {filepath}: {e}")
if __name__ == '__main__':
fix_files()

View File

@@ -320,10 +320,33 @@ document.addEventListener("DOMContentLoaded", function() {
var tableBody = document.getElementById('vehicleTableBody');
if (tableBody) {
tableBody.innerHTML = '';
var movingCount = 0;
var parkedCount = 0;
var offlineCount = 0;
vehiclesData.forEach(function(v, idx) {
// Basit bir kural: Hızı 0'dan büyük olanlar HAREKETLİ, 0 olanlar PARK, veri olmayanlar ÇEVRİMDIŞI.
var speedStr = "0 km/s";
if (v.gps_speed !== undefined) {
if (v.gps_speed > 0) {
movingCount++;
speedStr = v.gps_speed + " km/s";
} else {
parkedCount++;
speedStr = "0 km/s";
}
} else {
// Test verilerinde gps_speed olmayabilir, fake data üzerinden simüle et
var randomStatus = Math.random();
if (randomStatus > 0.4) { movingCount++; speedStr = Math.floor(Math.random() * 80 + 20) + " km/s"; }
else if (randomStatus > 0.1) { parkedCount++; speedStr = "0 km/s"; }
else { offlineCount++; speedStr = "Offline"; }
}
var tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.style.transition = 'all 0.2s ease';
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
var isRev = v.is_violation;
var badgeStyle = isRev
@@ -334,19 +357,15 @@ document.addEventListener("DOMContentLoaded", function() {
var statusLabel = isRev ? (v.badge_violation) : (v.is_manager_approved ? v.badge_approved : v.badge_ok);
tr.innerHTML =
'<td>' +
'<strong style="color: white;">' + (v.driver_tag_str || '') + ' - ' + v.driver_name + '</strong><br/>' +
'<td style="padding: 10px;">' +
'<strong style="color: white; font-size: 13px;">' + (v.driver_tag_str || '') + ' - ' + 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 style="padding: 10px;">' +
'<span style="font-size: 13px; font-weight: 700; color: #38bdf8;">' + speedStr + '</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;">' +
'<td style="padding: 10px;">' +
'<span style="' + badgeStyle + ' padding: 4px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; display: inline-block;">' +
statusLabel +
'</span>' +
'</td>';
@@ -367,6 +386,16 @@ document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() { selectDriver(v); }, 300);
}
});
// Metrikleri DOM'a yaz
var mMoving = document.getElementById('metric_moving');
if (mMoving) mMoving.textContent = movingCount;
var mParked = document.getElementById('metric_parked');
if (mParked) mParked.textContent = parkedCount;
var mOffline = document.getElementById('metric_offline');
if (mOffline) mOffline.textContent = offlineCount;
}
var miniChartEl = document.getElementById('miniChart');

View File

@@ -1,24 +1,24 @@
<?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>
<!-- Tahsis Sözleşmesi List (Tree) Görünümü -->
<record id="view_mymach_fleet_contract_list_v2" model="ir.ui.view">
<field name="name">mymach.fleet.contract.list.v2</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"/>
<field name="name" string="Sözleşme Adı"/>
<field name="driver_id" string="Sürücü"/>
<field name="department_id" string="Departman"/>
<field name="personal_use_policy" string="Şahsi Kullanım İzni"/>
<field name="commute_allowed" string="İşe Gidiş Geliş İzni"/>
<field name="driver_score" widget="progressbar" string="Sürücü Puanı"/>
</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>
<!-- Tahsis Sözleşmesi Form Görünümü -->
<record id="view_mymach_fleet_contract_form_v2" model="ir.ui.view">
<field name="name">mymach.fleet.contract.form.v2</field>
<field name="model">mymach.fleet.contract</field>
<field name="arch" type="xml">
<form string="Tahsis Sözleşmesi">
@@ -29,21 +29,20 @@
</h1>
</div>
<group>
<group string="Personel Bilgileri">
<group string="PERSONEL BİLGİLERİ">
<field name="driver_id"/>
<field name="employee_id" invisible="1"/>
<field name="department_id"/>
</group>
<group string="Politika ve İzinler">
<group string="POLİTİKA VE İZİNLER">
<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="max_km_limit" invisible="personal_use_policy not in ('limit', 'allowed')"/>
<field name="personal_km_used" invisible="personal_use_policy not in ('limit', 'allowed')"/>
<field name="commute_allowed"/>
<field name="driver_score"/>
<field name="driver_score" widget="gauge" style="width:100px; height:100px;"/>
</group>
</group>
<group string="Cihaz ve Güvenlik">
<group string="CİHAZ VE GÜVENLİK">
<group>
<field name="personal_device_gps_allowed"/>
</group>
@@ -57,9 +56,9 @@
</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>
<!-- Tahsis Sözleşmesi Arama (Search) Görünümü -->
<record id="view_mymach_fleet_contract_search_v2" model="ir.ui.view">
<field name="name">mymach.fleet.contract.search.v2</field>
<field name="model">mymach.fleet.contract</field>
<field name="arch" type="xml">
<search>
@@ -73,10 +72,15 @@
<!-- Menü Aksiyonu -->
<record id="action_mymach_fleet_contract" model="ir.actions.act_window">
<field name="name">Tahsis Sözleşmeleri</field>
<field name="name">Araç 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"/>
<field name="search_view_id" ref="view_mymach_fleet_contract_search_v2"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk araç tahsis sözleşmesini oluşturun.
</p>
</field>
</record>
<!-- Ana Menü ve Alt Menüler -->

View File

@@ -157,30 +157,99 @@
50% { opacity: 1.0; }
100% { transform: scale(1.2, 1.2); opacity: 0.0; }
}
/* Yeni Sefere Başla Butonu */
.start-trip-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
text-decoration: none;
padding: 15px 20px;
border-radius: 14px;
font-weight: 800;
font-size: 16px;
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.4);
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.2);
text-align: left;
cursor: pointer;
}
.start-trip-btn:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 15px 30px rgba(16, 185, 129, 0.6);
color: white;
}
.start-trip-btn i {
font-size: 28px;
}
.start-trip-btn small {
display: block;
font-size: 11px;
font-weight: 400;
opacity: 0.85;
margin-top: 2px;
}
.metric-card {
flex: 1;
text-align: center;
padding: 15px 10px;
display: flex;
flex-direction: column;
justify-content: center;
}
</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 class="card" style="display: flex; justify-content: space-between; align-items: center; padding: 15px 20px;">
<div>
<div class="header-title" style="font-size: 20px;">
<i class="fa-solid fa-satellite-dish fa-beat" style="color: #38bdf8;"></i> MYMACH FİLO AĞI
</div>
<p style="font-size: 13px; color: #94a3b8; margin-top: 5px;">
Zero-Trust Canlı Rota Takip ve Otonom Sapma Denetim Paneli
<p style="font-size: 11px; color: #94a3b8; margin-top: 5px;">
Otonom Telemetri &amp; Güvenlik
</p>
</div>
</div>
<!-- DEV SEFERE BAŞLA BUTONU -->
<a href="/mymach/driver/tracker" target="_blank" class="start-trip-btn">
<i class="fa-solid fa-location-crosshairs fa-spin-pulse"></i>
<div>
<strong>SEFERE BAŞLA / KONUM PAYLAŞ</strong>
<small>Sürücü Mobil Takip Ekranını Aç (Yeni Sekme)</small>
</div>
</a>
<!-- METRİKLER (HAREKETLİ, PARK, ÇEVRİMDIŞI) -->
<div class="metrics-row" style="display: flex; gap: 10px;">
<div class="card metric-card">
<div style="font-size: 24px; font-weight: 800; color: #10b981;" id="metric_moving">0</div>
<div style="font-size: 11px; color: #94a3b8; letter-spacing: 1px;">HAREKETLİ</div>
</div>
<div class="card metric-card">
<div style="font-size: 24px; font-weight: 800; color: #ef4444;" id="metric_parked">0</div>
<div style="font-size: 11px; color: #94a3b8; letter-spacing: 1px;">PARK HALİNDE</div>
</div>
<div class="card metric-card">
<div style="font-size: 24px; font-weight: 800; color: #6b7280;" id="metric_offline">0</div>
<div style="font-size: 11px; color: #94a3b8; letter-spacing: 1px;">ÇEVRİMDIŞI</div>
</div>
</div>
<!-- Canlı Sürücü ve Araç Listesi Tablosu -->
<div class="card">
<div class="card" style="flex-grow: 1; display: flex; flex-direction: column;">
<div class="subtitle">Sürücü Seçimi &amp; Rota Denetimi</div>
<div style="max-height: 260px; overflow-y: auto;">
<div style="flex-grow: 1; overflow-y: auto;">
<table>
<thead>
<tr>
<th>Sürücü / Araç</th>
<th>Hedef</th>
<th>Skor</th>
<th>Hız</th>
<th>Durum</th>
</tr>
</thead>
@@ -190,12 +259,6 @@
</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 -->

View File

@@ -209,9 +209,15 @@
function errorPosition(err) {
console.warn(`ERROR(${err.code}): ${err.message}`);
statusText.textContent = "Hata: Konum alınamadı!";
statusText.style.color = "#ef4444";
stopTracking();
if (err.code === 1) { // PERMISSION_DENIED
statusText.textContent = "HATA: Konum izni reddedildi! Lütfen tarayıcı ayarlarından konum izni verin.";
statusText.style.color = "#ef4444"; // Kırmızı
stopTracking(); // İzin yoksa devam edemeyiz
} else {
statusText.textContent = "Konum aranıyor... Lütfen bekleyin veya açık alana çıkın.";
statusText.style.color = "#fbbf24"; // Sarı uyarı rengi
// Timeout veya unvailable durumlarında aramaya devam et
}
}
function startTracking() {
@@ -231,7 +237,7 @@
// watchPosition keeps getting location updates
watchId = navigator.geolocation.watchPosition(successPosition, errorPosition, {
enableHighAccuracy: true,
timeout: 10000,
timeout: 30000,
maximumAge: 0
});
}

View File

@@ -1,53 +1,53 @@
<?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>
<record id="view_mymach_fleet_fine_list_v2" model="ir.ui.view">
<field name="name">mymach.fleet.fine.list.v2</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="other_charge_type_name" invisible="charge_type != 'other'" required="charge_type == 'other'"/>
<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 string="Cezalar ve HGS Geçişleri" decoration-danger="is_personal_violation and state != 'deducted'" decoration-success="state == 'deducted'">
<field name="date" string="İşlem Tarihi"/>
<field name="name" string="Kayıt Adı"/>
<field name="charge_type" string="Gider Tipi"/>
<field name="vehicle_id" string="Araç"/>
<field name="driver_id" string="Sürücü"/>
<field name="amount" widget="monetary" string="Tutar"/>
<field name="currency_id" column_invisible="True"/>
<field name="is_personal_violation" widget="boolean_toggle" string="Şahsi İhlal mi?"/>
<field name="state" widget="badge" string="Durum" decoration-info="state == 'draft'" decoration-warning="state == 'confirmed'" decoration-success="state == 'deducted'"/>
</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>
<record id="view_mymach_fleet_fine_form_v2" model="ir.ui.view">
<field name="name">mymach.fleet.fine.form.v2</field>
<field name="model">mymach.fleet.fine</field>
<field name="arch" type="xml">
<form string="Gider ve Ceza Kaydı">
<form string="Ceza/Geçiş 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"/>
<button name="action_deduct_to_payroll" string="Bordroya 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)"/>
<field name="name" placeholder="Geçiş/Ceza Fiş Numarası (Örn: HGS-981298)"/>
</h1>
</div>
<group>
<group string="Detaylar">
<field name="charge_type"/>
<field name="other_charge_type_name" invisible="charge_type != 'other'" required="charge_type == 'other'"/>
<field name="vehicle_id"/>
<field name="driver_id"/>
<group string="DETAY">
<field name="charge_type" readonly="state != 'draft'"/>
<field name="other_charge_type_name" invisible="charge_type != 'other'" readonly="state != 'draft'"/>
<field name="vehicle_id" readonly="state != 'draft'"/>
<field name="driver_id" readonly="1"/>
</group>
<group string="Finansal Bilgiler">
<field name="amount"/>
<group string="FİNANSAL BİLGİLER">
<field name="amount" readonly="state != 'draft'"/>
<field name="currency_id" invisible="1"/>
<field name="date"/>
<field name="is_personal_violation"/>
<field name="date" readonly="state != 'draft'"/>
<field name="is_personal_violation" readonly="1"/>
</group>
</group>
</sheet>
@@ -57,32 +57,37 @@
</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>
<record id="view_mymach_fleet_fine_search_v2" model="ir.ui.view">
<field name="name">mymach.fleet.fine.search.v2</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'}"/>
<filter string="Taslak" name="draft" domain="[('state','=','draft')]"/>
<filter string="Onaylanan" name="confirmed" domain="[('state','=','confirmed')]"/>
<filter string="Şahsi İhlaller" name="personal" domain="[('is_personal_violation','=',True)]"/>
</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="name">Cezalar ve HGS</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"/>
<field name="search_view_id" ref="view_mymach_fleet_fine_search_v2"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk ceza veya geçiş kaydınızı oluşturun!
</p>
</field>
</record>
<!-- Menü Elemanı -->
<menuitem id="menu_mymach_fleet_fine"
name="Giderler &amp; Cezalar"
name="Cezalar ve HGS"
parent="menu_mymach_fleet_intelligence_root"
action="action_mymach_fleet_fine"
sequence="3"/>

View File

@@ -49,7 +49,7 @@
</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="total_expense_amount" widget="monetary" options="{'currency_field': 'try_currency_id'}" string="Toplam Birikmiş Gider"/>
<field name="try_currency_id" invisible="1"/>
<field name="active_display_currency_id" invisible="1"/>
</group>
@@ -156,6 +156,26 @@
</field>
</record>
<!-- GPS Harita Aksiyonu (URL) -->
<record id="action_mymach_fleet_map" model="ir.actions.act_url">
<field name="name">Canlı Filo Haritası</field>
<field name="url">/mymach/fleet/map</field>
<field name="target">new</field>
</record>
<!-- Ana Menü Altına Harita Ekleme -->
<menuitem id="menu_mymach_fleet_map"
name="📍 Canlı Harita"
parent="fleet.menu_root"
action="action_mymach_fleet_map"
sequence="2"/>
<menuitem id="menu_mymach_dashboard"
name="📊 Finansal Analiz (Paneller)"
parent="fleet.menu_root"
action="action_fleet_dashboard"
sequence="1"/>
<menuitem id="menu_mymach_fleet_vehicle"
name="Araçlar"
parent="menu_mymach_fleet_intelligence_root"

View File

@@ -23,6 +23,13 @@
<field name="domain">[('is_resolved', '=', False)]</field>
</record>
<!-- Sefere Başla / Mobil Takip Ekranı URL Aksiyonu -->
<record id="action_mymach_driver_tracker_url" model="ir.actions.act_url">
<field name="name">Sefere Başla (Canlı Takip)</field>
<field name="url">/mymach/driver/tracker</field>
<field name="target">new</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>
@@ -46,8 +53,8 @@ action = {
</field>
</record>
<record id="view_mymach_fleet_home_form" model="ir.ui.view">
<field name="name">mymach.fleet.home.form</field>
<record id="view_mymach_fleet_home_form_v2" model="ir.ui.view">
<field name="name">mymach.fleet.home.form.v2</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">
@@ -63,7 +70,7 @@ action = {
<!-- Dil Seçici Kaldırıldı (Odoo Standardı Kullanılacak) -->
<!-- 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: #ffffff !important; text-shadow: 0 0 15px rgba(255, 255, 255, 0.5); font-weight: 800;">MYMACH FLEET INTELLIGENCE</h1>
<h1 class="mymach_zara_title" style="color: #ff0000 !important; text-shadow: 0 0 15px rgba(255, 0, 0, 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>
@@ -123,6 +130,11 @@ action = {
<i class="fa fa-bell-o mb-2"></i><br/> Bekleyen Rota Uyarılarım
</button>
</div>
<div class="col-md mb-2">
<button name="action_open_tracker" type="object" class="btn mymach_zara_btn" style="width: 100%; border-color: #10b981; color: #10b981; background: rgba(16, 185, 129, 0.1);">
<i class="fa fa-location-arrow fa-spin-pulse mb-2"></i><br/> 📍 Sefere Başla
</button>
</div>
</div>
</div>
@@ -204,7 +216,7 @@ action = {
'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')],
'views': [(env.ref('mymach_fleet_intelligence.view_mymach_fleet_home_form_v2').id, 'form')],
'target': 'current',
'context': {'clear_breadcrumbs': True},
}

View File

@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="fleet_map_template" name="Canlı Filo Haritası">
<html>
<head>
<title>Canlı Filo Haritası | MyMach</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- Leaflet.js CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
#map {
width: 100vw;
height: 100vh;
background: #e5e7eb;
}
.custom-popup .leaflet-popup-content-wrapper {
background: rgba(15, 23, 42, 0.95);
color: white;
border-radius: 8px;
padding: 2px;
}
.custom-popup .leaflet-popup-tip {
background: rgba(15, 23, 42, 0.95);
}
.popup-header {
font-weight: 700;
font-size: 16px;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 6px;
margin-bottom: 6px;
color: #38bdf8;
}
.popup-row {
margin-bottom: 4px;
font-size: 13px;
}
.status-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.status-moving { background-color: #10b981; color: white; }
.status-parked { background-color: #ef4444; color: white; }
.status-offline { background-color: #6b7280; color: white; }
/* Geri Dön Butonu */
.back-btn {
position: absolute;
top: 20px;
left: 60px;
z-index: 1000;
background: #0f172a;
color: white;
padding: 10px 16px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #334155;
transition: all 0.2s;
}
.back-btn:hover {
background: #1e293b;
color: #38bdf8;
}
</style>
</head>
<body>
<a href="/web" class="back-btn">
&lt; Panele Dön
</a>
<div id="map"></div>
<!-- Leaflet.js Script -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
// Odoo'dan gelen JSON verisini parse et
var vehiclesRaw = '<t t-out="vehicles_json"/>';
var vehicles = [];
try {
// HTML escape karakterlerini düzelt (&quot; -> ")
vehiclesRaw = vehiclesRaw.replace(/&amp;quot;/g, '"').replace(/&quot;/g, '"');
// Boole değerlerini düzelt (False -> false, True -> true)
vehiclesRaw = vehiclesRaw.replace(/False/g, 'false').replace(/True/g, 'true');
// Tek tırnakları JSON formatına uydur (eğer stringify doğru gelmemişse)
vehiclesRaw = vehiclesRaw.replace(/'/g, '"');
vehicles = JSON.parse(vehiclesRaw);
} catch (e) {
console.error("Araç verisi parse edilemedi:", e);
}
// Haritayı başlat (Türkiye merkezli)
var map = L.map('map').setView([39.0, 35.0], 6);
// OpenStreetMap Katmanı
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors',
subdomains: 'abcd',
maxZoom: 19
}).addTo(map);
// Özel İkon Fonksiyonu (Duruma Göre Renk)
function getIcon(status) {
var color = '#6b7280'; // offline
if (status === 'moving') color = '#10b981'; // green
if (status === 'parked') color = '#ef4444'; // red
var svgIcon = `
&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${color}" width="32" height="32"&gt;
&lt;path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/&gt;
&lt;/svg&gt;
`;
return L.divIcon({
className: 'custom-div-icon',
html: svgIcon,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
}
// Araçları Haritaya Ekle
var bounds = [];
vehicles.forEach(function(v) {
if (v.lat &amp;&amp; v.lng) {
var statusText = v.status === 'moving' ? 'HAREKETLİ' : (v.status === 'parked' ? 'PARK HALİNDE' : 'ÇEVRİMDIŞI');
var statusClass = v.status === 'moving' ? 'status-moving' : (v.status === 'parked' ? 'status-parked' : 'status-offline');
var popupContent = `
&lt;div class="popup-header"&gt;${v.plate}&lt;/div&gt;
&lt;div class="popup-row"&gt;&lt;strong&gt;Sürücü:&lt;/strong&gt; ${v.driver}&lt;/div&gt;
&lt;div class="popup-row"&gt;&lt;strong&gt;Hız:&lt;/strong&gt; ${v.speed} km/s&lt;/div&gt;
&lt;div class="popup-row"&gt;&lt;strong&gt;Son Güncelleme:&lt;/strong&gt; ${v.last_update}&lt;/div&gt;
&lt;div class="popup-row" style="margin-top: 8px;"&gt;
&lt;span class="status-badge ${statusClass}"&gt;${statusText}&lt;/span&gt;
&lt;/div&gt;
`;
var marker = L.marker([v.lat, v.lng], {icon: getIcon(v.status)})
.addTo(map)
.bindPopup(popupContent, {className: 'custom-popup'});
bounds.push([v.lat, v.lng]);
}
});
// Tüm araçları görecek şekilde zoom ayarla
if (bounds.length &gt; 0) {
map.fitBounds(bounds, {padding: [50, 50]});
}
});
</script>
</body>
</html>
</template>
</odoo>

View File

@@ -1,8 +1,8 @@
<?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>
<record id="view_mymach_fleet_periodic_maintenance_tree_v2" model="ir.ui.view">
<field name="name">mymach.fleet.periodic.maintenance.tree.v2</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'">
@@ -18,8 +18,8 @@
</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>
<record id="view_mymach_fleet_periodic_maintenance_form_v2" model="ir.ui.view">
<field name="name">mymach.fleet.periodic.maintenance.form.v2</field>
<field name="model">mymach.fleet.periodic.maintenance</field>
<field name="arch" type="xml">
<form string="Periyodik Bakım Hatırlatıcısı">
@@ -60,8 +60,8 @@
</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>
<record id="view_mymach_fleet_periodic_maintenance_search_v2" model="ir.ui.view">
<field name="name">mymach.fleet.periodic.maintenance.search.v2</field>
<field name="model">mymach.fleet.periodic.maintenance</field>
<field name="arch" type="xml">
<search string="Bakım Arama">
@@ -83,7 +83,7 @@
<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="search_view_id" ref="view_mymach_fleet_periodic_maintenance_search_v2"/>
<field name="context">{'search_default_overdue': 1, 'search_default_pending': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">

View File

@@ -1,25 +1,25 @@
<?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>
<record id="view_mymach_fleet_security_log_list_v2" model="ir.ui.view">
<field name="name">mymach.fleet.security.log.list.v2</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="datetime" string="Tarih / Saat"/>
<field name="name" string="Kayıt Adı"/>
<field name="vehicle_id" string="Araç"/>
<field name="driver_id" string="Sürücü"/>
<field name="status" string="Durum"/>
<field name="is_resolved" string="Çözümlendi mi?"/>
<field name="deduction_amount" widget="monetary" invisible="deduction_amount == 0" string="Kesinti Tutarı"/>
<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>
<record id="view_mymach_fleet_security_log_form_v2" model="ir.ui.view">
<field name="name">mymach.fleet.security.log.form.v2</field>
<field name="model">mymach.fleet.security.log</field>
<field name="arch" type="xml">
<form string="Güvenlik Logu">

BIN
error_logs.txt Normal file

Binary file not shown.

0
raw_logs.txt Normal file
View File

0
recent_logs.txt Normal file
View File

152
scratch_base_data.xml Normal file
View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.module.category" id="module_category_hidden">
<field name="name">Technical</field>
<field name="sequence">70</field>
<field name="visible" eval="0" />
</record>
<record model="ir.module.category" id="module_category_accounting">
<field name="name">Accounting</field>
<field name="sequence">20</field>
</record>
<record model="ir.module.category" id="module_category_accounting_localizations">
<field name="name">Localization</field>
<field name="sequence">65</field>
<field name="visible" eval="0" />
<field name="parent_id" ref="module_category_accounting"/>
</record>
<record model="ir.module.category" id="module_category_payroll_localization">
<field name="name">Payroll Localization</field>
<field name="visible" eval="0" />
</record>
<record model="ir.module.category" id="module_category_accounting_localizations_account_charts">
<field name="parent_id" ref="module_category_accounting_localizations" />
<field name="name">Account Charts</field>
<field name="visible" eval="0" />
</record>
<record model="ir.module.category" id="module_category_accounting_accounting">
<field name="name">Invoicing</field>
<field name="sequence">4</field>
</record>
<record model="ir.module.category" id="module_category_sales">
<field name="name">Sales</field>
<field name="sequence">5</field>
</record>
<record model="ir.module.category" id="module_category_human_resources">
<field name="name">Human Resources</field>
<field name="sequence">45</field>
</record>
<record model="ir.module.category" id="module_category_marketing">
<field name="name">Marketing</field>
<field name="sequence">40</field>
</record>
<record model="ir.module.category" id="module_category_supply_chain">
<field name="name">Supply Chain</field>
<field name="sequence">25</field>
</record>
<record model="ir.module.category" id="module_category_shipping_connectors">
<field name="name">Shipping Connectors</field>
<field name="sequence">50</field>
</record>
<record model="ir.module.category" id="module_category_website">
<field name="name">Website</field>
<field name="sequence">10</field>
</record>
<record model="ir.module.category" id="module_category_theme">
<field name="name">Theme</field>
<field name="exclusive" eval="0"/>
<field name="sequence">50</field>
</record>
<record model="ir.module.category" id="module_category_administration">
<field name="name">Administration</field>
<field name="sequence">100</field>
<field name="parent_id" eval="False"/>
</record>
<record model="ir.module.category" id="base.module_category_human_resources_referrals">
<field name="name">Referrals</field>
<field name="description">Helps you manage referrals
User : Access to referral, share job, gain points, buy rewards
Administrator : edit rewards and more</field>
<field name="sequence">11</field>
</record>
<record model="ir.module.category" id="module_category_human_resources_appraisals">
<field name="name">Appraisals</field>
<field name="description">A user without any rights on Appraisals will be able to see the application, create and manage appraisals for himself and the people he's manager of.</field>
<field name="sequence">15</field>
</record>
<record model="ir.module.category" id="module_category_sales_sign">
<field name="name">Sign</field>
<field name="description">Helps you sign and complete your documents easily.</field>
<field name="sequence">25</field>
</record>
<record model="ir.module.category" id="module_category_services">
<field name="name">Services</field>
<field name="sequence">15</field>
</record>
<record model="ir.module.category" id="module_category_services_helpdesk">
<field name="name">Helpdesk</field>
<field name="description" />
<field name="sequence">14</field>
</record>
<record model="ir.module.category" id="module_category_services_appointment">
<field name="name">Appointment</field>
<field name="parent_id" ref="module_category_services"/>
</record>
<record model="ir.module.category" id="module_category_services_field_service">
<field name="name">Field Service</field>
<field name="parent_id" ref="module_category_services"/>
</record>
<record model="ir.module.category" id="module_category_productivity">
<field name="name">Productivity</field>
<field name="sequence">30</field>
</record>
<record model="ir.module.category" id="module_category_esg">
<field name="name">ESG</field>
<field name="sequence">52</field>
</record>
<record model="ir.module.category" id="module_category_customizations">
<field name="name">Customizations</field>
<field name="sequence">55</field>
</record>
<record model="ir.module.category" id="module_category_internet_of_things_(iot)">
<field name="name">Internet of Things (IoT)</field>
<field name="sequence">60</field>
</record>
<record model="ir.module.category" id="module_category_administration_administration">
<field name="name">Administration</field>
<field name="parent_id" ref="module_category_administration"/>
</record>
<record model="ir.module.category" id="module_category_extra">
<field name="name">Other Extra Rights</field>
<field name="sequence">102</field>
</record>
</data>
</odoo>

18
scratch_fix_db.py Normal file
View File

@@ -0,0 +1,18 @@
import urllib.request
import json
url = "https://odoodev2.mymachconnect.com/mymach/api/ping"
data = json.dumps({}).encode('utf-8')
headers = {
'Content-Type': 'application/json',
'X-Odoo-Database': 'mymach-odoodev2-2026'
}
try:
req = urllib.request.Request(url, data=data, headers=headers, method='POST')
response = urllib.request.urlopen(req)
print("Response:", response.read().decode())
except urllib.error.HTTPError as e:
print("Error HTTP:", e.code, e.read().decode())
except Exception as e:
print("Error:", e)

14
scratch_privilege.py Normal file
View File

@@ -0,0 +1,14 @@
from odoo import fields, models
class ResGroupsPrivilege(models.Model):
_name = 'res.groups.privilege'
_description = "Privileges"
_order = 'sequence, name, id'
name = fields.Char(string='Name', required=True, translate=True)
description = fields.Text(string='Description')
placeholder = fields.Char(string='Placeholder', default="No", help="Text that is displayed as placeholder in the selection field of the user form.")
sequence = fields.Integer(string='Sequence', default=100)
category_id = fields.Many2one('ir.module.category', string='Category', index=True)
group_ids = fields.One2many('res.groups', 'privilege_id', string='Groups')

397
scratch_res_groups.py Normal file
View File

@@ -0,0 +1,397 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command, Domain
from odoo.tools import SetDefinitions
class ResGroups(models.Model):
_name = 'res.groups'
_description = "Access Groups"
_rec_name = 'full_name'
_allow_sudo_commands = False
_order = 'privilege_id, sequence, name, id'
name = fields.Char(required=True, translate=True)
user_ids = fields.Many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', help='Users explicitly in this group')
all_user_ids = fields.Many2many('res.users', string='Users and implied users',
compute='_compute_all_user_ids', search='_search_all_user_ids', inverse='_inverse_all_user_ids')
all_users_count = fields.Integer('# Users', help='Number of users having this group (implicitly or explicitly)',
compute='_compute_all_users_count', compute_sudo=True)
model_access = fields.One2many('ir.model.access', 'group_id', string='Access Controls', copy=True)
rule_groups = fields.Many2many('ir.rule', 'rule_group_rel',
'group_id', 'rule_group_id', string='Rules', domain="[('global', '=', False)]")
menu_access = fields.Many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', string='Access Menu')
view_access = fields.Many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', string='Views')
comment = fields.Text(translate=True)
full_name = fields.Char(compute='_compute_full_name', string='Group Name', search='_search_full_name')
share = fields.Boolean(string='Share Group', help="Group created to set access rights for sharing data with some users.")
api_key_duration = fields.Float(string='API Keys maximum duration days',
help="Determines the maximum duration of an api key created by a user belonging to this group.")
sequence = fields.Integer(string='Sequence')
privilege_id = fields.Many2one('res.groups.privilege', string='Privilege', index=True)
view_group_hierarchy = fields.Json(string='Technical field for default group setting', compute='_compute_view_group_hierarchy')
_name_uniq = models.Constraint("UNIQUE (privilege_id, name)",
'The name of the group must be unique within a group privilege!')
_check_api_key_duration = models.Constraint(
'CHECK(api_key_duration >= 0)',
'The api key duration cannot be a negative value.',
)
""" The groups involved are to be interpreted as sets.
Thus we can define groups that we will call for example N, Z... such as mathematical sets.
┌──────────────────────────────────────────┐
│ C ┌──────────────────────────┐ │
│ │ R ┌───────────────────┐ │ ┌──────┐ | "C"
│ │ │ Q ┌────────────┐ │ │ │ I | | "I" implied "C"
│ │ │ │ Z ┌─────┐ │ │ │ │ | | "R" implied "C"
│ │ │ │ │ N │ │ │ │ │ │ │ "Q" implied "R"
│ │ │ │ └─────┘ │ │ │ │ │ │ "P" implied "R"
│ │ │ └────────────┘ │ │ │ │ │ "Z" implied "Q"
│ │ └───────────────────┘ │ │ │ │ "N" implied "Z"
│ │ ┌───────────────┐ │ │ │ │
│ │ │ P │ │ │ │ │
│ │ └───────────────┘ │ └──────┘ │
│ └──────────────────────────┘ │
└──────────────────────────────────────────┘
For example:
* A manager group will imply a user group: all managers are users (like Z imply C);
* A group "computer developer employee" will imply that he is an employee group, a user
group, that he has access to the timesheet user group.... "computer developer employee"
is therefore a set of users in the intersection of these groups. These users will
therefore have all the rights of these groups in addition to their own access rights.
"""
implied_ids = fields.Many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
string='Implied Groups', help='Users of this group are also implicitly part of those groups')
all_implied_ids = fields.Many2many('res.groups', string='Transitively Implied Groups', recursive=True,
compute='_compute_all_implied_ids', compute_sudo=True, search='_search_all_implied_ids',
help="The group itself with all its implied groups.")
implied_by_ids = fields.Many2many('res.groups', 'res_groups_implied_rel', 'hid', 'gid',
string='Implying Groups', help="Users in those groups are implicitly part of this group.")
all_implied_by_ids = fields.Many2many('res.groups', string='Transitively Implying Groups', recursive=True,
compute='_compute_all_implied_by_ids', compute_sudo=True, search='_search_all_implied_by_ids')
disjoint_ids = fields.Many2many('res.groups', string='Disjoint Groups',
help="A user may not belong to this group and one of those. For instance, users may not be portal users and internal users.",
compute='_compute_disjoint_ids')
@api.constrains('implied_ids', 'implied_by_ids')
def _check_disjoint_groups(self):
# check for users that might have two exclusive groups
self.env.registry.clear_cache('groups')
self.all_implied_by_ids._check_user_disjoint_groups()
@api.constrains('view_access')
def _check_inherited_view_groups(self):
self.view_access._check_groups()
@api.constrains('user_ids')
def _check_user_disjoint_groups(self):
# Here we should check all the users in any group of 'self':
#
# self.user_ids._check_disjoint_groups()
#
# But that wouldn't scale at all for large groups, like more than 10K
# users. So instead we search for such a nasty user.
gids = self._get_user_type_groups().ids
domain = (
Domain('active', '=', True)
& Domain('group_ids', 'in', self.ids)
& Domain.OR(
Domain('all_group_ids', 'in', [gids[index]])
& Domain('all_group_ids', 'in', gids[index+1:])
for index in range(0, len(gids) - 1)
)
)
user = self.env['res.users'].search(domain, order='id', limit=1)
if user:
user._check_disjoint_groups() # raises a ValidationError
@api.ondelete(at_uninstall=False)
def _unlink_except_settings_group(self):
classified = self.env['res.config.settings']._get_classified_fields()
for _name, _groups, implied_group in classified['group']:
if implied_group.id in self.ids:
raise ValidationError(self.env._('You cannot delete a group linked with a settings field.'))
@api.depends('privilege_id.name', 'name')
@api.depends_context('short_display_name')
def _compute_full_name(self):
# Important: value must be stored in environment of group, not group1!
for group, group1 in zip(self, self.sudo()):
if group1.privilege_id and not self.env.context.get('short_display_name'):
group.full_name = '%s / %s' % (group1.privilege_id.name, group1.name)
else:
group.full_name = group1.name
def _search_full_name(self, operator, operand):
if operator in Domain.NEGATIVE_OPERATORS:
return NotImplemented
if isinstance(operand, str):
def make_operand(val): return val
operands = [operand]
else:
def make_operand(val): return [val]
operands = operand
where_domains = [Domain('name', operator, operand)]
for group in operands:
if not group:
continue
domain = Domain('name', operator, make_operand(group))
where_domains.append(domain)
if '/' in group:
privilege_name, _, group_name = group.partition('/')
group_name = group_name.strip()
privilege_name = privilege_name.strip()
else:
privilege_name = group
group_name = None
if privilege_name:
domain = Domain(
'privilege_id', 'any!', Domain('name', operator, make_operand(privilege_name)),
)
if group_name:
domain &= Domain('name', operator, make_operand(group_name))
where_domains.append(domain)
return Domain.OR(where_domains)
@api.model
def _search(self, domain, offset=0, limit=None, order=None, **kwargs):
# add explicit ordering if search is sorted on full_name
if order and order.startswith('full_name'):
groups = super().search(domain)
groups = groups.sorted('full_name', reverse=order.endswith('DESC'))
groups = groups[offset:offset+limit] if limit else groups[offset:]
return groups._as_query(order)
return super()._search(domain, offset, limit, order, **kwargs)
def copy_data(self, default=None):
default = dict(default or {})
vals_list = super().copy_data(default=default)
for group, vals in zip(self, vals_list):
vals['name'] = default.get('name') or self.env._('%s (copy)', group.name)
return vals_list
def write(self, vals):
if 'name' in vals:
if vals['name'].startswith('-'):
raise UserError(self.env._('The name of the group can not start with "-"'))
# invalidate caches before updating groups, since the recomputation of
# field 'share' depends on method has_group()
# DLE P139
if self.ids:
self.env['ir.model.access'].call_cache_clearing_methods()
res = super().write(vals)
if 'implied_ids' in vals or 'implied_by_ids' in vals:
# Invalidate the cache of groups and their relationships
self.env.registry.clear_cache('groups')
return res
def _ensure_xml_id(self):
"""Return the groups external identifiers, creating the external identifier for groups missing one"""
result = self.get_external_id()
missings = {group_id: f'__custom__.group_{group_id}' for group_id, ext_id in result.items() if not ext_id}
if missings:
self.env['ir.model.data'].sudo().create(
[
{
'name': name.split('.')[1],
'model': 'res.groups',
'res_id': group_id,
'module': name.split('.')[0],
}
for group_id, name in missings.items()
]
)
result.update(missings)
return result
@api.depends('all_implied_by_ids.user_ids')
def _compute_all_user_ids(self):
for group in self.with_context(active_test=False):
group.all_user_ids = group.all_implied_by_ids.user_ids
def _inverse_all_user_ids(self):
for group in self:
user_to_add = group.all_user_ids - group.all_implied_by_ids.user_ids
user_to_remove = group.all_implied_by_ids.user_ids - group.all_user_ids
group.user_ids = group.user_ids - user_to_remove + user_to_add
cannot_remove = group.all_implied_by_ids.user_ids & user_to_remove
if cannot_remove:
raise UserError(self.env._(
"It is not possible to remove implied group %(group)s from users %(users)s",
group=repr(group.name),
users=', '.join(cannot_remove.mapped('name')),
))
def _search_all_user_ids(self, operator, value):
return [('all_implied_by_ids.user_ids', operator, value)]
@api.depends('implied_ids.all_implied_ids')
def _compute_all_implied_ids(self):
""" Compute the reflexive transitive closure of implied_ids. """
group_definitions = self._get_group_definitions()
for g in self:
g.all_implied_ids = g.ids + group_definitions.get_superset_ids(g.ids)
def _search_all_implied_ids(self, operator, value):
""" Compute the search on the reflexive transitive closure of implied_ids. """
if operator not in ('in', 'not in'):
return NotImplemented
group_definitions = self._get_group_definitions()
ids = [*value, *group_definitions.get_subset_ids(value)]
return [('id', operator, ids)]
@api.depends('implied_by_ids.all_implied_by_ids')
def _compute_all_implied_by_ids(self):
""" Compute the reflexive transitive closure of implied_by_ids. """
group_definitions = self._get_group_definitions()
for g in self:
g.all_implied_by_ids = g.ids + group_definitions.get_subset_ids(g.ids)
def _search_all_implied_by_ids(self, operator, value):
""" Compute the search on the reflexive transitive closure of implied_by_ids. """
if operator in ("any", "not any") and isinstance(value, Domain):
value = self.search(value).ids
operator = "in" if operator == "any" else "not in"
elif operator not in ('in', 'not in'):
return NotImplemented
group_definitions = self._get_group_definitions()
ids = [*value, *group_definitions.get_superset_ids(value)]
return [('id', operator, ids)]
def _get_user_type_groups(self):
""" Return the (disjoint) user type groups (employee, portal, public). """
group_ids = [
gid
for xid in ('base.group_user', 'base.group_portal', 'base.group_public')
if (gid := self.env['ir.model.data']._xmlid_to_res_id(xid, raise_if_not_found=False))
]
return self.sudo().browse(group_ids)
def _compute_disjoint_ids(self):
user_type_groups = self._get_user_type_groups()
for group in self:
if group in user_type_groups:
group.disjoint_ids = user_type_groups - group
else:
group.disjoint_ids = False
@api.model_create_multi
def create(self, vals_list):
groups = super().create(vals_list)
self.env.registry.clear_cache('groups')
return groups
def unlink(self):
res = super().unlink()
self.env.registry.clear_cache('groups')
return res
def _apply_group(self, implied_group):
""" Add the given group to the groups implied by the current group
:param implied_group: the implied group to add
"""
groups = self.filtered(lambda g: implied_group not in g.all_implied_ids)
groups.write({'implied_ids': [Command.link(implied_group.id)]})
def _remove_group(self, implied_group):
""" Remove the given group from the implied groups of the current group
:param implied_group: the implied group to remove
"""
groups = self.all_implied_ids.filtered(lambda g: implied_group in g.implied_ids)
groups.write({'implied_ids': [Command.unlink(implied_group.id)]})
def _compute_view_group_hierarchy(self):
self.view_group_hierarchy = self._get_view_group_hierarchy()
@api.model
@tools.ormcache('self.env.lang', cache='groups')
def _get_view_group_hierarchy(self):
return {
'groups': {
group.id: {
'id': group.id,
'name': group.name,
'comment': group.comment,
'privilege_id': group.privilege_id.id,
'disjoint_ids': group.disjoint_ids.ids,
'implied_ids': group.implied_ids.ids,
'all_implied_ids': group.all_implied_ids.ids,
'all_implied_by_ids': group.all_implied_by_ids.ids,
}
for group in self.search([])
},
'privileges': {
privilege.id: {
'id': privilege.id,
'name': privilege.name,
'category_id': privilege.category_id.id,
'description': privilege.description,
'placeholder': privilege.placeholder,
'group_ids': [group.id for group in privilege.group_ids.sorted(lambda g: (len(g.all_implied_ids & privilege.group_ids) if g.privilege_id else 0, g.sequence, g.id))]
}
for privilege in self.env['res.groups.privilege'].search([])
},
'categories': [
{
'id': category.id,
'name': category.name,
'privilege_ids': category.privilege_ids.sorted(lambda p: p.sequence).filtered(lambda p: p.group_ids).ids,
} for category in self.env['ir.module.category'].search([('privilege_ids.group_ids', '!=', False)])
]
}
@api.model
@tools.ormcache(cache='groups')
def _get_group_definitions(self):
""" Return the definition of all the groups as a :class:`~odoo.tools.SetDefinitions`. """
groups = self.sudo().search([], order='id')
id_to_ref = groups.get_external_id()
data = {
group.id: {
'ref': id_to_ref[group.id] or str(group.id),
'supersets': group.implied_ids.ids,
'disjoints': group.disjoint_ids.ids,
}
for group in groups
}
return SetDefinitions(data)
@api.model
def _is_feature_enabled(self, group_reference):
return self.env['res.users'].sudo().browse(api.SUPERUSER_ID)._has_group(group_reference)
@api.depends('all_user_ids')
def _compute_all_users_count(self):
for group in self:
group.all_users_count = len(group.all_user_ids)
def action_show_all_users(self):
self.ensure_one()
return {
'name': self.env._('Users and implied users of %(group)s', group=self.display_name),
'view_mode': 'list,form',
'res_model': 'res.users',
'type': 'ir.actions.act_window',
'context': {'create': False, 'delete': False, 'form_view_ref': 'base.view_users_form'},
'domain': [('all_group_ids', 'in', self.ids)],
'target': 'current',
}

81
scratch_xmlrpc.py Normal file
View File

@@ -0,0 +1,81 @@
import xmlrpc.client
import ssl
url = "https://odoodev2.mymachconnect.com"
dbs = ['mymach-odoodev2-2026', 'mymach_odoodev2']
username = "ceren.camkaya@mymach.com.tr"
password = "cerenhanım"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url), context=ctx)
models = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url), context=ctx)
for db_name in dbs:
print(f"--- Trying DB: {db_name} ---")
try:
uid = common.authenticate(db_name, username, password, {})
if uid:
print(f"Logged in successfully to {db_name}! UID: {uid}")
# Check module status
module_ids = models.execute_kw(db_name, uid, password,
'ir.module.module', 'search_read',
[[['name', '=', 'mymach_fleet_intelligence']]],
{'fields': ['state', 'latest_version']})
if module_ids:
mod = module_ids[0]
print(f"Module found! State: {mod['state']}, Version: {mod['latest_version']}")
# Check current view XML (e.g. looking for the language selector removal)
# Let's search for a view that we modified.
# 'fleet.vehicle.form' was modified? No, 'fleet_vehicle_view_form' might have been.
# Actually, let's look for 'res.groups' view if we added it, but it's data.
# Let's look for 'mymach_fleet_intelligence.view_home_form' (which had lang removed)
# Find the view by external ID
model_data = models.execute_kw(db_name, uid, password,
'ir.model.data', 'search_read',
[[['module', '=', 'mymach_fleet_intelligence'], ['name', '=', 'view_home_form']]],
{'fields': ['res_id']})
if model_data:
view_id = model_data[0]['res_id']
view = models.execute_kw(db_name, uid, password,
'ir.ui.view', 'read',
[[view_id]],
{'fields': ['arch_db']})
if view:
arch = view[0]['arch_db']
if 'field name="lang"' in arch:
print("The view STILL HAS the language selector (Old code).")
else:
print("The view DOES NOT have language selector (New code).")
# Try to trigger an upgrade
print("Triggering module upgrade...")
models.execute_kw(db_name, uid, password,
'ir.module.module', 'button_immediate_upgrade',
[[mod['id']]])
print("Upgrade finished! Let's check the view again...")
if model_data:
view = models.execute_kw(db_name, uid, password,
'ir.ui.view', 'read',
[[view_id]],
{'fields': ['arch_db']})
if view:
arch = view[0]['arch_db']
if 'field name="lang"' in arch:
print("AFTER UPGRADE: The view STILL HAS the language selector (Old code). This means git pull failed on the server!")
else:
print("AFTER UPGRADE: The view is updated! The issue was just that it wasn't upgraded properly.")
else:
print("Module not found in this DB.")
else:
print("Login failed for this DB.")
except Exception as e:
print(f"Error for DB {db_name}: {e}")