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