This commit is contained in:
cerenX9
2026-07-21 17:17:25 +03:00
parent f497805d08
commit 03b97ca431
31 changed files with 16371 additions and 708 deletions

View File

@@ -22,6 +22,7 @@
'views/fleet_fine_views.xml', 'views/fleet_fine_views.xml',
'views/fleet_handover_views.xml', 'views/fleet_handover_views.xml',
'views/home_views.xml', 'views/home_views.xml',
'views/fuel_index_views.xml',
], ],
'installable': True, 'installable': True,
'application': True, 'application': True,

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import http from odoo import http, _
from odoo.http import request from odoo.http import request
import json import json
@@ -245,22 +245,22 @@ class MymachFleetController(http.Controller):
# Uyumluluk ve İhlal Mantığı # Uyumluluk ve İhlal Mantığı
if is_manager_approved: if is_manager_approved:
is_violation = False is_violation = False
deviation_label = "✅ Yönetici Tarafından Onaylı Rota" deviation_label = _("✅ Yönetici Tarafından Onaylı Rota")
elif is_over_1km and policy == 'forbidden': elif is_over_1km and policy == 'forbidden':
is_violation = True is_violation = True
deviation_label = f"🚨 UYUMSUZ ({deviation} km Yasaklı Şahsi Kullanım)" deviation_label = _("🚨 UYUMSUZ (%(dev)s km Yasaklı Şahsi Kullanım)") % {'dev': deviation}
elif is_over_1km and policy == 'limit' and contract and ((contract.personal_km_used + deviation) > (contract.max_km_limit or 0.0)): elif is_over_1km and policy == 'limit' and contract and ((contract.personal_km_used + deviation) > (contract.max_km_limit or 0.0)):
is_violation = True is_violation = True
deviation_label = f"🚨 UYUMSUZ ({deviation} km Sözleşme KM Sınırııldı)" deviation_label = _("🚨 UYUMSUZ (%(dev)s km Sözleşme KM Sınırııldı)") % {'dev': deviation}
elif has_free_use: elif has_free_use:
is_violation = False is_violation = False
deviation_label = "Serbest İzinli Şahsi Kullanım" deviation_label = _("Serbest İzinli Şahsi Kullanım")
elif is_over_1km: elif is_over_1km:
is_violation = True is_violation = True
deviation_label = f"🚨 UYUMSUZ ({deviation} km Sapma - Yöneticiye Alarm)" deviation_label = _("🚨 UYUMSUZ (%(dev)s km Sapma - Yöneticiye Alarm)") % {'dev': deviation}
else: else:
is_violation = False is_violation = False
deviation_label = f"{deviation} km Uyumlu Rota" deviation_label = _("%(dev)s km Uyumlu Rota") % {'dev': deviation}
# Eğer UYUMSUZ (İhlal) ise yöneticiye güvenlik bildirimi/logu üret # Eğer UYUMSUZ (İhlal) ise yöneticiye güvenlik bildirimi/logu üret
if is_violation and v.driver_id: if is_violation and v.driver_id:
@@ -309,7 +309,18 @@ class MymachFleetController(http.Controller):
'green_route': rdata['green_route'], 'green_route': rdata['green_route'],
'red_route': rdata['red_route'], 'red_route': rdata['red_route'],
'alt1': rdata['alt1'], 'alt1': rdata['alt1'],
'alt2': rdata['alt2'] 'alt2': rdata['alt2'],
'label_home': _("Personel Ev Adresi:"),
'label_start': _("Başlangıç Noktası (İş Yeri):"),
'label_dest': _("Ulaşım Noktası (Hedef / Proje):"),
'label_plan': _("Planlanan / Kat Edilen:"),
'label_dev': _("Sapma & Durum:"),
'label_policy': _("Sözleşme Poliçesi:"),
'badge_violation': _("🚨 UYUMSUZ ROTA (YÖNETİCİYE ALARM BİLDİRİLDİ)"),
'badge_approved': _("✅ YÖNETİCİ ONAYLI PROJE GÖREVİ"),
'badge_ok': _("✅ UYUMLU ANA ROTA"),
'label_score': _('Skor:'),
'driver_tag_str': _(rdata['driver_tag']) if rdata['driver_tag'] else ''
}) })
# Son 50 güvenlik logunu çek (Haritada göstermek için) # Son 50 güvenlik logunu çek (Haritada göstermek için)

View File

@@ -23,5 +23,27 @@
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="active" eval="True"/> <field name="active" eval="True"/>
</record> </record>
<!-- Otonom Google Konum Çekme Servisi (Her 1 Dakikada Çalışır) -->
<record id="ir_cron_scrape_google_location" model="ir.cron">
<field name="name">MyMach: Google Konum Paylaşımı (GPS) Otonom Senkronizasyon</field>
<field name="model_id" ref="mymach_fleet_intelligence.model_mymach_fleet_security_log"/>
<field name="state">code</field>
<field name="code">model.action_mymach_scrape_google_location()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
</record>
<!-- Akaryakıt Fiyat Endeksi ve Enflasyon Takibi (Her Gece Çalışır) -->
<record id="ir_cron_fuel_index_fetch" model="ir.cron">
<field name="name">MyMach: Akaryakıt Fiyat Endeksi Güncelleme</field>
<field name="model_id" ref="mymach_fleet_intelligence.model_mymach_fleet_fuel_index"/>
<field name="state">code</field>
<field name="code">model.action_auto_fetch_fuel_prices()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True"/>
</record>
</data> </data>
</odoo> </odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
import polib
import os
base_dir = "/mnt/extra-addons/mymach_fleet_intelligence/i18n"
pot_file = os.path.join(base_dir, "mymach_fleet_intelligence.pot")
tr_file = os.path.join(base_dir, "tr_TR.po")
po = polib.pofile(pot_file, encoding='utf-8')
po_copy = polib.POFile(encoding='utf-8')
po_copy.metadata = po.metadata.copy()
po_copy.metadata['Language'] = 'tr_TR'
for entry in po:
po_entry = polib.POEntry(
msgid=entry.msgid,
msgstr=entry.msgid, # Explicitly set Turkish string to itself!
occurrences=entry.occurrences
)
po_copy.append(po_entry)
po_copy.save(tr_file)
print("Successfully generated tr_TR.po")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
import polib
from deep_translator import GoogleTranslator
import os
print("Starting english-only translation fix...")
base_dir = "/mnt/extra-addons/mymach_fleet_intelligence/i18n"
pot_file_path = os.path.join(base_dir, "mymach_fleet_intelligence.pot")
# MUST use utf-8 to avoid Mojibake with Turkish characters!
po = polib.pofile(pot_file_path, encoding='utf-8')
languages = {'en_US': 'en'}
msgids = [entry.msgid for entry in po if entry.msgid.strip()]
def chunk_list(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i + n]
for odoo_lang, gl_lang in languages.items():
print(f"Translating to {odoo_lang}...")
translator = GoogleTranslator(source='tr', target=gl_lang)
po_copy = polib.POFile(encoding='utf-8')
po_copy.metadata = po.metadata.copy()
translated_dict = {}
chunks = list(chunk_list(msgids, 50))
for i, chunk in enumerate(chunks):
try:
translated_chunk = translator.translate_batch(chunk)
for j, orig in enumerate(chunk):
translated_dict[orig] = translated_chunk[j]
except Exception as e:
for orig in chunk:
try:
translated_dict[orig] = translator.translate(orig)
except Exception as e2:
translated_dict[orig] = orig
print(f"Processed chunk {i+1}/{len(chunks)} for {odoo_lang}")
for entry in po:
if entry.msgid.strip():
msgstr = translated_dict.get(entry.msgid, entry.msgid)
else:
msgstr = ""
po_entry = polib.POEntry(
msgid=entry.msgid,
msgstr=msgstr,
occurrences=entry.occurrences
)
po_copy.append(po_entry)
output_file = os.path.join(base_dir, f"{odoo_lang}.po")
po_copy.save(output_file)
print(f"Successfully Saved {odoo_lang}.po")

View File

@@ -0,0 +1,67 @@
import polib
from deep_translator import GoogleTranslator
import os
import time
base_dir = "/mnt/extra-addons/mymach_fleet_intelligence/i18n"
pot_file_path = os.path.join(base_dir, "mymach_fleet_intelligence.pot")
target_langs = {
'fr_FR': 'fr',
'de_DE': 'de',
'es_ES': 'es',
'ar_001': 'ar'
}
print("Starting batch translation for multiple languages...")
po_template = polib.pofile(pot_file_path, encoding='utf-8')
msgids = [entry.msgid for entry in po_template if entry.msgid.strip()]
# Chunking function to avoid API limits
def chunk_list(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i + n]
for lang_code, google_lang in target_langs.items():
print(f"Translating to {lang_code}...")
po_file = polib.POFile(encoding='utf-8')
po_file.metadata = po_template.metadata.copy()
po_file.metadata['Language'] = lang_code
translator = GoogleTranslator(source='tr', target=google_lang)
# Translate in chunks of 50 strings
translated_dict = {}
chunks = list(chunk_list(msgids, 50))
for i, chunk in enumerate(chunks):
try:
translations = translator.translate_batch(chunk)
for orig, trans in zip(chunk, translations):
translated_dict[orig] = trans
print(f"Processed chunk {i+1}/{len(chunks)} for {lang_code}")
time.sleep(1) # Small delay to avoid rate limiting
except Exception as e:
print(f"Error in chunk {i+1} for {lang_code}: {e}")
# Fallback to one-by-one for this chunk
for orig in chunk:
try:
translated_dict[orig] = translator.translate(orig)
except:
translated_dict[orig] = orig
# Build PO entries
for entry in po_template:
msgstr = translated_dict.get(entry.msgid, entry.msgid) if entry.msgid.strip() else ""
po_entry = polib.POEntry(
msgid=entry.msgid,
msgstr=msgstr,
occurrences=entry.occurrences
)
po_file.append(po_entry)
po_file_path = os.path.join(base_dir, f"{lang_code}.po")
po_file.save(po_file_path)
print(f"Successfully Saved {lang_code}.po")
print("All translations completed.")

View File

@@ -8,3 +8,4 @@ from . import home
from . import fleet_fine from . import fleet_fine
from . import fleet_handover from . import fleet_handover
from . import periodic_maintenance from . import periodic_maintenance
from . import fuel_index

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import models, fields, api from odoo import models, fields, api, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
class MymachFleetReservation(models.Model): class MymachFleetReservation(models.Model):
@@ -52,7 +52,7 @@ class MymachFleetReservation(models.Model):
('end_datetime', '>', record.start_datetime) ('end_datetime', '>', record.start_datetime)
]) ])
if overlapping_reservations: if overlapping_reservations:
raise ValidationError(f"Seçtiğiniz tarihlerde '{record.vehicle_id.license_plate}' plakalı araç zaten rezerve edilmiştir! Çakışan talep: {overlapping_reservations[0].name}") raise ValidationError(_("Seçtiğiniz tarihlerde '%s' plakalı araç zaten rezerve edilmiştir! Çakışan talep: %s", record.vehicle_id.license_plate, overlapping_reservations[0].name))
def action_submit(self): def action_submit(self):
self.write({'state': 'submitted'}) self.write({'state': 'submitted'})

View File

@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from dateutil.relativedelta import relativedelta
import random
class MymachFleetFuelIndex(models.Model):
"""
Akaryakıt Fiyat Endeksi ve Enflasyon Takip Modeli
Yakıt fiyatlarındaki aylık ve yıllık değişim oranlarını (enflasyonu) otonom hesaplar.
"""
_name = 'mymach.fleet.fuel.index'
_description = 'Akaryakıt Fiyat Endeksi'
_order = 'date desc, id desc'
name = fields.Char(string="Endeks Ref", compute="_compute_name", store=True)
date = fields.Date(string="Tarih", required=True, default=fields.Date.context_today)
fuel_type = fields.Selection([
('gasoline', 'Benzinli'),
('diesel', 'Dizel'),
('lpg', 'LPG')
], string="Yakıt Türü", required=True, default='gasoline')
unit_price = fields.Float(string="Birim Fiyatı (TL)", required=True, digits=(10, 2))
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="Veri Kaynağı", default="Manuel Giriş")
@api.depends('date', 'fuel_type')
def _compute_name(self):
for record in self:
if record.date and record.fuel_type:
fuel_dict = dict(self._fields['fuel_type'].selection)
f_name = fuel_dict.get(record.fuel_type, 'Yakıt')
record.name = f"{f_name} Fiyatı - {record.date.strftime('%d.%m.%Y')}"
else:
record.name = "Yeni Akaryakıt Fiyatı"
@api.depends('unit_price', 'date', 'fuel_type')
def _compute_inflation(self):
for record in self:
if not record.date or not record.unit_price or not record.fuel_type:
record.mom_change_perc = 0.0
record.yoy_change_perc = 0.0
continue
# Geçen ayki fiyata bak (aynı yakıt türü için)
last_month_date = record.date - relativedelta(months=1)
last_month_record = self.env['mymach.fleet.fuel.index'].search([
('fuel_type', '=', record.fuel_type),
('date', '<=', last_month_date),
('id', '!=', record.id if isinstance(record.id, int) else False)
], order='date desc', limit=1)
if last_month_record and last_month_record.unit_price > 0:
record.mom_change_perc = ((record.unit_price - last_month_record.unit_price) / last_month_record.unit_price) * 100.0
else:
record.mom_change_perc = 0.0
# Geçen yılki fiyata bak
last_year_date = record.date - relativedelta(years=1)
last_year_record = self.env['mymach.fleet.fuel.index'].search([
('fuel_type', '=', record.fuel_type),
('date', '<=', last_year_date),
('id', '!=', record.id if isinstance(record.id, int) else False)
], order='date desc', limit=1)
if last_year_record and last_year_record.unit_price > 0:
record.yoy_change_perc = ((record.unit_price - last_year_record.unit_price) / last_year_record.unit_price) * 100.0
else:
record.yoy_change_perc = 0.0
@api.model
def action_auto_fetch_fuel_prices(self):
"""
Gider matrisindeki fişlerden gerçek yakıt maliyetlerini okuyup endeksi oluşturur.
"""
import re
today = fields.Date.today()
# Bugün veya en son kaydedilen yakıt fişlerini al (isimlerinde 'Yakıt' ve litre formatı olanlar)
domain = [('name', 'ilike', 'Yakıt')]
costs = self.env['mymach.fleet.maintenance.cost'].search(domain, order='date desc')
# Son günün tarihini bul (veri yoksa çık)
if not costs:
return
latest_date = costs[0].date
recent_costs = costs.filtered(lambda c: c.date == latest_date)
total_amount = 0.0
total_liters = 0.0
for cost in recent_costs:
# Örn: Otonom API Yakıt Alımı (25.3 L)
match = re.search(r'\(([\d\.]+)\s*L\)', cost.name)
if match and cost.amount > 0:
liters = float(match.group(1))
if liters > 0:
total_amount += cost.amount
total_liters += liters
if total_liters > 0:
avg_price = round(total_amount / total_liters, 2)
# Endekste bu tarih için kayıt var mı?
existing = self.search([('date', '=', latest_date), ('fuel_type', '=', 'gasoline')], limit=1)
if not existing:
self.create({
'date': latest_date,
'fuel_type': 'gasoline',
'unit_price': avg_price,
'source': 'Gider Fişlerinden Hesaplandı'
})
else:
existing.write({
'unit_price': avg_price,
'source': 'Gider Fişlerinden Güncellendi'
})

View File

@@ -14,6 +14,33 @@ class MymachFleetHome(models.Model):
display_title = fields.Char(compute='_compute_display_title') display_title = fields.Char(compute='_compute_display_title')
upcoming_maintenance_html = fields.Html(compute='_compute_upcoming_maintenance_html') upcoming_maintenance_html = fields.Html(compute='_compute_upcoming_maintenance_html')
# Dil Seçimi
dashboard_lang_id = fields.Many2one(
'res.lang',
string="Dil / Language",
compute='_compute_dashboard_lang',
inverse='_inverse_dashboard_lang',
domain=[('active', '=', True)]
)
@api.depends()
def _compute_dashboard_lang(self):
for record in self:
lang_code = self.env.user.lang or 'tr_TR'
lang = self.env['res.lang'].search([('code', '=', lang_code)], limit=1)
record.dashboard_lang_id = lang
def _inverse_dashboard_lang(self):
for record in self:
if record.dashboard_lang_id and self.env.user.lang != record.dashboard_lang_id.code:
self.env.user.lang = record.dashboard_lang_id.code
def action_apply_language(self):
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
# Grafik Filtreleri ve HTML Tuvali # Grafik Filtreleri ve HTML Tuvali
selected_vehicle_id = fields.Many2one('fleet.vehicle', string="Seçilen Araç") selected_vehicle_id = fields.Many2one('fleet.vehicle', string="Seçilen Araç")
time_range = fields.Selection([ time_range = fields.Selection([
@@ -39,149 +66,7 @@ class MymachFleetHome(models.Model):
route_simulated_distance = fields.Float(string="Simüle Edilen Yolculuk Mesafesi (KM)", default=15.2) route_simulated_distance = fields.Float(string="Simüle Edilen Yolculuk Mesafesi (KM)", default=15.2)
route_results_html = fields.Html(string="Alternatif Rotalar Sonucu", sanitize=False) route_results_html = fields.Html(string="Alternatif Rotalar Sonucu", sanitize=False)
# Finans & Gider Matrisi Özel Alanları
cost_matrix_vehicle_id = fields.Many2one('fleet.vehicle', string="Gider Analizi İçin Araç Seçimi")
cost_per_km_matrix = fields.Float(string="KM BAŞINA MALİYET", compute="_compute_cost_matrix_summary")
annual_total_cost_matrix = fields.Float(string="YILLIK TOPLAM MALİYET", compute="_compute_cost_matrix_summary")
monthly_avg_cost_matrix = fields.Float(string="AYLIK ORTALAMA MALİYET", compute="_compute_cost_matrix_summary")
cost_matrix_cards_html = fields.Html(string="Gider Matrisi Başlık Kartları", compute="_compute_cost_matrix_summary", sanitize=False)
@api.depends('cost_matrix_vehicle_id', 'selected_vehicle_id')
def _compute_cost_matrix_summary(self):
current_year = fields.Date.today().year
try_curr = self.env.ref('base.TRY', raise_if_not_found=False) or self.env.company.currency_id
for record in self:
vehicle = record.cost_matrix_vehicle_id or record.selected_vehicle_id or self.env['fleet.vehicle'].search([], limit=1)
if not vehicle:
record.cost_per_km_matrix = 0.0
record.annual_total_cost_matrix = 0.0
record.monthly_avg_cost_matrix = 0.0
record.cost_matrix_cards_html = "<div style='color:#cbd5e1; text-align:center; padding:20px;'>Araç seçilmedi.</div>"
continue
costs = self.env['mymach.fleet.maintenance.cost'].search([('vehicle_id', '=', vehicle.id)])
total_amount_try = sum(costs.mapped('amount')) if costs else 0.0
odo = vehicle.odometer or vehicle.total_driven_odometer or 1.0
if odo <= 0:
odo = 1.0
cpk_tl = total_amount_try / odo if odo > 0 else 0.0
this_year_costs_tl = sum(c.amount for c in costs if c.date and c.date.year == current_year)
annual_tl = this_year_costs_tl + (vehicle.annual_fixed_cost or 0.0)
if annual_tl == 0.0 and total_amount_try > 0:
annual_tl = total_amount_try
monthly_tl = annual_tl / 12.0
# Döviz Conversions & Average
curr1 = vehicle.cost_curr1_id or self.env.ref('base.USD', raise_if_not_found=False)
curr2 = vehicle.cost_curr2_id or self.env.ref('base.EUR', raise_if_not_found=False)
cpk_c1 = try_curr._convert(cpk_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0
cpk_c2 = try_curr._convert(cpk_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0
annual_c1 = try_curr._convert(annual_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0
annual_c2 = try_curr._convert(annual_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0
monthly_c1 = try_curr._convert(monthly_tl, curr1, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else 0.0
monthly_c2 = try_curr._convert(monthly_tl, curr2, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else 0.0
c1_back_try = curr1._convert(cpk_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else cpk_tl
c2_back_try = curr2._convert(cpk_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else cpk_tl
cpk_avg = (cpk_tl + c1_back_try + c2_back_try) / 3.0
ann_c1_try = curr1._convert(annual_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else annual_tl
ann_c2_try = curr2._convert(annual_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else annual_tl
annual_avg = (annual_tl + ann_c1_try + ann_c2_try) / 3.0
mon_c1_try = curr1._convert(monthly_c1, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr1 else monthly_tl
mon_c2_try = curr2._convert(monthly_c2, try_curr, vehicle.company_id or self.env.company, fields.Date.today()) if curr2 else monthly_tl
monthly_avg = (monthly_tl + mon_c1_try + mon_c2_try) / 3.0
record.cost_per_km_matrix = cpk_tl
record.annual_total_cost_matrix = annual_tl
record.monthly_avg_cost_matrix = monthly_tl
c1_name = curr1.name if curr1 else 'USD'
c2_name = curr2.name if curr2 else 'EUR'
# High Impact Dashboard HTML Cards with Currency Selection Matrix
html = f"""
<div class="card border-0 shadow-sm mt-3 mb-3" style="border-radius: 12px; border: 1px solid #e2e8f0; background: #ffffff; padding: 20px;">
<h4 style="color: #1e293b; font-size: 16px; font-weight: 700; margin-top: 0; margin-bottom: 20px;">
🚘 {vehicle.license_plate} ({vehicle.name or ""}) - FİNANSAL GİDER MATRİSİ ANALİZİ
</h4>
<div class="row text-center mb-4">
<div class="col-md-4 mb-3">
<div style="background: #f0fdf4; border: 1.5px solid #10b981; border-radius: 12px; padding: 18px;">
<div style="color: #047857; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">BİR KM BAŞINA MALİYET</div>
<div style="color: #059669; font-size: 24px; font-weight: 800; margin-top: 6px;">{cpk_tl:,.2f} ₺ / KM</div>
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{cpk_avg:,.2f} ₺</strong></div>
</div>
</div>
<div class="col-md-4 mb-3">
<div style="background: #f0f9ff; border: 1.5px solid #38bdf8; border-radius: 12px; padding: 18px;">
<div style="color: #0369a1; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">YILLIK TOPLAM MALİYET</div>
<div style="color: #0284c7; font-size: 24px; font-weight: 800; margin-top: 6px;">{annual_tl:,.2f} ₺ / YIL</div>
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{annual_avg:,.2f} ₺</strong></div>
</div>
</div>
<div class="col-md-4 mb-3">
<div style="background: #fffbeb; border: 1.5px solid #f59e0b; border-radius: 12px; padding: 18px;">
<div style="color: #b45309; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">AYLIK ORTALAMA MALİYET</div>
<div style="color: #d97706; font-size: 24px; font-weight: 800; margin-top: 6px;">{monthly_tl:,.2f} ₺ / AY</div>
<div style="color: #475569; font-size: 11px; margin-top: 4px;">Tahminî Ortalama: <strong>{monthly_avg:,.2f} ₺</strong></div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm" style="border-radius: 8px; border: 1px solid #e2e8f0; background: #ffffff;">
<div class="card-header bg-light border-bottom d-flex align-items-center justify-content-between" style="padding: 10px 15px;">
<h6 class="mb-0 font-weight-bold text-dark" style="font-size: 13px;">
💱 3 Para Birimli Karşılaştırmalı Gider Listbox Tablosu
</h6>
<span class="badge bg-success text-white" style="font-size: 11px;">Canlı Kur Dönüşümü</span>
</div>
<div class="card-body p-0">
<table class="table table-sm table-striped table-hover mb-0" style="font-size: 13px; width: 100%;">
<thead>
<tr style="background-color: #f8fafc; color: #475569; border-bottom: 2px solid #e2e8f0;">
<th style="padding: 10px 15px; width: 25%;">Metrik Başlığı</th>
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">🇹🇷 TL (Ana Para Birimi)</th>
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">💵 {c1_name} (Döviz 1)</th>
<th style="padding: 10px 15px; width: 20%; font-weight: 600;">💶 {c2_name} (Döviz 2)</th>
<th style="padding: 10px 15px; width: 25%; font-weight: 700; color: #047857; background-color: #ecfdf5;">📊 3 Dövizli Ortalama Tahminî Değer (TL)</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">BİR KM BAŞINA MALİYET</td>
<td style="padding: 10px 15px; color: #059669; font-weight: 600;">{cpk_tl:,.2f} ₺</td>
<td style="padding: 10px 15px; color: #334155;">{cpk_c1:,.2f} {c1_name}</td>
<td style="padding: 10px 15px; color: #334155;">{cpk_c2:,.2f} {c2_name}</td>
<td style="padding: 10px 15px; font-weight: 700; color: #047857; background-color: #f0fdf4;">{cpk_avg:,.2f} ₺</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">YILLIK TOPLAM MALİYET</td>
<td style="padding: 10px 15px; color: #0284c7; font-weight: 600;">{annual_tl:,.2f} ₺</td>
<td style="padding: 10px 15px; color: #334155;">{annual_c1:,.2f} {c1_name}</td>
<td style="padding: 10px 15px; color: #334155;">{annual_c2:,.2f} {c2_name}</td>
<td style="padding: 10px 15px; font-weight: 700; color: #0369a1; background-color: #f0f9ff;">{annual_avg:,.2f} ₺</td>
</tr>
<tr>
<td style="padding: 10px 15px; font-weight: 600; color: #1e293b;">AYLIK ORTALAMA MALİYET</td>
<td style="padding: 10px 15px; color: #d97706; font-weight: 600;">{monthly_tl:,.2f} ₺</td>
<td style="padding: 10px 15px; color: #334155;">{monthly_c1:,.2f} {c1_name}</td>
<td style="padding: 10px 15px; color: #334155;">{monthly_c2:,.2f} {c2_name}</td>
<td style="padding: 10px 15px; font-weight: 700; color: #b45309; background-color: #fffbeb;">{monthly_avg:,.2f} ₺</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
"""
record.cost_matrix_cards_html = html
def _compute_display_title(self): def _compute_display_title(self):
for record in self: for record in self:
@@ -209,7 +94,6 @@ class MymachFleetHome(models.Model):
html_lines = [] html_lines = []
html_lines.append('<div class="mymach_zara_table_container">') html_lines.append('<div class="mymach_zara_table_container">')
html_lines.append('<h3 class="mymach_zara_group_title" style="color: #fbbf24; border-color: rgba(245, 158, 11, 0.25); margin-bottom: 15px;">⏳ Yaklaşan ve Geciken Periyodik Bakım Listesi</h3>')
html_lines.append('<table class="mymach_zara_dashboard_table">') html_lines.append('<table class="mymach_zara_dashboard_table">')
html_lines.append('<thead>') html_lines.append('<thead>')
html_lines.append('<tr>') html_lines.append('<tr>')

View File

@@ -60,7 +60,10 @@ class MymachFleetMaintenanceCost(models.Model):
# API'den geldiği varsayılan litre (Örn: 20 ile 40 litre arası) # API'den geldiği varsayılan litre (Örn: 20 ile 40 litre arası)
import random import random
added_liters = random.uniform(10.0, 40.0) added_liters = random.uniform(10.0, 40.0)
price_per_liter = 45.50
# Dinamik Fiyat Çekimi
index_record = self.env['mymach.fleet.fuel.index'].search([('fuel_type', '=', 'gasoline')], order='date desc', limit=1)
price_per_liter = index_record.unit_price if index_record else 45.50
product = self.env['product.product'].search([], limit=1) product = self.env['product.product'].search([], limit=1)

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import models, fields, api from odoo import models, fields, api, _
from odoo.exceptions import UserError from odoo.exceptions import UserError
class MymachFleetRouteExceptionWizard(models.TransientModel): class MymachFleetRouteExceptionWizard(models.TransientModel):
@@ -57,11 +57,11 @@ class MymachFleetRouteExceptionWizard(models.TransientModel):
# 2. PROJE MALİYET SENARYOSU # 2. PROJE MALİYET SENARYOSU
elif self.exception_type == 'project': elif self.exception_type == 'project':
if not self.project_id: if not self.project_id:
raise UserError("Lütfen yolculuk maliyetinin yansıtılacağı projeyi seçin!") raise UserError(_("Lütfen yolculuk maliyetinin yansıtılacağı projeyi seçin!"))
analytic_account = self.project_id.account_id analytic_account = self.project_id.account_id
if not analytic_account: if not analytic_account:
raise UserError(f"Seçilen projenin ({self.project_id.name}) analitik hesabı tanımlanmamış!") raise UserError(_("Seçilen projenin (%s) analitik hesabı tanımlanmamış!", self.project_id.name))
# Analitik gider kaydı (account.analytic.line) oluştur # Analitik gider kaydı (account.analytic.line) oluştur
self.env['account.analytic.line'].create({ self.env['account.analytic.line'].create({
@@ -84,7 +84,7 @@ class MymachFleetRouteExceptionWizard(models.TransientModel):
], limit=1) ], limit=1)
if not contract: if not contract:
raise UserError("Sürücüye ait aktif bir tahsis sözleşmesi bulunamadı! Kişisel kullanım çözümlenemez.") raise UserError(_("Sürücüye ait aktif bir tahsis sözleşmesi bulunamadı! Kişisel kullanım çözümlenemez."))
distance = self.distance or 0.0 distance = self.distance or 0.0
charge_amount = 0.0 charge_amount = 0.0

View File

@@ -200,3 +200,84 @@ class MymachFleetSecurityLog(models.Model):
'default_estimated_cost': self.distance * (self.vehicle_id.km_cost or 5.0), 'default_estimated_cost': self.distance * (self.vehicle_id.km_cost or 5.0),
} }
} }
@api.model
def action_mymach_scrape_google_location(self):
"""
Google test hesabına giriş yaparak Kerem Bey'in canlı konum,
hız ve pil durumunu çeken otonom test fonksiyonu.
"""
# ==============================================================================
# BU SCRIPT: Kerem Bey'in şahsi hesabından test hesabına aktarılan canlı GPS
# verilerini Google Haritalar arayüzünden kazıyarak Odoo'ya aktarmak için yazılmıştır.
# Madde 3 uyarınca tüm kod blokları Türkçe teknik yorum satırları ile mühürlenmiştir.
# ==============================================================================
import logging
from datetime import datetime
_logger = logging.getLogger(__name__)
try:
# Google konum paylaşım kütüphanesi (Gerekirse sunucuya pip ile kurulacak)
from locationsharinglib import Service
except ImportError:
_logger.error("locationsharinglib kurulu değil. Lütfen 'pip install locationsharinglib' çalıştırın.")
return False
# Etap 1'de oluşturulan test hesabı
google_email = "mymach.fleet.test@gmail.com"
# NOT: Yeni kütüphane sürümlerinde (v5+) email/şifre yerine cookies_file kullanılmaktadır.
cookies_file_path = "/tmp/google_cookies.txt"
try:
# Google servis bağlantısını başlatıyoruz
# ==============================================================================
# NOT: Bu bağlantı Google Cloud API faturalandırmasına tabi değildir, tamamen ücretsizdir.
# ==============================================================================
service = Service(cookies_file=cookies_file_path, authenticating_account=google_email)
# Paylaşılan konumlar içinden Kerem Bey'in şahsi cihaz kaydını buluyoruz
for person in service.get_all_people():
if "Kerem" in person.full_name or person.is_self == False:
# Google'dan gelen ham koordinat ve durum verileri
latitude = person.latitude
longitude = person.longitude
accuracy = person.accuracy # Metre cinsinden sapma payı
battery_level = person.battery_level # Telefonun şarj durumu
last_seen = person.timestamp # Son konum güncelleme zamanı
# Okunan bu verileri test loguna veya Odoo geçici tablosuna yazıyoruz
_logger.info(
"MyMach GPS Yakalandı - Enlem: %s, Boylam: %s, Şarj: %s%%",
latitude, longitude, battery_level
)
# Bu koordinatlar Odoo rota doğrulama hiyerarşisine gönderilecek.
# Kerem isimli sürücünün aracını veya sistemdeki ilk aracı buluyoruz.
vehicle = self.env['fleet.vehicle'].search([('driver_id.name', 'ilike', 'Kerem')], limit=1)
if not vehicle:
vehicle = self.env['fleet.vehicle'].search([], limit=1)
if vehicle:
# mymach.security.log tablosuna yeni hamle oluşturuluyor
self.create({
'name': f"Google Canlı Konum: {person.full_name} ({battery_level}%)",
'vehicle_id': vehicle.id,
'latitude': latitude,
'longitude': longitude,
'status': 'project_match', # Varsayılan olarak proje güzergahında sayılır
})
return {
'lat': latitude,
'lng': longitude,
'battery': battery_level,
'time': datetime.fromtimestamp(last_seen)
}
return False
except Exception as e:
_logger.error("Google konum kazıma tünelinde hata oluştu: %s", str(e))
return False

View File

@@ -14,3 +14,4 @@ access_mymach_fleet_handover_manager,handover_manager,model_mymach_fleet_handove
access_mymach_fleet_periodic_maintenance_user,periodic_maintenance_user,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_user,1,1,0,0 access_mymach_fleet_periodic_maintenance_user,periodic_maintenance_user,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_user,1,1,0,0
access_mymach_fleet_periodic_maintenance_manager,periodic_maintenance_manager,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_manager,1,1,1,1 access_mymach_fleet_periodic_maintenance_manager,periodic_maintenance_manager,model_mymach_fleet_periodic_maintenance,group_mymach_fleet_manager,1,1,1,1
access_mymach_fleet_fuel_index,mymach.fleet.fuel.index,model_mymach_fleet_fuel_index,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
14 access_mymach_fleet_periodic_maintenance_user periodic_maintenance_user model_mymach_fleet_periodic_maintenance group_mymach_fleet_user 1 1 0 0
15 access_mymach_fleet_periodic_maintenance_manager periodic_maintenance_manager model_mymach_fleet_periodic_maintenance group_mymach_fleet_manager 1 1 1 1
16 access_mymach_fleet_fuel_index mymach.fleet.fuel.index model_mymach_fleet_fuel_index base.group_user 1 1 1 1
17

View File

@@ -271,18 +271,18 @@ document.addEventListener("DOMContentLoaded", function() {
var badgeBorder = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981'); var badgeBorder = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
var badgeText = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981'); var badgeText = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
var badgeTitle = vehicle.is_violation var badgeTitle = vehicle.is_violation
? '🚨 UYUMSUZ ROTA (YÖNETİCİYE ALARM BİLDİRİLDİ)' ? vehicle.badge_violation
: (vehicle.is_manager_approved ? '✅ YÖNETİCİ ONAYLI PROJE GÖREVİ' : '✅ UYUMLU ANA ROTA'); : (vehicle.is_manager_approved ? vehicle.badge_approved : vehicle.badge_ok);
overlayEl.style.display = 'block'; overlayEl.style.display = 'block';
overlayEl.innerHTML = overlayEl.innerHTML =
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">' + '<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">' +
'<div>' + '<div>' +
'<h3 style="color: white; font-size: 15px; font-weight: 700; margin: 0;">' + (vehicle.driver_tag || '') + ' - ' + vehicle.driver_name + '</h3>' + '<h3 style="color: white; font-size: 15px; font-weight: 700; margin: 0;">' + (vehicle.driver_tag_str || '') + ' - ' + vehicle.driver_name + '</h3>' +
'<span style="color: #94a3b8; font-size: 12px;">' + vehicle.license_plate + ' (' + vehicle.name + ')</span>' + '<span style="color: #94a3b8; font-size: 12px;">' + vehicle.license_plate + ' (' + vehicle.name + ')</span>' +
'</div>' + '</div>' +
'<span style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; color: ' + badgeText + '; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 12px;">' + '<span style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; color: ' + badgeText + '; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 12px;">' +
'Skor: ' + vehicle.score + vehicle.label_score + ' ' + vehicle.score +
'</span>' + '</span>' +
'</div>' + '</div>' +
'<div style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; border-radius: 8px; padding: 8px; margin-bottom: 12px; font-size: 11px; font-weight: bold; color: ' + badgeText + '; text-align: center;">' + '<div style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; border-radius: 8px; padding: 8px; margin-bottom: 12px; font-size: 11px; font-weight: bold; color: ' + badgeText + '; text-align: center;">' +
@@ -290,27 +290,27 @@ document.addEventListener("DOMContentLoaded", function() {
'</div>' + '</div>' +
'<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">' + '<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Personel Ev Adresi:</span>' + '<span style="color: #94a3b8;">' + vehicle.label_home + '</span>' +
'<strong style="color: #f1f5f9; font-size: 11px;">' + (vehicle.driver_home_address || 'Kadıköy/Moda') + '</strong>' + '<strong style="color: #f1f5f9; font-size: 11px;">' + (vehicle.driver_home_address || '') + '</strong>' +
'</div>' + '</div>' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Başlangıç Noktası (İş Yeri):</span>' + '<span style="color: #94a3b8;">' + vehicle.label_start + '</span>' +
'<strong style="color: white; font-size: 11px;">' + (vehicle.start_location || 'Genel Merkez Ofis') + '</strong>' + '<strong style="color: white; font-size: 11px;">' + (vehicle.start_location || '') + '</strong>' +
'</div>' + '</div>' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Ulaşım Noktası (Hedef / Proje):</span>' + '<span style="color: #94a3b8;">' + vehicle.label_dest + '</span>' +
'<strong style="color: #38bdf8; font-size: 11px;">' + vehicle.destination + '</strong>' + '<strong style="color: #38bdf8; font-size: 11px;">' + vehicle.destination + '</strong>' +
'</div>' + '</div>' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Planlanan / Kat Edilen:</span>' + '<span style="color: #94a3b8;">' + vehicle.label_plan + '</span>' +
'<span>' + vehicle.planned_km + ' KM / <strong>' + vehicle.driven_km + ' KM</strong></span>' + '<span>' + vehicle.planned_km + ' KM / <strong>' + vehicle.driven_km + ' KM</strong></span>' +
'</div>' + '</div>' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Sapma &amp; Durum:</span>' + '<span style="color: #94a3b8;">' + vehicle.label_dev + '</span>' +
'<strong style="color: ' + (vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981')) + ';">' + vehicle.deviation_label + '</strong>' + '<strong style="color: ' + (vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981')) + ';">' + vehicle.deviation_label + '</strong>' +
'</div>' + '</div>' +
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' + '<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
'<span style="color: #94a3b8;">Sözleşme Poliçesi:</span>' + '<span style="color: #94a3b8;">' + vehicle.label_policy + '</span>' +
'<span style="color: ' + (vehicle.has_free_use ? '#38bdf8' : '#f59e0b') + '; font-weight: 600;">' + vehicle.policy_desc + '</span>' + '<span style="color: ' + (vehicle.has_free_use ? '#38bdf8' : '#f59e0b') + '; font-weight: 600;">' + vehicle.policy_desc + '</span>' +
'</div>' + '</div>' +
'</div>'; '</div>';
@@ -331,11 +331,11 @@ document.addEventListener("DOMContentLoaded", function() {
: (v.is_manager_approved : (v.is_manager_approved
? 'background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4);' ? 'background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4);'
: 'background: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.4);'); : 'background: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.4);');
var statusLabel = isRev ? ('🚨 UYUMSUZ (' + v.deviation + ' km)') : (v.is_manager_approved ? '✅ Onaylı Rota' : '✅ Uyumlu'); var statusLabel = isRev ? (v.badge_violation) : (v.is_manager_approved ? v.badge_approved : v.badge_ok);
tr.innerHTML = tr.innerHTML =
'<td>' + '<td>' +
'<strong style="color: white;">' + (v.driver_tag || '') + ' - ' + v.driver_name + '</strong><br/>' + '<strong style="color: white;">' + (v.driver_tag_str || '') + ' - ' + v.driver_name + '</strong><br/>' +
'<span style="color: #94a3b8; font-size: 11px;">' + v.license_plate + '</span>' + '<span style="color: #94a3b8; font-size: 11px;">' + v.license_plate + '</span>' +
'</td>' + '</td>' +
'<td>' + '<td>' +

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_fuel_index_tree" model="ir.ui.view">
<field name="name">mymach.fleet.fuel.index.tree</field>
<field name="model">mymach.fleet.fuel.index</field>
<field name="arch" type="xml">
<list string="Akaryakıt Enflasyon Endeksi">
<field name="date" />
<field name="fuel_type" />
<field name="unit_price" widget="monetary" />
<field name="mom_change_perc" widget="percentage" decoration-danger="mom_change_perc &gt; 0" decoration-success="mom_change_perc &lt;= 0" />
<field name="yoy_change_perc" widget="percentage" decoration-danger="yoy_change_perc &gt; 0" decoration-success="yoy_change_perc &lt;= 0" />
<field name="source" />
</list>
</field>
</record>
<record id="view_fuel_index_form" model="ir.ui.view">
<field name="name">mymach.fleet.fuel.index.form</field>
<field name="model">mymach.fleet.fuel.index</field>
<field name="arch" type="xml">
<form string="Akaryakıt Fiyat Girişi">
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1" />
</h1>
</div>
<group>
<group>
<field name="date" />
<field name="fuel_type" />
<field name="unit_price" widget="monetary" />
</group>
<group>
<field name="mom_change_perc" widget="percentage" />
<field name="yoy_change_perc" widget="percentage" />
<field name="source" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_fuel_index" model="ir.actions.act_window">
<field name="name">Akaryakıt Enflasyon Endeksi</field>
<field name="res_model">mymach.fleet.fuel.index</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Sistem henüz bir akaryakıt endeksi kaydetmemiş.
</p>
<p>
Günlük yakıt fiyatları buradan takip edilir ve enflasyon oranları otomatik hesaplanır.
</p>
</field>
</record>
<!-- Ana Pano Configuration Menüsüne veya Ekonomi Alt Menüsüne Eklenebilir -->
<menuitem id="menu_mymach_fleet_fuel_index"
name="Ekonomi &amp; Enflasyon"
parent="menu_mymach_fleet_intelligence_root"
action="action_fuel_index"
sequence="90"/>
</odoo>

View File

@@ -53,10 +53,17 @@ action = {
<div class="mymach_zara_overlay"></div> <div class="mymach_zara_overlay"></div>
<!-- Floating Content Wrapper --> <!-- Floating Content Wrapper -->
<div class="mymach_zara_content"> <div class="mymach_zara_content" style="position: relative;">
<!-- Dil Seçici (Top Right) -->
<div style="position: absolute; top: 10px; right: 15px; width: 140px; z-index: 100;">
<field name="dashboard_lang_id" widget="selection" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 4px; width: 100%; display: block; margin-bottom: 5px;"/>
<button name="action_apply_language" type="object" string="🌐 Dili Değiştir" class="btn btn-sm btn-outline-light" style="width: 100%; font-size: 11px; padding: 3px;"/>
</div>
<!-- Zara-Style Minimalist Editorial Header --> <!-- 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;"> <div style="text-align: center; margin-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 25px;">
<h1 class="mymach_zara_title" style="color: #ef4444 !important; text-shadow: 0 0 15px rgba(239, 68, 68, 0.5); font-weight: 800;">MYMACH FLEET INTELLIGENCE</h1> <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>
<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);"/> <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> </div>
@@ -130,7 +137,7 @@ action = {
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<button name="%(mymach_fleet_intelligence.action_mymach_fleet_maintenance_cost)d" type="action" class="btn mymach_zara_btn" style="width: 100%;"> <button name="%(mymach_fleet_intelligence.action_mymach_fleet_maintenance_cost)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
<i class="fa fa-money mb-2"></i><br/> Gider Matrisi (Endeks) <i class="fa fa-money mb-2"></i><br/> Akaryakıt Giderleri
</button> </button>
</div> </div>
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
@@ -138,32 +145,15 @@ action = {
<i class="fa fa-line-chart mb-2"></i><br/> Kilometre &amp; Yakıt Grafiği <i class="fa fa-line-chart mb-2"></i><br/> Kilometre &amp; Yakıt Grafiği
</button> </button>
</div> </div>
<div class="col-md-3 mb-3">
<button name="%(mymach_fleet_intelligence.action_fuel_index)d" type="action" class="btn mymach_zara_btn" style="width: 100%;">
<i class="fa fa-percent mb-2"></i><br/> Akaryakıt Endeksi &amp; Enflasyon
</button>
</div>
</div> </div>
</div> </div>
<!-- Finans Gider Matrisi & Araç Maliyet Analitiği -->
<div style="margin-top: 20px; background: rgba(15, 23, 42, 0.4); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px;" groups="mymach_fleet_intelligence.group_mymach_fleet_manager">
<h3 style="color: #10b981; font-size: 15px; font-weight: 600; margin-top: 0; margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
💰 FİNANS GİDER MATRİSİ VE ARAÇ MALİYET ANALİTİĞİ
</h3>
<div class="row">
<div class="col-md-6 mb-3">
<label for="cost_matrix_vehicle_id" style="color: #cbd5e1; font-weight: 500; margin-bottom: 5px; display: block;">Maliyeti Hesaplanacak Aracı Seçin:</label>
<field name="cost_matrix_vehicle_id" options="{'no_create': True, 'no_open': True}" style="background: rgba(15, 23, 42, 0.6) !important; color: white !important; border: 1px solid rgba(255, 255, 255, 0.15) !important; border-radius: 6px; padding: 6px; width: 100%; display: block;"/>
</div>
<div class="col-md-6 mb-3" style="display: flex; align-items: flex-end;">
<button name="action_save_settings" string="Seçilen Araç İçin Hesapla ve Güncelle" type="object" class="btn btn-success" style="width: 100%; background: #10b981; color: white; border: none; font-weight: bold; padding: 8px;"/>
</div>
</div>
<div style="margin-top: 10px;">
<field name="cost_matrix_cards_html" readonly="1"/>
</div>
</div>
<!-- Periyodik Bakım Canlı İzleme Tablosu -->
<div style="margin-top: 20px;">
<field name="upcoming_maintenance_html" readonly="1"/>
</div>
</div> </div>
</div> </div>
</sheet> </sheet>

View File

@@ -61,7 +61,7 @@
<!-- Gider Matrisi Aksiyonu --> <!-- Gider Matrisi Aksiyonu -->
<record id="action_mymach_fleet_maintenance_cost" model="ir.actions.act_window"> <record id="action_mymach_fleet_maintenance_cost" model="ir.actions.act_window">
<field name="name">Gider Matrisi (Araç Bazlı Analiz)</field> <field name="name">Akaryakıt Giderleri (Araç Bazlı Analiz)</field>
<field name="res_model">mymach.fleet.maintenance.cost</field> <field name="res_model">mymach.fleet.maintenance.cost</field>
<field name="view_mode">list,form</field> <field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_vehicle': 1}</field> <field name="context">{'search_default_group_by_vehicle': 1}</field>
@@ -69,7 +69,7 @@
<!-- Alt Menü --> <!-- Alt Menü -->
<menuitem id="menu_mymach_fleet_maintenance_cost" <menuitem id="menu_mymach_fleet_maintenance_cost"
name="Finans &amp; Gider Matrisi" name="Akaryakıt Giderleri"
parent="menu_mymach_fleet_intelligence_root" parent="menu_mymach_fleet_intelligence_root"
action="action_mymach_fleet_maintenance_cost" action="action_mymach_fleet_maintenance_cost"
sequence="2" sequence="2"

57
populate_fuel_data.py Normal file
View File

@@ -0,0 +1,57 @@
import random
from datetime import timedelta, date
env = env(su=True)
company = env.company
if company.currency_id and getattr(company.currency_id, 'rounding', 0.0) == 0.0:
company.currency_id.write({'rounding': 0.01})
try_currency = env.ref('base.TRY', raise_if_not_found=False)
if try_currency:
try_currency.write({'active': True})
if getattr(try_currency, 'rounding', 0.0) == 0.0:
try_currency.write({'rounding': 0.01})
currency = try_currency or company.currency_id
product = env['product.template'].search([('type', '=', 'service')], limit=1)
if not product:
product = env['product.template'].create({'name': 'Akaryakıt (Litre)', 'type': 'service', 'list_price': 42.5})
vehicles = env['fleet.vehicle'].search([], limit=2)
if not vehicles:
print("Sistemde araç bulunamadı.")
else:
today = date.today()
for v in vehicles:
print(f"Araç {v.license_plate} için KM ve Yakıt verileri oluşturuluyor...")
base_km = v.odometer if v.odometer > 1000 else 45000
for i in range(30, -1, -2):
record_date = today - timedelta(days=i)
increment = random.randint(50, 150)
base_km += increment
env['fleet.vehicle.odometer'].create({
'vehicle_id': v.id,
'date': record_date,
'value': base_km
})
liters = random.uniform(20.0, 50.0)
price_per_liter = 42.5
total_cost = liters * price_per_liter
env['mymach.fleet.maintenance.cost'].create({
'vehicle_id': v.id,
'date': record_date,
'name': f"Akaryakıt Alımı ({liters:.1f} L)",
'amount': total_cost,
'currency_id': currency.id,
'product_id': product.id
})
print(f"Araç {v.license_plate} için son değer KM: {base_km}, başarıyla tamamlandı.")
env.cr.commit()
print("Tüm örnek KM ve Yakıt verileri sisteme başarıyla yüklendi!")

9
set_turkish.py Normal file
View File

@@ -0,0 +1,9 @@
lang = env['res.lang'].search([('code', '=', 'tr_TR')])
if not lang:
installer = env['base.language.install'].create({'lang_ids': [(4, env.ref('base.lang_tr').id)]})
installer.lang_install()
env['res.lang'].search([('code', '=', 'tr_TR')]).write({'active': True})
env['res.users'].search([]).write({'lang': 'tr_TR'})
env.cr.commit()
print("Language changed successfully!")

3
set_turkish_users.py Normal file
View File

@@ -0,0 +1,3 @@
env['res.users'].search([]).write({'lang': 'tr_TR'})
env.cr.commit()
print("All users updated to Turkish!")

52
test_fuel_index.py Normal file
View File

@@ -0,0 +1,52 @@
import time
from datetime import date
# Yetki yükselt (sudo)
env = env(su=True)
print("="*50)
print("AKARYAKIT FİYAT ENDEKSİ TESTİ")
print("="*50)
fuel_model = env['mymach.fleet.fuel.index']
# 1. İlk gün fiyatlarını oluştur
print("\n[Adım 1] Geçmiş Fiyat Simülasyonu (Geçen Ay ve Geçen Yıl)")
last_year = date.today().replace(year=date.today().year - 1)
last_month = date.today().replace(month=date.today().month - 1) if date.today().month > 1 else date.today().replace(year=date.today().year - 1, month=12)
# Geçen yılki benzin fiyatı (Örn: 25.0 TL)
fuel_model.create({
'date': last_year,
'fuel_type': 'gasoline',
'unit_price': 25.0,
'source': 'Test Data (Geçen Yıl)'
})
# Geçen ayki benzin fiyatı (Örn: 42.0 TL)
fuel_model.create({
'date': last_month,
'fuel_type': 'gasoline',
'unit_price': 42.0,
'source': 'Test Data (Geçen Ay)'
})
print("✅ Geçmiş fiyatlar (1 Yıl ve 1 Ay Öncesi) başarıyla oluşturuldu.")
# 2. Otonom Cron Job Metodunu Çalıştır (Bugünün fiyatını çeksin/simüle etsin)
print("\n[Adım 2] Otonom Fiyat Çekme (Cron) Metodu Tetikleniyor...")
fuel_model.action_auto_fetch_fuel_prices()
# 3. Sonuçları Kontrol Et
print("\n[Adım 3] Sonuçlar Analiz Ediliyor...")
today_record = fuel_model.search([('fuel_type', '=', 'gasoline')], order='date desc', limit=1)
print(f"Bugünkü Benzin Fiyatı: {today_record.unit_price} TL")
print(f"Aylık Değişim (MoM): %{today_record.mom_change_perc:.2f}")
print(f"Yıllık Değişim (YoY): %{today_record.yoy_change_perc:.2f}")
print("\n" + "="*50)
print("TEST TAMAMLANDI")
print("="*50)
env.cr.rollback()

2
test_google_scrape.py Normal file
View File

@@ -0,0 +1,2 @@
result = env['mymach.fleet.security.log'].action_mymach_scrape_google_location()
print("Scrape Result:", result)

100
test_integration.py Normal file
View File

@@ -0,0 +1,100 @@
import time
from datetime import date
# Yetki yükselt (sudo)
env = env(su=True)
print("="*50)
print("SİSTEM ENTEGRASYON TESTİ BAŞLIYOR")
print("="*50)
# 1. Yeni Sürücü (Partner) Oluşturma
print("\n[Adım 1] Yeni bir test sürücüsü oluşturuluyor...")
test_driver = env['res.partner'].create({
'name': 'Entegrasyon Test Sürücüsü',
'phone': '05555555555',
'email': 'test.surucu@mymach.com',
})
print(f"✅ Sürücü Oluşturuldu: {test_driver.name} (ID: {test_driver.id})")
# 2. Yeni Araç Oluşturma
print("\n[Adım 2] Yeni bir test aracı oluşturuluyor...")
vehicle_model = env['fleet.vehicle.model'].search([], limit=1)
if not vehicle_model:
brand = env['fleet.vehicle.model.brand'].create({'name': 'Test Marka'})
vehicle_model = env['fleet.vehicle.model'].create({
'name': 'Test Model',
'brand_id': brand.id,
})
test_vehicle = env['fleet.vehicle'].create({
'license_plate': '99-TEST-99',
'model_id': vehicle_model.id,
'driver_id': test_driver.id,
'odometer': 10000,
'purchase_value': 1000000.0,
'company_id': env.company.id,
})
print(f"✅ Araç Oluşturuldu: {test_vehicle.license_plate} (ID: {test_vehicle.id})")
print(f" ↳ Araç sürücüsü '{test_vehicle.driver_id.name}' olarak otomatik tanımlandı.")
# 3. Yakıt Gideri Ekleme (Maliyet Modülü Entegrasyonu)
print("\n[Adım 3] Araca yakıt masrafı işleniyor...")
product = env['product.template'].search([('type', '=', 'service')], limit=1)
currency = env.ref('base.TRY', raise_if_not_found=False) or env.company.currency_id
fuel_cost = env['mymach.fleet.maintenance.cost'].create({
'vehicle_id': test_vehicle.id,
'name': 'Entegrasyon Test Yakıt Alımı',
'amount': 2500.0, # 2500 TL
'date': date.today(),
'currency_id': currency.id,
'product_id': product.id if product else False,
})
print(f"✅ Yakıt Masrafı Eklendi: {fuel_cost.amount} {fuel_cost.currency_id.name}")
# Compute alanların tetiklenmesi için flush/invalidate yapalım
env.cr.flush()
test_vehicle.invalidate_recordset(['total_expense_amount', 'cost_per_km_calculated'])
print(f" ↳ Aracın Toplam Masraf Alanı (total_expense_amount): {test_vehicle.total_expense_amount}")
print(f" ↳ Aracın KM Başına Maliyeti (cost_per_km_calculated): {test_vehicle.cost_per_km_calculated}")
if test_vehicle.total_expense_amount == 2500.0:
print(" [BAŞARILI] Masraf girdisi otomatik olarak aracın toplam maliyet alanını güncelledi!")
# 4. Zimmet Sözleşmesi ve Sürücü Skoru Kontrolü (Sözleşme Modülü Entegrasyonu)
print("\n[Adım 4] Sürücü Sözleşmesi ve Skor Testi...")
contract = env['mymach.fleet.contract'].create({
'name': f'Tahsis-{test_driver.name}',
'driver_id': test_driver.id,
'driver_score': 100,
})
print(f"✅ Sözleşme Oluşturuldu. Sürücü Başlangıç Skoru: {contract.driver_score}")
# Hız İhlali (Ceza/Güvenlik Modülü Entegrasyonu) - Sözleşmedeki skoru düşürüp düşürmediğine bakalım
print(" ↳ Sürücü için bir güvenlik ihlali (Ofiste Araç Hareket Halinde) kaydediliyor...")
security_log = env['mymach.fleet.security.log'].create({
'vehicle_id': test_vehicle.id,
'name': 'Test Güvenlik İhlali',
'status': 'in_office_moving',
'datetime': date.today(),
'latitude': 41.0,
'longitude': 29.0
})
# Çoğu iş mantığında security log oluşunca sürücü sözleşmesindeki skor düşer (Eğer trigger yazıldıysa).
# Biz durumu kontrol ediyoruz:
env.cr.flush()
contract.invalidate_recordset(['driver_score'])
print(f" ↳ Güvenlik ihlali sonrası güncel sürücü skoru: {contract.driver_score}")
if contract.driver_score < 100:
print(" [BAŞARILI] Güvenlik modülü ile sözleşme/sürücü skor modülü entegre çalışıyor!")
else:
print(" [BİLGİ] Otomatik skor düşürme kancası yok, ancak kayıtlar birbiriyle başarıyla ilişkili.")
print("\n" + "="*50)
print("SİSTEM ENTEGRASYON TESTİ TAMAMLANDI")
print("Tüm modüllerin birbirine zincirleme bağlı olduğu doğrulanmıştır.")
print("="*50)
env.cr.rollback() # Test verilerini geri alıyoruz (kalıcı olmasın)