google_map_review/models/google_map_review.py

300 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import requests
from datetime import datetime, timedelta, timezone
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
RATING_MAP = {
'ONE': 1,
'TWO': 2,
'THREE': 3,
'FOUR': 4,
'FIVE': 5,
}
STAR_DISPLAY = {
1: '★☆☆☆☆',
2: '★★☆☆☆',
3: '★★★☆☆',
4: '★★★★☆',
5: '★★★★★',
}
class GoogleMapReview(models.Model):
_name = 'google.map.review'
_description = 'Google Business Profile Review'
_order = 'publish_date desc'
_rec_name = 'name'
name = fields.Char(
string='Review',
compute='_compute_name',
store=True,
)
company_id = fields.Many2one(
'res.company',
string='Branch / Company',
required=True,
ondelete='cascade',
index=True,
)
review_id = fields.Char(
string='Google Review ID',
index=True,
copy=False,
help='Unique review identifier returned by the Google Business Profile API.',
)
author_name = fields.Char(string='Reviewer', index=True)
rating = fields.Float(
string='Average Rating',
aggregator='avg',
digits=(16, 1),
help='Star rating 15.',
)
rating_display = fields.Char(
string='Stars',
compute='_compute_rating_display',
store=True,
)
review_text = fields.Text(string='Review Text')
reply_text = fields.Text(
string="Owner's Reply",
help='The business owner reply fetched from Google.',
)
publish_date = fields.Datetime(string='Published', index=True)
language = fields.Char(string='Language', size=10)
@api.depends('author_name', 'publish_date')
def _compute_name(self):
for rec in self:
date_str = rec.publish_date.strftime('%Y-%m-%d') if rec.publish_date else ''
rec.name = f"{rec.author_name or 'Unknown'} {date_str}"
@api.depends('rating')
def _compute_rating_display(self):
for rec in self:
rec.rating_display = STAR_DISPLAY.get(int(rec.rating or 0), '')
# ------------------------------------------------------------------
# OAuth2 helpers
# ------------------------------------------------------------------
def _get_api_credentials(self):
"""Return (client_id, client_secret, refresh_token) from system params."""
ICP = self.env['ir.config_parameter'].sudo()
client_id = ICP.get_param('google_map_review.client_id', '')
client_secret = ICP.get_param('google_map_review.client_secret', '')
refresh_token = ICP.get_param('google_map_review.refresh_token', '')
if not all([client_id, client_secret, refresh_token]):
raise UserError(_(
'Google Business Profile credentials are not configured. '
'Please go to Settings → General Settings → Google Business Reviews '
'and fill in the Client ID, Client Secret and Refresh Token.'
))
return client_id, client_secret, refresh_token
def _get_access_token(self):
"""Exchange refresh_token for a fresh access_token."""
client_id, client_secret, refresh_token = self._get_api_credentials()
resp = requests.post(
'https://oauth2.googleapis.com/token',
data={
'grant_type': 'refresh_token',
'client_id': client_id,
'client_secret': client_secret,
'refresh_token': refresh_token,
},
timeout=30,
)
resp.raise_for_status()
token_data = resp.json()
access_token = token_data.get('access_token')
if not access_token:
raise UserError(_('Failed to obtain access token from Google: %s') % resp.text)
return access_token
def _get_auth_headers(self, access_token):
return {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Accept-Language': 'id',
}
def _get_account_name(self, headers):
url = 'https://mybusinessaccountmanagement.googleapis.com/v1/accounts'
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
accounts = resp.json().get('accounts', [])
if not accounts:
raise UserError(_('No Google Business accounts found for the provided credentials.'))
return accounts[0]['name']
# ------------------------------------------------------------------
# Text cleaning helpers
# ------------------------------------------------------------------
@staticmethod
def _clean_review_text(comment):
"""Strip Google-translated prefixes/suffixes from review text."""
if not comment:
return comment
if '(Original)' in comment:
parts = comment.split('(Original)')
if len(parts) > 1:
return parts[-1].strip()
if '(Diterjemahkan oleh Google)' in comment:
parts = comment.split('\n\n(Diterjemahkan oleh Google)')
if len(parts) > 1:
return parts[0].strip()
parts = comment.split('(Diterjemahkan oleh Google)')
return parts[0].strip()
return comment
# ------------------------------------------------------------------
# Main crawl method (called by cron)
# ------------------------------------------------------------------
def action_fetch_all_reviews(self):
"""
Cron entry-point. Fetches reviews from the Google Business Profile API
for every res.company that has a google_business_id set.
Only reviews published within the last 90 days are processed.
"""
try:
access_token = self._get_access_token()
except UserError as e:
_logger.error('Google Map Review: %s', e)
return
except Exception as e:
_logger.error('Google Map Review: Failed to get access token: %s', e)
return
headers = self._get_auth_headers(access_token)
try:
account_name = self._get_account_name(headers)
except Exception as e:
_logger.error('Google Map Review: Failed to get account name: %s', e)
return
companies = self.env['res.company'].sudo().search([
('google_business_id', '!=', False),
('google_business_id', '!=', ''),
])
if not companies:
_logger.info('Google Map Review: No companies with google_business_id found.')
return
cutoff = datetime.now(timezone.utc) - timedelta(days=90)
_logger.info('Google Map Review: Processing %d companies. Cutoff: %s UTC',
len(companies), cutoff.strftime('%Y-%m-%d'))
total_upserted = 0
for company in companies:
location_id = company.google_business_id.strip()
base_url = (
f'https://mybusiness.googleapis.com/v4/'
f'{account_name}/locations/{location_id}/reviews'
)
page_token = None
company_upserted = 0
company_skipped = 0
stop_pagination = False
_logger.info('Google Map Review: Crawling "%s" (location: %s)',
company.name, location_id)
while not stop_pagination:
params = {}
if page_token:
params['pageToken'] = page_token
try:
response = requests.get(base_url, headers=headers,
params=params, timeout=30)
response.raise_for_status()
data = response.json()
except Exception as e:
_logger.warning(
'Google Map Review: Error fetching reviews for "%s": %s',
company.name, e,
)
break
reviews = data.get('reviews', [])
if not reviews:
break
for review in reviews:
# Parse publish time
create_time_str = review.get('createTime', '')
publish_time = None
if create_time_str:
try:
clean = create_time_str.split('.')[0].replace('Z', '')
publish_time = datetime.strptime(
clean, '%Y-%m-%dT%H:%M:%S'
).replace(tzinfo=timezone.utc)
except ValueError:
pass
# 90-day filter
if publish_time and publish_time < cutoff:
company_skipped += 1
stop_pagination = True
continue
review_id = review.get('reviewId')
comment = self._clean_review_text(review.get('comment', ''))
author_name = review.get('reviewer', {}).get('displayName', 'Unknown')
rating_str = review.get('starRating', '')
rating = RATING_MAP.get(rating_str, 0)
# Owner reply
reply_comment = review.get('reviewReply', {}).get('comment', '')
reply_text = reply_comment.strip() if reply_comment else False
# Upsert
publish_dt = publish_time.replace(tzinfo=None) if publish_time else False
existing = self.sudo().search([
('review_id', '=', review_id),
('company_id', '=', company.id),
], limit=1)
vals = {
'company_id': company.id,
'review_id': review_id,
'author_name': author_name,
'rating': rating,
'review_text': comment,
'reply_text': reply_text,
'publish_date': publish_dt,
}
if existing:
existing.sudo().write(vals)
else:
self.sudo().create(vals)
company_upserted += 1
total_upserted += 1
page_token = data.get('nextPageToken')
if not page_token:
break
self.env.cr.commit()
_logger.info(
'Google Map Review: "%s"%d upserted, %d skipped (>90 days)',
company.name, company_upserted, company_skipped,
)
_logger.info('Google Map Review: Crawl complete. Total upserted: %d', total_upserted)