DEV: Probation Tracking
This commit is contained in:
parent
19669a7a3a
commit
7f8a627a8c
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import tools
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "Firebase Integration",
|
||||||
|
"version": "18.0.1.0.0",
|
||||||
|
"category": "Tools",
|
||||||
|
"author": "IdeaCode Academy",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"summary": "Integrate Firebase services and push notifications with Odoo",
|
||||||
|
"depends": ["base", "bus","mail"],
|
||||||
|
"external_dependencies": {
|
||||||
|
"python": ["firebase-admin"],
|
||||||
|
},
|
||||||
|
"data": [
|
||||||
|
"views/res_users.xml",
|
||||||
|
],
|
||||||
|
"images": ["static/description/banner.png"],
|
||||||
|
"installable": True,
|
||||||
|
"application": False,
|
||||||
|
"auto_install": False,
|
||||||
|
"price": 0.0,
|
||||||
|
"currency": "USD",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "hrmsopzento",
|
||||||
|
"private_key_id": "60336fd85549183b6c890121aaa3f08256d3f0a4",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZn3agMptdcJ7n\noZIS1E0x/3PBB0uPVljoQRrqDwwEPtwKC+Sh9DY/eiXbvXyQGfzH3LkToVgvNPCY\nsFRfPnGtp9W45bwYOi0qXc8MWzYiBLRfO663hOjh90AfTEgE02wD3DOWo9eFlzMn\nJMPrOZUD0fNpzQFXQ7o8ufdrCS40d4hDQNiMGSx2hPPOnmJioz2GOr1ABhxNFJQY\nv53L1PEP1kCEnoQcCKfpuMPNY2ApzlIEndLoBhr1W5RsHp9web22CukqtJYzVCbw\naWJymZQFSkos5rmCSCvkaTvtEYBkl//4t++WjbNSxL8C48EH1O97Hg4djYULnL1D\n/GmSIYibAgMBAAECggEAVHkYk+Bw/Fk95U2LJPHxsQmmhfPt+Yqb4jN7XgVPNcqs\noN2y9saT1Bn23g/0bP8ZZv8ffCYx08kp5yry5TGY8L5oMGhElebnJz3Yo8Q4BAZt\neVXyYNwfha7y3fM/NVhX4ju0brHUc8+YFIap4gGs/Rme8Z+Y+KWagf3xs0OSAtz3\n0d6rOsMl7UfJojChrx70CrwB0y+O+3vbD+IzInWq0RBjcu78VqCiUP+X6BuKmSgN\n2qp61FrW6eNgY1M6thL2UZeDke5hEfLcjWcdSskTj7/yONErzgFiT754IiWXMGPd\n0gGKBAJKkmmBy1stGR/5JoaqV8fVt8GFypm1LZcJEQKBgQD1EEJgyV6IyPWGzu8m\nYhW80zSQ0fQxPX9Y62UP52TecUY5qMvMOmBdec8LE18IKIRgBesCvtdtv2am0rLs\nXg/1Yrfm7AsR/eEYRY3E/8Oyk+XVteUxGR6ZgAIUSLXAsnsfx3/5CkqVTszASime\nvK2httSvmHwWaWvALnAFv3oncwKBgQDjVbUa/jBxf4N2Hcf4oYTJSfkTWa2Llwvm\n63yjcPaqm6HS1psgPd4ZiITEg4UOYpL8Cngb5hqRRIusZGatdBxkUi3AunDuOORL\nYGcn2Zka3aXtTqXzXyhXR3aQH3m2wn7KGDUs4lcoUC/x30UQKqNpWvIpfelLdNgA\n5ZyniSZAOQKBgQDu0Kt/Gn3Pmtb6SorvwsIgQ0qEnrXzjlSd2Leh6gN4arbe1cnU\n+kaSkXPc/UGs958Y3GuLP2M9BjsI82d9xKSUo2FH3ltjax+CwbVId17EljByNVJm\nqG4TdJWSItFMOiKWc5oYnZjVK/eIpD0u/fvPDhbyEA1M4espW5e7Yj+uVQKBgC2g\n7ELItixxrY8tlw9+S8qjAE0z+LNF0+u7ZD7h04CW0DojPOuRv1xcnFldFH24p0vT\nRhxDaR2zJl2poTo7Td+M5wYB5dzKqne+l7XV5PcRedZRrNlWRiCOhWuUBbf6/bvO\ndA3YOCotPhJL/+6owDfLO0O8s/CjOR+k9nZh/r1xAoGAbJJoQhBrF8sCHuTi9Hev\nRrwKnL8Ck5HPB19SkifWA9lg604w2LfRJ1ijOKSm9G1ADLT5a6XfsU7tM/6oG5Np\n3tBxhgEpX03+NS3cwRgYGdzVNS+X5D/9uduCiN0Y4cDUjyin1OT+acOOhd0YtdGe\n9Rp7Ae9GrOKPQ5GvRazbmOI=\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "firebase-adminsdk-fbsvc@hrmsopzento.iam.gserviceaccount.com",
|
||||||
|
"client_id": "111625966108394286408",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40hrmsopzento.iam.gserviceaccount.com",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
from odoo import models
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussChannelFirebaseNotify(models.Model):
|
||||||
|
_inherit = "discuss.channel"
|
||||||
|
|
||||||
|
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Extend Odoo Discuss notifications with Firebase push notifications.
|
||||||
|
"""
|
||||||
|
res = super()._notify_thread(
|
||||||
|
message,
|
||||||
|
msg_vals=msg_vals,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
recipients = self._notify_get_recipients(
|
||||||
|
message,
|
||||||
|
msg_vals or {},
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"Firebase notification processing. Channel=%s Message=%s Recipients=%s",
|
||||||
|
self.display_name,
|
||||||
|
message.id,
|
||||||
|
len(recipients),
|
||||||
|
)
|
||||||
|
|
||||||
|
notifications = []
|
||||||
|
|
||||||
|
for recipient in recipients:
|
||||||
|
user = False
|
||||||
|
|
||||||
|
# Direct user recipient
|
||||||
|
if recipient.get("uid"):
|
||||||
|
user = (
|
||||||
|
self.env["res.users"]
|
||||||
|
.sudo()
|
||||||
|
.browse(recipient["uid"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Channel member recipient (uid=False)
|
||||||
|
elif recipient.get("id"):
|
||||||
|
partner = (
|
||||||
|
self.env["res.partner"]
|
||||||
|
.sudo()
|
||||||
|
.browse(recipient["id"])
|
||||||
|
)
|
||||||
|
|
||||||
|
user = partner.user_ids[:1]
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
continue
|
||||||
|
|
||||||
|
token = user.token
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
_logger.info(
|
||||||
|
"Skipping user %s: no token",
|
||||||
|
user.name,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Record name
|
||||||
|
record_name = (
|
||||||
|
msg_vals.get("record_name")
|
||||||
|
if msg_vals and isinstance(msg_vals, dict)
|
||||||
|
else message.record_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Author
|
||||||
|
author = message.author_id
|
||||||
|
|
||||||
|
if self.channel_type == "chat":
|
||||||
|
title = author.name or "New Message"
|
||||||
|
|
||||||
|
elif self.channel_type == "channel":
|
||||||
|
title = (
|
||||||
|
f"#{record_name} - {author.name}"
|
||||||
|
if record_name
|
||||||
|
else author.name
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.channel_type == "group":
|
||||||
|
title = (
|
||||||
|
f"{record_name} - {author.name}"
|
||||||
|
if record_name
|
||||||
|
else author.name
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
title = record_name or author.name or "New Message"
|
||||||
|
|
||||||
|
# Remove html tags
|
||||||
|
body_html = (
|
||||||
|
msg_vals.get("body")
|
||||||
|
if msg_vals and msg_vals.get("body")
|
||||||
|
else message.body
|
||||||
|
)
|
||||||
|
|
||||||
|
body = re.sub(
|
||||||
|
r"<[^>]+>",
|
||||||
|
"",
|
||||||
|
body_html or "",
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"action": "mail.action_discuss",
|
||||||
|
"channel_id": str(self.id),
|
||||||
|
"message_id": str(message.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.channel_type:
|
||||||
|
payload["channel_type"] = self.channel_type
|
||||||
|
|
||||||
|
notifications.append({
|
||||||
|
"token": token,
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"data": payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"Prepared Firebase notification for user=%s",
|
||||||
|
user.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if notifications:
|
||||||
|
_logger.info(
|
||||||
|
"Sending %s Firebase notifications",
|
||||||
|
len(notifications),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.env["firebase.service"].send_notification(
|
||||||
|
notifications
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
_logger.info(
|
||||||
|
"No Firebase notifications to send"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.exception(
|
||||||
|
"Firebase notification error: %s",
|
||||||
|
exc
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
|
|
@ -0,0 +1,21 @@
|
||||||
|
<section class="oe_container">
|
||||||
|
<div class="oe_row oe_padded">
|
||||||
|
<h2 class="oe_slogan" style="color:#7C7BAD;">Firebase Integration</h2>
|
||||||
|
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
<p>Add the following key to your <code>odoo.conf</code> file:</p>
|
||||||
|
<pre style="background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 10px; border-radius: 4px;"><code>[options]
|
||||||
|
firebase_private_key_path = /home/documents/cre.json</code></pre>
|
||||||
|
|
||||||
|
<h3>Usage</h3>
|
||||||
|
<p>Import and call the function anywhere within your backend Odoo modules:</p>
|
||||||
|
<pre style="background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 10px; border-radius: 4px;"><code>from odoo.addons.firebase_integration.tools.firebase import send_firebase_notifications
|
||||||
|
|
||||||
|
msg = [
|
||||||
|
{'token': 'DEVICE_TOKEN_1', 'body': 'Notification Body', 'title': 'Notification Title'},
|
||||||
|
{'token': 'DEVICE_TOKEN_2', 'body': 'Another Body', 'title': 'Another Title'}
|
||||||
|
]
|
||||||
|
|
||||||
|
success_sent_message_count = send_firebase_notifications(msg)</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import firebase
|
||||||
|
from . import firebase_service
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import logging
|
||||||
|
import firebase_admin
|
||||||
|
from firebase_admin import credentials, messaging
|
||||||
|
from odoo.tools import config
|
||||||
|
|
||||||
|
# Fetch the credential file path safely from odoo.conf
|
||||||
|
firebase_key_path = config.get("firebase_private_key_path")
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Initialize the app safely to prevent "app already exists" runtime crashes
|
||||||
|
try:
|
||||||
|
firebase_app = firebase_admin.get_app()
|
||||||
|
except ValueError:
|
||||||
|
if firebase_key_path:
|
||||||
|
firebase_cred = credentials.Certificate(firebase_key_path)
|
||||||
|
firebase_app = firebase_admin.initialize_app(firebase_cred)
|
||||||
|
else:
|
||||||
|
# Fallback to default application credentials if config key is missing
|
||||||
|
firebase_app = firebase_admin.initialize_app()
|
||||||
|
|
||||||
|
|
||||||
|
def send_firebase_notifications(messages: list) -> int:
|
||||||
|
"""Sends list of notification messages using Firebase Cloud Messaging.
|
||||||
|
|
||||||
|
Expecting input layout:
|
||||||
|
[
|
||||||
|
{'token': '...', 'body': '...', 'title': '...'},
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
if not messages:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Build the payload using native Firebase messaging objects
|
||||||
|
fcm_messages = []
|
||||||
|
for msg in messages:
|
||||||
|
token = msg.get("token")
|
||||||
|
if not token:
|
||||||
|
continue
|
||||||
|
notification = messaging.Notification(
|
||||||
|
title=msg.get("title", ""),
|
||||||
|
body=msg.get("body", ""),
|
||||||
|
)
|
||||||
|
# include optional data payload
|
||||||
|
data_payload = msg.get("data")
|
||||||
|
fcm_messages.append(
|
||||||
|
messaging.Message(
|
||||||
|
notification=notification,
|
||||||
|
token=token,
|
||||||
|
data=data_payload if isinstance(data_payload, dict) else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not fcm_messages:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Execute the batch transmission using the Firebase Admin SDK
|
||||||
|
try:
|
||||||
|
# The SDK / FCM backend limits batch sizes; be conservative and chunk (500 is a safe upper bound).
|
||||||
|
success_total = 0
|
||||||
|
chunk_size = 500
|
||||||
|
|
||||||
|
for i in range(0, len(fcm_messages), chunk_size):
|
||||||
|
batch = fcm_messages[i : i + chunk_size]
|
||||||
|
|
||||||
|
# Try newer API first (send_all), fall back to send_multicast for older SDK versions
|
||||||
|
if hasattr(messaging, 'send_all'):
|
||||||
|
response = messaging.send_all(batch)
|
||||||
|
_logger.info(
|
||||||
|
"Firebase Success Count: %s",
|
||||||
|
response.success_count
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"Firebase Failure Count: %s",
|
||||||
|
response.failure_count
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, resp in enumerate(response.responses):
|
||||||
|
if not resp.success:
|
||||||
|
_logger.error(
|
||||||
|
"Firebase Error %s: %s",
|
||||||
|
idx,
|
||||||
|
resp.exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Older firebase-admin SDK: use send_multicast
|
||||||
|
from firebase_admin.messaging import MulticastMessage
|
||||||
|
multicast = MulticastMessage(tokens=[m.token for m in batch if m.token])
|
||||||
|
# send_multicast doesn't support per-token custom payloads; use individual sends as fallback
|
||||||
|
success_count = 0
|
||||||
|
for msg in batch:
|
||||||
|
try:
|
||||||
|
messaging.send(msg)
|
||||||
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug("Failed to send message to token %s: %s", msg.token, e)
|
||||||
|
response = type('obj', (object,), {'success_count': success_count})()
|
||||||
|
|
||||||
|
sent = getattr(response, "success_count", 0)
|
||||||
|
success_total += sent
|
||||||
|
_logger.info("Firebase: sent %s/%s messages in chunk", sent, len(batch))
|
||||||
|
return success_total
|
||||||
|
except Exception as exc: # catch firebase exceptions and log them
|
||||||
|
_logger.exception("Failed to send firebase notifications: %s", exc)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue