This commit is contained in:
XuanHuyen
2025-04-21 16:23:41 +07:00
parent 1adc5cc52a
commit 80e48f4df8
4 changed files with 280 additions and 127 deletions
+155 -53
View File
@@ -1,15 +1,30 @@
from odoo import fields, models, api, _ from odoo import fields, models, api, _
from odoo.exceptions import UserError from odoo.exceptions import UserError, ValidationError
import requests import requests
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
import logging
_logger = logging.getLogger(__name__)
class WeatherAPI(models.Model): class WeatherAPI(models.Model):
_name = 'weather.api' _name = 'weather.api'
_description = 'Weather API' _description = 'Weather API Integration'
_rec_name = 'company_id' _rec_name = 'company_id'
company_id = fields.Many2one('res.company', string='Công ty', required=True, default=lambda self: self.env.company) # Thêm trường mới để cấu hình timeout
api_timeout = fields.Integer(
string="API Timeout (seconds)",
default=10,
help="Timeout for API requests in seconds"
)
company_id = fields.Many2one(
'res.company',
string='Công ty',
required=True,
default=lambda self: self.env.company
)
current_temperature = fields.Float(string="Nhiệt độ hiện tại", compute="_compute_weather", store=False) current_temperature = fields.Float(string="Nhiệt độ hiện tại", compute="_compute_weather", store=False)
current_humidity = fields.Float(string="Độ ẩm hiện tại", compute="_compute_weather", store=False) current_humidity = fields.Float(string="Độ ẩm hiện tại", compute="_compute_weather", store=False)
@@ -25,6 +40,7 @@ class WeatherAPI(models.Model):
@api.depends() @api.depends()
def _compute_weather(self): def _compute_weather(self):
for rec in self: for rec in self:
try:
current = rec.get_weather_current() current = rec.get_weather_current()
rec.current_temperature = current.get('temperature') rec.current_temperature = current.get('temperature')
rec.current_humidity = current.get('humidity') rec.current_humidity = current.get('humidity')
@@ -39,95 +55,169 @@ class WeatherAPI(models.Model):
# Next 3 days # Next 3 days
forecast = rec._get_next_3days_forecast() forecast = rec._get_next_3days_forecast()
html = "<ul>" rec.next_3_days_weather = rec._format_3days_html(forecast)
for day in forecast:
html += f"<li><b>{day['date']}</b>: {day['main_description']}, {day['temp_min']}°C - {day['temp_max']}°C, Gió: {day['wind_speed']} m/s</li>" except Exception as e:
html += "</ul>" _logger.error("Error computing weather data: %s", str(e))
rec.next_3_days_weather = html # Set default values when error occurs
rec._set_default_weather_values()
def _set_default_weather_values(self):
"""Set default values when API fails"""
self.update({
'current_temperature': 0.0,
'current_humidity': 0.0,
'current_wind_speed': 0.0,
'current_description': _("Không thể lấy dữ liệu thời tiết"),
'today_hourly_first': "<p>Không có dữ liệu</p>",
'today_hourly_second': "<p>Không có dữ liệu</p>",
'today_hourly_third': "<p>Không có dữ liệu</p>",
'next_3_days_weather': "<p>Không có dữ liệu</p>",
})
def _format_html(self, data): def _format_html(self, data):
if not data: if not data:
return "<p>Không có dữ liệu.</p>" return "<p>Không có dữ liệu.</p>"
html = "<ul>" html = "<ul>"
for d in data: for d in data:
html += f"<li><b>{d['hour']}h</b>: {d['description']}, {d['temp_min']}°C - {d['temp_max']}°C, Gió: {d['wind_speed']} m/s</li>" html += f"""
<li>
<b>{d['hour']}h</b>: {d['description']}
{d['temp_min']}°C - {d['temp_max']}°C,
Gió: {d['wind_speed']} m/s,
Độ ẩm: {d['humidity']}%
</li>
"""
html += "</ul>" html += "</ul>"
return html return html
def _format_3days_html(self, forecast_data):
if not forecast_data:
return "<p>Không có dữ liệu.</p>"
html = """
<style>
.weather-forecast {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.weather-day {
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
min-width: 200px;
}
</style>
<div class="weather-forecast">
"""
for day in forecast_data:
html += f"""
<div class="weather-day">
<h4>{day['date']}</h4>
<p><b>Tình trạng:</b> {day['main_description']}</p>
<p><b>Nhiệt độ:</b> {day['temp_min']}°C - {day['temp_max']}°C</p>
<p><b>Gió:</b> {day['wind_speed']} m/s</p>
<p><b>Độ ẩm:</b> {day['humidity']}%</p>
</div><br>
"""
html += "</div>"
return html
# Lấy thông tin API key từ cấu hình
@api.model @api.model
def get_api_key(self): def get_api_key(self):
"""Lấy thông tin API key từ cấu hình""" """Lấy thông tin API key từ cấu hình với kiểm tra bảo mật"""
api_key = self.env['ir.config_parameter'].sudo().get_param('weather_infor.api_key') api_key = self.env['ir.config_parameter'].sudo().get_param('weather_infor.api_key')
if not api_key: if not api_key:
raise UserError("API Key chưa được cấu hình. Vui lòng vào Settings > Weather API Settings để nhập API Key.") raise ValidationError(_("API Key chưa được cấu hình. Vui lòng vào Settings > Weather API Settings để nhập API Key."))
return api_key return api_key
# Lấy thông tin thành phố từ cấu hình công ty
@api.model @api.model
def get_city(self): def get_city(self):
"""Lấy thông tin thành phố từ cấu hình công ty""" """Lấy thông tin thành phố từ cấu hình công ty với validation"""
city = self.env.company.city city = self.env.company.city
if not city: if not city:
raise UserError(_("Thành phố chưa được cấu hình. Vui lòng vào Settings > Công ty để nhập thành phố.")) raise ValidationError(_("Thành phố chưa được cấu hình. Vui lòng vào Settings > Công ty để nhập thành phố."))
return city return city.strip()
def _make_api_request(self, url):
"""Helper method to make API requests with proper error handling"""
try:
response = requests.get(
url,
timeout=self.api_timeout,
headers={'User-Agent': 'Odoo/1.0'}
)
response.raise_for_status() # Raises HTTPError for bad responses
return response.json()
except requests.exceptions.Timeout:
_logger.error("Weather API timeout after %s seconds", self.api_timeout)
raise UserError(_("Dịch vụ thời tiết không phản hồi. Vui lòng thử lại sau."))
except requests.exceptions.RequestException as e:
_logger.error("Weather API request failed: %s", str(e))
raise UserError(_("Không thể kết nối đến dịch vụ thời tiết. Vui lòng kiểm tra kết nối mạng."))
except ValueError as e: # Includes JSON decode errors
_logger.error("Invalid JSON response from weather API: %s", str(e))
raise UserError(_("Dữ liệu thời tiết nhận được không hợp lệ."))
# Lấy thông tin thời tiết hiện tại
@api.model @api.model
def get_weather_current(self): def get_weather_current(self):
"""Lấy dữ liệu thời tiết hiện tại""" """Lấy dữ liệu thời tiết hiện tại với xử lý lỗi tốt hơn"""
try:
api_key = self.get_api_key() api_key = self.get_api_key()
city = self.get_city() city = self.get_city()
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric" url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric&lang=vi"
data = self._make_api_request(url)
response = requests.get(url)
if response.status_code == 200:
data = response.json()
return { return {
'temperature': data['main']['temp'], 'temperature': data['main']['temp'],
'humidity': data['main']['humidity'], 'humidity': data['main']['humidity'],
'wind_speed': data['wind']['speed'], 'wind_speed': data['wind']['speed'],
'description': data['weather'][0]['description'] 'description': data['weather'][0]['description'].capitalize()
} }
else: except KeyError as e:
raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố.")) _logger.error("Missing expected data in weather API response: %s", str(e))
raise UserError(_("Dữ liệu thời tiết không đầy đủ. Vui lòng thử lại sau."))
# Lấy thông tin thời tiết 3 ngày
@api.model @api.model
def _get_next_3days_forecast(self): def _get_next_3days_forecast(self):
"""Lấy dữ liệu thời tiết cho 3 ngày tiếp theo""" """Lấy dữ liệu thời tiết cho 3 ngày tiếp theo với xử lý dữ liệu tốt hơn"""
try:
api_key = self.get_api_key() api_key = self.get_api_key()
city = self.get_city() city = self.get_city()
url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric" url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi"
response = requests.get(url) data = self._make_api_request(url)
if response.status_code != 200:
raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố."))
data = response.json()
forecast_data = data['list'] forecast_data = data['list']
# Gom dữ liệu theo ngày # Gom dữ liệu theo ngày
weather_data = defaultdict(list) weather_data = defaultdict(list)
for forecast in forecast_data: for forecast in forecast_data:
try:
dt_txt = forecast['dt_txt'] dt_txt = forecast['dt_txt']
date_str = dt_txt.split(' ')[0] date_str = dt_txt.split(' ')[0]
weather_data[date_str].append(forecast) weather_data[date_str].append(forecast)
except (KeyError, AttributeError) as e:
_logger.warning("Invalid forecast data structure: %s", str(e))
continue
# Chọn 3 ngày tiếp theo # Chọn 3 ngày tiếp theo (bỏ qua hôm nay)
sorted_dates = sorted(weather_data.keys()) today = datetime.now().date().strftime('%Y-%m-%d')
today = datetime.now().date() sorted_dates = sorted([d for d in weather_data.keys() if d != today])
if today in sorted_dates:
sorted_dates.remove(today) if not sorted_dates:
next_3_days = sorted_dates[1:4] return []
next_3_days = sorted_dates[:3] # Lấy 3 ngày đầu tiên sau hôm nay
result = [] result = []
for date in next_3_days: for date in next_3_days:
daily_forecasts = weather_data[date] daily_forecasts = weather_data[date]
if not daily_forecasts:
continue
temps = [f['main']['temp'] for f in daily_forecasts] temps = [f['main']['temp'] for f in daily_forecasts]
humidities = [f['main']['humidity'] for f in daily_forecasts] humidities = [f['main']['humidity'] for f in daily_forecasts]
descriptions = [f['weather'][0]['description'] for f in daily_forecasts] descriptions = [f['weather'][0]['description'] for f in daily_forecasts]
@@ -139,24 +229,23 @@ class WeatherAPI(models.Model):
'temp_max': max(temps), 'temp_max': max(temps),
'humidity': round(sum(humidities) / len(humidities), 2), 'humidity': round(sum(humidities) / len(humidities), 2),
'wind_speed': round(sum(wind_speeds) / len(wind_speeds), 2), 'wind_speed': round(sum(wind_speeds) / len(wind_speeds), 2),
'main_description': max(set(descriptions), key=descriptions.count), # mô tả xuất hiện nhiều nhất 'main_description': max(set(descriptions), key=descriptions.count).capitalize(),
}) })
return result return result
except Exception as e:
_logger.error("Error getting 3-day forecast: %s", str(e))
raise UserError(_("Không thể lấy dự báo thời tiết. Vui lòng thử lại sau."))
# Lấy thông tin thời tiết ngày hôm nay, chia làm 3 khung giờ
@api.model @api.model
def _get_today_weather(self): def _get_today_weather(self):
"""Lấy dữ liệu thời tiết ngày hôm nay chia làm 3 khung giờ: 1-8h, 9-16h, 17-24h""" """Lấy dữ liệu thời tiết ngày hôm nay chia làm 3 khung giờ"""
try:
api_key = self.get_api_key() api_key = self.get_api_key()
city = self.env.company.city city = self.get_city()
url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric" url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi"
response = requests.get(url) data = self._make_api_request(url)
if response.status_code != 200:
raise UserError(_("Không thể lấy dữ liệu thời tiết. Vui lòng kiểm tra lại API key hoặc tên thành phố."))
data = response.json()
forecast_data = data['list'] forecast_data = data['list']
today_str = datetime.now().strftime('%Y-%m-%d') today_str = datetime.now().strftime('%Y-%m-%d')
@@ -167,10 +256,11 @@ class WeatherAPI(models.Model):
} }
for forecast in forecast_data: for forecast in forecast_data:
try:
dt_txt = forecast['dt_txt'] dt_txt = forecast['dt_txt']
date_part, time_part = dt_txt.split(' ') date_part, time_part = dt_txt.split(' ')
if date_part != today_str: if date_part != today_str:
continue # chỉ lấy dữ liệu của ngày hôm nay continue
hour = int(time_part.split(':')[0]) hour = int(time_part.split(':')[0])
weather_info = { weather_info = {
@@ -180,7 +270,7 @@ class WeatherAPI(models.Model):
'temp_max': forecast['main']['temp_max'], 'temp_max': forecast['main']['temp_max'],
'humidity': forecast['main']['humidity'], 'humidity': forecast['main']['humidity'],
'wind_speed': forecast['wind']['speed'], 'wind_speed': forecast['wind']['speed'],
'description': forecast['weather'][0]['description'] 'description': forecast['weather'][0]['description'].capitalize()
} }
if 0 <= hour <= 8: if 0 <= hour <= 8:
@@ -189,4 +279,16 @@ class WeatherAPI(models.Model):
hourly_data['second'].append(weather_info) hourly_data['second'].append(weather_info)
else: else:
hourly_data['third'].append(weather_info) hourly_data['third'].append(weather_info)
except (KeyError, ValueError) as e:
_logger.warning("Invalid hourly forecast data: %s", str(e))
continue
return hourly_data return hourly_data
except Exception as e:
_logger.error("Error getting today's weather: %s", str(e))
return {
'first': [],
'second': [],
'third': []
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,51 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2106 8989 c-424 -62 -798 -376 -936 -787 -64 -190 -60 1 -60 -3084
0 -1943 3 -2830 11 -2884 60 -433 368 -804 784 -942 200 -66 -24 -62 3190 -62
3177 0 2978 -4 3167 56 346 109 632 392 747 740 66 199 62 -8 59 3129 l-3
2840 -27 100 c-47 175 -107 302 -204 433 -185 247 -475 419 -778 461 -95 13
-5860 13 -5950 0z m4442 -600 c59 -21 62 -33 62 -302 0 -231 -1 -246 -21 -270
-28 -37 -88 -52 -131 -34 -18 8 -42 24 -53 37 -19 21 -20 34 -17 264 2 132 5
249 8 259 7 24 62 57 95 57 14 0 40 -5 57 -11z m-1120 -346 c23 -21 269 -314
294 -352 48 -70 -6 -171 -92 -171 -23 0 -50 4 -60 10 -20 11 -290 332 -309
367 -22 43 -14 92 23 129 29 29 41 34 79 34 28 0 52 -6 65 -17z m2252 -8 c41
-21 60 -53 60 -102 0 -46 2 -43 -214 -305 -101 -123 -129 -139 -197 -111 -45
18 -69 55 -69 107 0 37 4 42 200 279 125 150 152 167 220 132z m-910 -513
c255 -68 474 -206 625 -395 126 -157 196 -305 235 -498 30 -144 24 -370 -13
-504 -71 -253 -198 -454 -364 -572 l-63 -45 -82 19 -82 18 -31 80 c-69 176
-133 275 -254 395 -130 130 -285 218 -476 270 -60 17 -121 24 -248 29 l-168 6
-34 102 c-59 175 -133 304 -255 442 l-72 82 45 72 c168 268 447 455 767 513
111 20 372 12 470 -14z m-2120 -376 c267 -61 489 -178 671 -353 179 -172 274
-339 349 -608 31 -114 27 -111 163 -95 172 20 309 10 429 -30 179 -59 335
-176 432 -323 76 -116 106 -193 130 -338 4 -21 13 -42 20 -48 8 -7 50 -16 95
-21 102 -12 152 -25 223 -60 239 -119 381 -355 365 -610 -11 -174 -81 -323
-207 -444 -83 -79 -173 -131 -290 -168 l-75 -23 -2065 0 c-1946 0 -2069 1
-2135 18 -207 52 -353 143 -476 296 -365 451 -199 1129 333 1359 103 44 202
64 324 66 l101 1 6 120 c9 192 54 370 137 540 183 376 557 657 974 730 103 18
401 13 496 -9z m3712 -56 c67 -52 58 -151 -18 -193 -31 -17 -397 -127 -424
-127 -23 0 -42 10 -66 34 -48 48 -46 105 3 166 10 12 412 138 444 139 22 1 46
-7 61 -19z m-201 -1069 c103 -37 198 -75 213 -84 35 -23 53 -86 36 -127 -18
-43 -56 -70 -99 -70 -31 0 -386 120 -440 149 -10 6 -26 26 -35 46 -31 64 1
134 70 155 36 10 29 12 255 -69z m-4362 -2308 c20 -17 20 -28 23 -333 l2 -315
-26 -55 c-34 -77 -79 -124 -146 -157 -46 -23 -70 -28 -132 -28 -85 0 -142 21
-203 74 -115 102 -124 306 -19 425 15 17 114 115 220 219 172 167 197 187 226
187 19 0 43 -8 55 -17z m1237 3 c18 -13 19 -32 25 -293 3 -153 2 -302 -3 -332
-22 -157 -146 -265 -303 -265 -119 0 -210 57 -262 162 -24 48 -28 69 -28 137
0 122 16 144 270 393 191 187 221 212 249 212 18 0 41 -6 52 -14z m1243 -11
c14 -13 16 -58 15 -328 l0 -312 -26 -55 c-34 -76 -79 -124 -146 -157 -74 -36
-183 -39 -253 -7 -110 51 -173 148 -174 268 0 130 20 159 286 415 208 200 212
203 247 197 20 -3 43 -12 51 -21z m-2067 -1072 c17 -15 18 -39 18 -324 0 -287
-1 -312 -21 -363 -25 -67 -91 -139 -157 -169 -68 -31 -200 -31 -259 1 -110 58
-168 149 -167 267 0 113 25 155 190 316 298 291 311 302 346 295 18 -3 40 -14
50 -23z m1211 16 c44 -20 47 -43 47 -338 0 -250 -2 -286 -20 -343 -25 -82 -81
-147 -158 -185 -48 -24 -69 -28 -137 -28 -67 0 -88 5 -130 27 -138 73 -198
228 -145 373 20 55 39 76 229 263 197 195 252 242 278 242 7 0 23 -5 36 -11z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<menuitem <menuitem web_icon="weather_forecast,static/description/icon.png"
id="menu_weather_root" id="menu_weather_root"
name="Dự báo thời tiết" name="Dự báo thời tiết"
sequence="10"/> sequence="10"/>