first commit
This commit is contained in:
commit
3fdf07577f
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Odoo
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Google Map Review — Odoo 19 Custom Module
|
||||||
|
|
||||||
|
**Author:** Suherdy Yacob | **Version:** 19.0.1.0.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Integrates the Google Business Profile API into Odoo 19 to automatically crawl
|
||||||
|
customer reviews for each branch company and display them as structured records.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `google_business_id` field on `res.company` (Google Business Location ID)
|
||||||
|
- Hourly scheduled cron fetching reviews via **Google Business Profile API (My Business v4)**
|
||||||
|
- Review records with: author, rating (★★★★★), text, owner reply, date, language
|
||||||
|
- List / Form / Kanban reporting views
|
||||||
|
- Smart button on company form showing review count
|
||||||
|
- General Settings section for OAuth2 credentials (Client ID, Client Secret, Refresh Token)
|
||||||
|
|
||||||
|
## Authentication Strategy
|
||||||
|
|
||||||
|
Uses **OAuth2 refresh token** stored in Odoo General Settings.
|
||||||
|
The cron exchanges the refresh token for a fresh access token on each run — no browser needed.
|
||||||
|
|
||||||
|
## One-Time Setup
|
||||||
|
|
||||||
|
### 1. Google Cloud Console
|
||||||
|
|
||||||
|
1. Create/select a Google Cloud project.
|
||||||
|
2. Enable: **My Business API**, **Business Profile API**.
|
||||||
|
3. Create an OAuth 2.0 Client ID (Desktop or Web app type).
|
||||||
|
4. Note the **Client ID** and **Client Secret**.
|
||||||
|
|
||||||
|
### 2. Get Refresh Token
|
||||||
|
|
||||||
|
Run `authorize.py` from the `google_map_review` standalone project on any machine with a browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/google_map_review
|
||||||
|
python authorize.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the `refresh_token` value from the generated `token.json`.
|
||||||
|
|
||||||
|
### 3. Configure Odoo
|
||||||
|
|
||||||
|
Go to **Settings → General Settings → Google Business Reviews**
|
||||||
|
Enter Client ID, Client Secret, and Refresh Token → Save.
|
||||||
|
|
||||||
|
### 4. Set Place ID on Each Branch
|
||||||
|
|
||||||
|
Run `list_locations.py` to find location IDs, then:
|
||||||
|
**Settings → Companies → [Company] → Google Reviews tab → Google Business Place ID**
|
||||||
|
|
||||||
|
### 5. Run the Crawler
|
||||||
|
|
||||||
|
Runs automatically every hour, or trigger manually:
|
||||||
|
**Settings → Technical → Scheduled Actions → Google Reviews: Fetch from Business Profile API → Run Manually**
|
||||||
|
|
||||||
|
## Views
|
||||||
|
|
||||||
|
| Menu | View |
|
||||||
|
|------|------|
|
||||||
|
| Google Reviews → All Reviews | List (color-coded by rating) |
|
||||||
|
| Google Reviews → By Branch (Kanban) | Kanban grouped by company |
|
||||||
|
| Company form → Smart button | Filtered list for that branch |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Odoo modules: `base`, `mail`
|
||||||
|
- Python: `requests` (standard in Odoo)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
LGPL-3
|
||||||
108
README.rst
Normal file
108
README.rst
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
Google Map Review
|
||||||
|
=================
|
||||||
|
|
||||||
|
**Author:** Suherdy Yacob
|
||||||
|
|
||||||
|
**Version:** 19.0.1.0.0
|
||||||
|
|
||||||
|
**Category:** Reporting
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
|
This module integrates the Google Business Profile API into Odoo 19.
|
||||||
|
It automatically crawls customer reviews for each branch company and
|
||||||
|
displays them as structured records inside Odoo.
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- Google Business Place ID field on each ``res.company`` record
|
||||||
|
- Hourly scheduled cron (configurable) to fetch reviews via the
|
||||||
|
Google Business Profile API (My Business API v4)
|
||||||
|
- Review records stored and linked to each branch company
|
||||||
|
- Owner reply stored alongside each review
|
||||||
|
- List, Form, and Kanban reporting views
|
||||||
|
- Search filters: by branch, rating, date range, reply status
|
||||||
|
- Group-by: branch, rating, month, language
|
||||||
|
- Smart button on company form showing review count
|
||||||
|
- General Settings section for OAuth2 credentials
|
||||||
|
|
||||||
|
Authentication Strategy
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The module uses OAuth2 with a long-lived **refresh token** stored in
|
||||||
|
Odoo's General Settings. This allows the headless cron job to obtain
|
||||||
|
fresh access tokens without any browser interaction.
|
||||||
|
|
||||||
|
One-Time Setup
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Step 1 — Google Cloud Console
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
1. Create (or use existing) Google Cloud project.
|
||||||
|
2. Enable: **My Business API**, **Business Profile API**.
|
||||||
|
3. Go to **APIs & Services → Credentials → Create OAuth 2.0 Client ID**
|
||||||
|
(Application type: Desktop app or Web app).
|
||||||
|
4. Download or note your **Client ID** and **Client Secret**.
|
||||||
|
|
||||||
|
Step 2 — Obtain Refresh Token
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Run the existing ``authorize.py`` script from the
|
||||||
|
``google_map_review`` standalone project on any machine with a browser::
|
||||||
|
|
||||||
|
cd /path/to/google_map_review
|
||||||
|
python authorize.py
|
||||||
|
|
||||||
|
After completing the OAuth2 flow in the browser, open the generated
|
||||||
|
``token.json`` file and copy the ``refresh_token`` value.
|
||||||
|
|
||||||
|
Step 3 — Configure Odoo
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
1. Go to **Settings → General Settings → Google Business Reviews**.
|
||||||
|
2. Enter **Client ID**, **Client Secret**, and **Refresh Token**.
|
||||||
|
3. Click **Save**.
|
||||||
|
|
||||||
|
Step 4 — Set Place ID on Each Branch
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
1. Run ``list_locations.py`` to list all your Google Business locations.
|
||||||
|
2. Open each company in **Settings → Companies**.
|
||||||
|
3. Go to the **Google Reviews** tab.
|
||||||
|
4. Paste the Location ID into **Google Business Place ID**.
|
||||||
|
5. Save.
|
||||||
|
|
||||||
|
Step 5 — Run the Crawler
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The hourly cron runs automatically. To run it manually:
|
||||||
|
|
||||||
|
1. Go to **Settings → Technical → Scheduled Actions**.
|
||||||
|
2. Find **Google Reviews: Fetch from Business Profile API**.
|
||||||
|
3. Click **Run Manually**.
|
||||||
|
|
||||||
|
Reporting
|
||||||
|
---------
|
||||||
|
|
||||||
|
Access reviews via the **Google Reviews** top-level app menu:
|
||||||
|
|
||||||
|
- **All Reviews** — list view with color-coded rows (green=5★, red=1-2★)
|
||||||
|
- **By Branch (Kanban)** — kanban grouped by company, showing star ratings
|
||||||
|
|
||||||
|
Filter and group by rating, branch, month, or language directly from
|
||||||
|
the search bar.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
- ``base``
|
||||||
|
- ``mail``
|
||||||
|
- Python ``requests`` library (standard in Odoo environment)
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
LGPL-3
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
30
__manifest__.py
Normal file
30
__manifest__.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
'name': 'Google Map Review',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'summary': 'Crawl and display Google Business Profile reviews per branch company',
|
||||||
|
'description': """
|
||||||
|
Integrates Google Business Profile API to automatically crawl customer reviews
|
||||||
|
for each branch (res.company) and display them inside Odoo.
|
||||||
|
Features:
|
||||||
|
- Google Business Place ID field on each company
|
||||||
|
- Hourly scheduled cron to fetch reviews via Business Profile API
|
||||||
|
- Review records linked to each company
|
||||||
|
- List, Form, Kanban reporting views
|
||||||
|
- General Settings section for OAuth2 credentials
|
||||||
|
""",
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'category': 'Reporting',
|
||||||
|
'depends': ['base', 'mail'],
|
||||||
|
'data': [
|
||||||
|
'security/google_map_review_groups.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/google_map_review_cron.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/res_company_views.xml',
|
||||||
|
'views/google_map_review_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
12
data/google_map_review_cron.xml
Normal file
12
data/google_map_review_cron.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="ir_cron_google_map_review_fetch" model="ir.cron">
|
||||||
|
<field name="name">Google Reviews: Fetch from Business Profile API</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">hours</field>
|
||||||
|
<field name="model_id" ref="model_google_map_review"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.action_fetch_all_reviews()</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import res_company
|
||||||
|
from . import google_map_review
|
||||||
|
from . import res_config_settings
|
||||||
299
models/google_map_review.py
Normal file
299
models/google_map_review.py
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
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',
|
||||||
|
group_operator='avg',
|
||||||
|
digits=(16, 1),
|
||||||
|
help='Star rating 1–5.',
|
||||||
|
)
|
||||||
|
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)
|
||||||
39
models/res_company.py
Normal file
39
models/res_company.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
google_business_id = fields.Char(
|
||||||
|
string='Google Business Place ID',
|
||||||
|
help=(
|
||||||
|
'The Google Business Profile Location ID for this branch. '
|
||||||
|
'You can find it by running list_locations.py from the '
|
||||||
|
'google_map_review crawler project, or from the Google Business '
|
||||||
|
'Profile dashboard. Example: 12345678901234567890'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
google_review_count = fields.Integer(
|
||||||
|
string='Google Reviews',
|
||||||
|
compute='_compute_google_review_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('google_business_id')
|
||||||
|
def _compute_google_review_count(self):
|
||||||
|
Review = self.env['google.map.review']
|
||||||
|
for company in self:
|
||||||
|
company.google_review_count = Review.search_count([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
])
|
||||||
|
|
||||||
|
def action_view_google_reviews(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': f'Google Reviews – {self.name}',
|
||||||
|
'res_model': 'google.map.review',
|
||||||
|
'view_mode': 'list,kanban,form',
|
||||||
|
'domain': [('company_id', '=', self.id)],
|
||||||
|
'context': {'default_company_id': self.id},
|
||||||
|
}
|
||||||
29
models/res_config_settings.py
Normal file
29
models/res_config_settings.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
google_map_review_client_id = fields.Char(
|
||||||
|
string='Client ID',
|
||||||
|
config_parameter='google_map_review.client_id',
|
||||||
|
help=(
|
||||||
|
'OAuth2 Client ID from your Google Cloud Console project. '
|
||||||
|
'Found in: APIs & Services → Credentials → OAuth 2.0 Client IDs.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
google_map_review_client_secret = fields.Char(
|
||||||
|
string='Client Secret',
|
||||||
|
config_parameter='google_map_review.client_secret',
|
||||||
|
help='OAuth2 Client Secret from your Google Cloud Console project.',
|
||||||
|
)
|
||||||
|
google_map_review_refresh_token = fields.Char(
|
||||||
|
string='Refresh Token',
|
||||||
|
config_parameter='google_map_review.refresh_token',
|
||||||
|
help=(
|
||||||
|
'Long-lived OAuth2 refresh token. '
|
||||||
|
'Obtain by running authorize.py from the google_map_review '
|
||||||
|
'standalone project, then copy the refresh_token value from '
|
||||||
|
'the generated token.json file.'
|
||||||
|
),
|
||||||
|
)
|
||||||
39
security/google_map_review_groups.xml
Normal file
39
security/google_map_review_groups.xml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="module_category_google_map_review" model="ir.module.category">
|
||||||
|
<!-- Defining a new category for the app -->
|
||||||
|
<field name="name">Google Reviews</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_groups_privilege_google_map_review" model="res.groups.privilege">
|
||||||
|
<field name="name">Google Reviews</field>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
<field name="category_id" ref="module_category_google_map_review"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_google_review_user" model="res.groups">
|
||||||
|
<field name="name">User</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="privilege_id" ref="res_groups_privilege_google_map_review"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_google_review_manager" model="res.groups">
|
||||||
|
<field name="name">Manager</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="privilege_id" ref="res_groups_privilege_google_map_review"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_google_review_user'))]"/>
|
||||||
|
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Multi-Company Rule: Users can only see reviews from their allowed companies -->
|
||||||
|
<record id="google_map_review_rule_multi_company" model="ir.rule">
|
||||||
|
<field name="name">Google Map Review Multi-Company</field>
|
||||||
|
<field name="model_id" ref="model_google_map_review"/>
|
||||||
|
<field name="global" eval="True"/>
|
||||||
|
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_google_map_review_manager,google.map.review.manager,model_google_map_review,google_map_review.group_google_review_manager,1,1,1,1
|
||||||
|
access_google_map_review_user,google.map.review.user,model_google_map_review,google_map_review.group_google_review_user,1,0,0,0
|
||||||
|
6
static/description/icon.svg
Normal file
6
static/description/icon.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90" width="90" height="90">
|
||||||
|
<rect width="90" height="90" rx="18" fill="#4285F4"/>
|
||||||
|
<text x="45" y="60" font-size="50" text-anchor="middle" fill="#FBC02D" font-family="Arial">★</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
263
views/google_map_review_views.xml
Normal file
263
views/google_map_review_views.xml
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<!-- ====================== LIST VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_list" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.list</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Google Reviews" default_order="publish_date desc"
|
||||||
|
decoration-success="rating == 5"
|
||||||
|
decoration-info="rating == 4"
|
||||||
|
decoration-warning="rating == 3"
|
||||||
|
decoration-danger="rating <= 2">
|
||||||
|
<field name="publish_date" string="Date" optional="show"/>
|
||||||
|
<field name="company_id" string="Branch" optional="show"/>
|
||||||
|
<field name="author_name" string="Reviewer" optional="show"/>
|
||||||
|
<field name="rating_display" string="Rating" optional="show"/>
|
||||||
|
<field name="rating" string="Stars (num)" optional="hide"/>
|
||||||
|
<field name="review_text" string="Review" optional="show"/>
|
||||||
|
<field name="language" string="Language" optional="hide"/>
|
||||||
|
<field name="reply_text" string="Owner Reply" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== FORM VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_form" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.form</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Google Review">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="rating_display" readonly="1"
|
||||||
|
style="font-size:2rem; color:#f4c430;"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Review Details">
|
||||||
|
<field name="company_id"/>
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="publish_date"/>
|
||||||
|
<field name="language"/>
|
||||||
|
</group>
|
||||||
|
<group string="Rating">
|
||||||
|
<field name="rating"
|
||||||
|
widget="priority"
|
||||||
|
readonly="1"/>
|
||||||
|
<field name="review_id" string="Google Review ID"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<page string="Review Text" name="review_text_page">
|
||||||
|
<field name="review_text"
|
||||||
|
readonly="1"
|
||||||
|
style="min-height:120px;"/>
|
||||||
|
</page>
|
||||||
|
<page string="Owner Reply" name="reply_page"
|
||||||
|
invisible="not reply_text">
|
||||||
|
<field name="reply_text"
|
||||||
|
readonly="1"
|
||||||
|
style="min-height:80px;"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== KANBAN VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.kanban</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban default_group_by="company_id"
|
||||||
|
class="o_kanban_small_column"
|
||||||
|
quick_create="false">
|
||||||
|
<field name="company_id"/>
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="rating"/>
|
||||||
|
<field name="rating_display"/>
|
||||||
|
<field name="review_text"/>
|
||||||
|
<field name="publish_date"/>
|
||||||
|
<field name="reply_text"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div t-attf-class="oe_kanban_card oe_kanban_global_click
|
||||||
|
#{record.rating.raw_value >= 4 ? 'border-success' :
|
||||||
|
record.rating.raw_value == 3 ? 'border-warning' :
|
||||||
|
'border-danger'}">
|
||||||
|
<div class="o_kanban_record_top mb4">
|
||||||
|
<div class="o_kanban_record_headings">
|
||||||
|
<strong class="o_kanban_record_title">
|
||||||
|
<field name="author_name"/>
|
||||||
|
</strong>
|
||||||
|
<span class="o_kanban_record_subtitle text-muted">
|
||||||
|
<field name="publish_date"
|
||||||
|
widget="date"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:1.1rem; color:#f4c430;">
|
||||||
|
<field name="rating_display"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_kanban_record_body">
|
||||||
|
<p class="text-muted" style="font-size:0.85rem;
|
||||||
|
max-height:80px;
|
||||||
|
overflow:hidden;">
|
||||||
|
<field name="review_text"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div t-if="record.reply_text.raw_value"
|
||||||
|
class="o_kanban_record_bottom mt4">
|
||||||
|
<small class="text-info">
|
||||||
|
<i class="fa fa-reply"/> Owner replied
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== SEARCH VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_search" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.search</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search Google Reviews">
|
||||||
|
<field name="author_name" string="Reviewer"/>
|
||||||
|
<field name="company_id" string="Branch"/>
|
||||||
|
<field name="review_text" string="Review Text"/>
|
||||||
|
|
||||||
|
<filter name="filter_5_stars" string="5 Stars (★★★★★)"
|
||||||
|
domain="[('rating', '=', 5)]"/>
|
||||||
|
<filter name="filter_4_stars" string="4 Stars"
|
||||||
|
domain="[('rating', '=', 4)]"/>
|
||||||
|
<filter name="filter_high" string="High (≥ 4 Stars)"
|
||||||
|
domain="[('rating', '>=', 4)]"/>
|
||||||
|
<filter name="filter_low" string="Low (≤ 2 Stars)"
|
||||||
|
domain="[('rating', '<=', 2)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="filter_has_reply" string="Has Owner Reply"
|
||||||
|
domain="[('reply_text', '!=', False)]"/>
|
||||||
|
<filter name="filter_no_reply" string="No Owner Reply"
|
||||||
|
domain="[('reply_text', '=', False)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="filter_this_month" string="This Month"
|
||||||
|
domain="[('publish_date', '>=', '=1d')]"/>
|
||||||
|
<filter name="filter_last_30" string="Last 30 Days"
|
||||||
|
domain="[('publish_date', '>=', '-30d')]"/>
|
||||||
|
<filter name="filter_last_90" string="Last 90 Days"
|
||||||
|
domain="[('publish_date', '>=', '-90d')]"/>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<filter name="group_by_company" string="Branch"
|
||||||
|
context="{'group_by': 'company_id'}"/>
|
||||||
|
<filter name="group_by_rating" string="Rating"
|
||||||
|
context="{'group_by': 'rating'}"/>
|
||||||
|
<filter name="group_by_month" string="Month"
|
||||||
|
context="{'group_by': 'publish_date:month'}"/>
|
||||||
|
<filter name="group_by_language" string="Language"
|
||||||
|
context="{'group_by': 'language'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== PIVOT VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.pivot</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Reviews Analysis" sample="1">
|
||||||
|
<field name="company_id" type="row"/>
|
||||||
|
<field name="publish_date" interval="month" type="col"/>
|
||||||
|
<field name="rating" type="measure"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== GRAPH VIEW ====================== -->
|
||||||
|
<record id="view_google_map_review_graph" model="ir.ui.view">
|
||||||
|
<field name="name">google.map.review.graph</field>
|
||||||
|
<field name="model">google.map.review</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Reviews Analysis" type="bar" sample="1">
|
||||||
|
<field name="company_id" type="row"/>
|
||||||
|
<field name="rating" type="measure"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== ACTIONS ====================== -->
|
||||||
|
<record id="action_google_map_review" model="ir.actions.act_window">
|
||||||
|
<field name="name">Google Reviews</field>
|
||||||
|
<field name="res_model">google.map.review</field>
|
||||||
|
<field name="view_mode">list,kanban,pivot,graph,form</field>
|
||||||
|
<field name="search_view_id" ref="view_google_map_review_search"/>
|
||||||
|
<field name="context">{}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No Google reviews yet!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Set a <b>Google Business Place ID</b> on each company branch,
|
||||||
|
then trigger the scheduled action or wait for the hourly cron.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ====================== MENUS ====================== -->
|
||||||
|
<!-- Top-level app menu -->
|
||||||
|
<menuitem id="menu_google_review_root"
|
||||||
|
name="Google Reviews"
|
||||||
|
web_icon="google_map_review,static/description/icon.svg"
|
||||||
|
groups="google_map_review.group_google_review_user"
|
||||||
|
sequence="100"/>
|
||||||
|
|
||||||
|
<!-- Sub-menu: Reviews list -->
|
||||||
|
<menuitem id="menu_google_review_list"
|
||||||
|
name="All Reviews"
|
||||||
|
parent="menu_google_review_root"
|
||||||
|
action="action_google_map_review"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<!-- Sub-menu: Reporting (kanban grouped by company) -->
|
||||||
|
<record id="action_google_map_review_report" model="ir.actions.act_window">
|
||||||
|
<field name="name">Reviews by Branch</field>
|
||||||
|
<field name="res_model">google.map.review</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_google_map_review_search"/>
|
||||||
|
<field name="context">{'search_default_group_by_company': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_google_review_report"
|
||||||
|
name="By Branch (Kanban)"
|
||||||
|
parent="menu_google_review_root"
|
||||||
|
action="action_google_map_review_report"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<!-- Sub-menu: Reporting (pivot analysis) -->
|
||||||
|
<record id="action_google_map_review_pivot_report" model="ir.actions.act_window">
|
||||||
|
<field name="name">Reviews Analysis</field>
|
||||||
|
<field name="res_model">google.map.review</field>
|
||||||
|
<field name="view_mode">pivot,graph,list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_google_map_review_search"/>
|
||||||
|
<field name="context">{}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_google_review_pivot_report"
|
||||||
|
name="Reviews Analysis"
|
||||||
|
parent="menu_google_review_root"
|
||||||
|
action="action_google_map_review_pivot_report"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
64
views/res_company_views.xml
Normal file
64
views/res_company_views.xml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!--
|
||||||
|
Inherit res.company form view:
|
||||||
|
- Add "Google Reviews" tab with the Place ID field
|
||||||
|
- Add smart button showing total review count
|
||||||
|
-->
|
||||||
|
<record id="view_company_form_google_review" model="ir.ui.view">
|
||||||
|
<field name="name">res.company.form.google.review</field>
|
||||||
|
<field name="model">res.company</field>
|
||||||
|
<field name="inherit_id" ref="base.view_company_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Smart button area -->
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button class="oe_stat_button"
|
||||||
|
type="object"
|
||||||
|
name="action_view_google_reviews"
|
||||||
|
icon="fa-star"
|
||||||
|
invisible="not google_business_id">
|
||||||
|
<field name="google_review_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Google Reviews"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- New tab: Google Reviews -->
|
||||||
|
<xpath expr="//page[@name='general_info']" position="after">
|
||||||
|
<page name="google_reviews_tab" string="Google Reviews">
|
||||||
|
<group>
|
||||||
|
<group string="Google Business Profile">
|
||||||
|
<field name="google_business_id"
|
||||||
|
placeholder="e.g. 12345678901234567890"/>
|
||||||
|
</group>
|
||||||
|
<group string="How to find your Place ID">
|
||||||
|
<div colspan="2" class="text-muted">
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Run <code>list_locations.py</code> from the
|
||||||
|
google_map_review project on a machine with
|
||||||
|
browser access.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Copy the long numeric Location ID shown in the
|
||||||
|
output for this branch.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Paste it in the "Google Business Place ID"
|
||||||
|
field above and save.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The scheduled crawl will automatically pick
|
||||||
|
up this branch on its next run.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
69
views/res_config_settings_views.xml
Normal file
69
views/res_config_settings_views.xml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="res_config_settings_view_form_google_review" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.google.map.review</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<div class="app_settings_block" data-string="Google Reviews"
|
||||||
|
string="Google Reviews" data-key="google_map_review">
|
||||||
|
|
||||||
|
<h2>Google Business Reviews</h2>
|
||||||
|
<p class="text-muted">
|
||||||
|
Configure your Google Cloud OAuth2 credentials to enable
|
||||||
|
automated review crawling from the Google Business Profile API.
|
||||||
|
Obtain the refresh token by running <code>authorize.py</code>
|
||||||
|
from the standalone google_map_review project on any machine
|
||||||
|
with a browser, then copy the value from <code>token.json</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row mt16 o_settings_container">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane"/>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="google_map_review_client_id" string="Client ID"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
OAuth2 Client ID from Google Cloud Console →
|
||||||
|
APIs & Services → Credentials.
|
||||||
|
</div>
|
||||||
|
<field name="google_map_review_client_id"
|
||||||
|
placeholder="xxxx.apps.googleusercontent.com"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane"/>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="google_map_review_client_secret" string="Client Secret"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
OAuth2 Client Secret for your project.
|
||||||
|
</div>
|
||||||
|
<field name="google_map_review_client_secret"
|
||||||
|
password="True"
|
||||||
|
placeholder="GOCSPX-…"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-12 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane"/>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="google_map_review_refresh_token" string="Refresh Token"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Long-lived OAuth2 refresh token. Run
|
||||||
|
<code>authorize.py</code> once, then copy
|
||||||
|
<code>refresh_token</code> from <code>token.json</code>.
|
||||||
|
</div>
|
||||||
|
<field name="google_map_review_refresh_token"
|
||||||
|
password="True"
|
||||||
|
placeholder="1//0g…"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user