"
+ return html
- # Lấy thông tin API key từ cấu hình
@api.model
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')
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
-
- # Lấy thông tin thành phố từ cấu hình công ty
@api.model
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
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ố."))
- return city
-
+ 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.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
def get_weather_current(self):
- """Lấy dữ liệu thời tiết hiện tại"""
- api_key = self.get_api_key()
- city = self.get_city()
- url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric"
-
- response = requests.get(url)
- if response.status_code == 200:
- data = response.json()
+ """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()
+ city = self.get_city()
+ 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)
+
return {
'temperature': data['main']['temp'],
'humidity': data['main']['humidity'],
'wind_speed': data['wind']['speed'],
- 'description': data['weather'][0]['description']
+ 'description': data['weather'][0]['description'].capitalize()
}
- else:
- 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ố."))
-
+ except KeyError as e:
+ _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
def _get_next_3days_forecast(self):
- """Lấy dữ liệu thời tiết cho 3 ngày tiếp theo"""
- api_key = self.get_api_key()
- city = self.get_city()
- url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric"
+ """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()
+ city = self.get_city()
+ url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi"
+
+ data = self._make_api_request(url)
+ forecast_data = data['list']
- response = requests.get(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']
+ # Gom dữ liệu theo ngày
+ weather_data = defaultdict(list)
+ for forecast in forecast_data:
+ try:
+ dt_txt = forecast['dt_txt']
+ date_str = dt_txt.split(' ')[0]
+ weather_data[date_str].append(forecast)
+ except (KeyError, AttributeError) as e:
+ _logger.warning("Invalid forecast data structure: %s", str(e))
+ continue
- # Gom dữ liệu theo ngày
- weather_data = defaultdict(list)
- for forecast in forecast_data:
- dt_txt = forecast['dt_txt']
- date_str = dt_txt.split(' ')[0]
- weather_data[date_str].append(forecast)
+ # Chọn 3 ngày tiếp theo (bỏ qua hôm nay)
+ today = datetime.now().date().strftime('%Y-%m-%d')
+ sorted_dates = sorted([d for d in weather_data.keys() if d != today])
+
+ if not sorted_dates:
+ return []
+
+ next_3_days = sorted_dates[:3] # Lấy 3 ngày đầu tiên sau hôm nay
+
+ result = []
+ for date in next_3_days:
+ daily_forecasts = weather_data[date]
+ if not daily_forecasts:
+ continue
+
+ temps = [f['main']['temp'] for f in daily_forecasts]
+ humidities = [f['main']['humidity'] for f in daily_forecasts]
+ descriptions = [f['weather'][0]['description'] for f in daily_forecasts]
+ wind_speeds = [f['wind']['speed'] for f in daily_forecasts]
- # Chọn 3 ngày tiếp theo
- sorted_dates = sorted(weather_data.keys())
- today = datetime.now().date()
- if today in sorted_dates:
- sorted_dates.remove(today)
- next_3_days = sorted_dates[1:4]
+ result.append({
+ 'date': date,
+ 'temp_min': min(temps),
+ 'temp_max': max(temps),
+ 'humidity': round(sum(humidities) / len(humidities), 2),
+ 'wind_speed': round(sum(wind_speeds) / len(wind_speeds), 2),
+ 'main_description': max(set(descriptions), key=descriptions.count).capitalize(),
+ })
+ return result
- result = []
- for date in next_3_days:
- daily_forecasts = weather_data[date]
- temps = [f['main']['temp'] for f in daily_forecasts]
- humidities = [f['main']['humidity'] for f in daily_forecasts]
- descriptions = [f['weather'][0]['description'] for f in daily_forecasts]
- wind_speeds = [f['wind']['speed'] for f in daily_forecasts]
+ 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."))
- result.append({
- 'date': date,
- 'temp_min': min(temps),
- 'temp_max': max(temps),
- 'humidity': round(sum(humidities) / len(humidities), 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
- })
- return result
-
-
- # Lấy thông tin thời tiết ngày hôm nay, chia làm 3 khung giờ
@api.model
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"""
- api_key = self.get_api_key()
- city = self.env.company.city
- url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric"
+ """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()
+ city = self.get_city()
+ url = f"https://api.openweathermap.org/data/2.5/forecast?q={city}&appid={api_key}&units=metric&lang=vi"
+
+ data = self._make_api_request(url)
+ forecast_data = data['list']
- response = requests.get(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']
-
- today_str = datetime.now().strftime('%Y-%m-%d')
- hourly_data = {
- 'first': [], # 1-8h
- 'second': [], # 9-16h
- 'third': [] # 17-24h
- }
-
- for forecast in forecast_data:
- dt_txt = forecast['dt_txt']
- date_part, time_part = dt_txt.split(' ')
- if date_part != today_str:
- continue # chỉ lấy dữ liệu của ngày hôm nay
-
- hour = int(time_part.split(':')[0])
- weather_info = {
- 'hour': hour,
- 'date': date_part,
- 'temp_min': forecast['main']['temp_min'],
- 'temp_max': forecast['main']['temp_max'],
- 'humidity': forecast['main']['humidity'],
- 'wind_speed': forecast['wind']['speed'],
- 'description': forecast['weather'][0]['description']
+ today_str = datetime.now().strftime('%Y-%m-%d')
+ hourly_data = {
+ 'first': [], # 1-8h
+ 'second': [], # 9-16h
+ 'third': [] # 17-24h
}
- if 0 <= hour <= 8:
- hourly_data['first'].append(weather_info)
- elif 9 <= hour <= 16:
- hourly_data['second'].append(weather_info)
- else:
- hourly_data['third'].append(weather_info)
- return hourly_data
+ for forecast in forecast_data:
+ try:
+ dt_txt = forecast['dt_txt']
+ date_part, time_part = dt_txt.split(' ')
+ if date_part != today_str:
+ continue
+
+ hour = int(time_part.split(':')[0])
+ weather_info = {
+ 'hour': hour,
+ 'date': date_part,
+ 'temp_min': forecast['main']['temp_min'],
+ 'temp_max': forecast['main']['temp_max'],
+ 'humidity': forecast['main']['humidity'],
+ 'wind_speed': forecast['wind']['speed'],
+ 'description': forecast['weather'][0]['description'].capitalize()
+ }
+
+ if 0 <= hour <= 8:
+ hourly_data['first'].append(weather_info)
+ elif 9 <= hour <= 16:
+ hourly_data['second'].append(weather_info)
+ else:
+ hourly_data['third'].append(weather_info)
+ except (KeyError, ValueError) as e:
+ _logger.warning("Invalid hourly forecast data: %s", str(e))
+ continue
+
+ return hourly_data
+
+ except Exception as e:
+ _logger.error("Error getting today's weather: %s", str(e))
+ return {
+ 'first': [],
+ 'second': [],
+ 'third': []
+ }
\ No newline at end of file
diff --git a/weather_forecast/static/description/icon.png b/weather_forecast/static/description/icon.png
new file mode 100644
index 0000000..0cc7518
Binary files /dev/null and b/weather_forecast/static/description/icon.png differ
diff --git a/weather_forecast/static/description/icon.svg b/weather_forecast/static/description/icon.svg
new file mode 100644
index 0000000..5168cab
--- /dev/null
+++ b/weather_forecast/static/description/icon.svg
@@ -0,0 +1,51 @@
+
+
+
diff --git a/weather_forecast/views/menu.xml b/weather_forecast/views/menu.xml
index 63cb1a1..6259109 100644
--- a/weather_forecast/views/menu.xml
+++ b/weather_forecast/views/menu.xml
@@ -1,7 +1,7 @@
-