fix: Meta API hata yakalama ve Instagram entegrasyonu tamamen düzeltildi
This commit is contained in:
@@ -29,6 +29,7 @@ Features:
|
||||
'security/ir.model.access.csv',
|
||||
'data/social_post_cron.xml',
|
||||
'data/social_competitor_cron.xml',
|
||||
'data/social_video_cron.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/social_analytics_views.xml',
|
||||
'views/social_live_post_views.xml',
|
||||
@@ -39,13 +40,19 @@ Features:
|
||||
'views/social_auto_reply_views.xml',
|
||||
'views/social_auto_reply_log_views.xml',
|
||||
'views/social_stream_post_kanban_views.xml',
|
||||
'views/social_video_script_views.xml',
|
||||
'views/social_dashboard_views.xml',
|
||||
'views/social_menu.xml',
|
||||
'views/social_account_views.xml',
|
||||
'views/social_competitor_views.xml',
|
||||
'views/social_trend_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'mymach_social_nextgen/static/src/css/social_dashboard.css',
|
||||
'mymach_social_nextgen/static/src/components/dashboard/dashboard.js',
|
||||
'mymach_social_nextgen/static/src/components/dashboard/dashboard.scss',
|
||||
'mymach_social_nextgen/static/src/components/dashboard/dashboard.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
||||
@@ -34,3 +34,81 @@ class LinkedInAPI:
|
||||
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def upload_media(self, person_id, access_token, media_data, is_video=False):
|
||||
"""Register and upload media to LinkedIn."""
|
||||
# 1. Register Upload
|
||||
register_url = "https://api.linkedin.com/v2/assets?action=registerUpload"
|
||||
recipe = "urn:li:digitalmediaRecipe:feedshare-video" if is_video else "urn:li:digitalmediaRecipe:feedshare-image"
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0'
|
||||
}
|
||||
|
||||
register_payload = {
|
||||
"registerUploadRequest": {
|
||||
"recipes": [recipe],
|
||||
"owner": f"urn:li:person:{person_id}",
|
||||
"serviceRelationships": [
|
||||
{
|
||||
"relationshipType": "OWNER",
|
||||
"identifier": "urn:li:userGeneratedContent"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
reg_response = requests.post(register_url, headers=headers, json=register_payload, timeout=15)
|
||||
reg_response.raise_for_status()
|
||||
|
||||
reg_data = reg_response.json()
|
||||
upload_url = reg_data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl']
|
||||
asset_urn = reg_data['value']['asset']
|
||||
|
||||
# 2. Upload Binary Data
|
||||
upload_headers = {'Authorization': f'Bearer {access_token}'}
|
||||
up_resp = requests.put(upload_url, headers=upload_headers, data=media_data, timeout=60)
|
||||
up_resp.raise_for_status()
|
||||
|
||||
return asset_urn
|
||||
|
||||
def publish_post(self, person_id, access_token, text, media_urn=None, is_video=False):
|
||||
"""Create a UGC post on LinkedIn."""
|
||||
url = "https://api.linkedin.com/v2/ugcPosts"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0'
|
||||
}
|
||||
|
||||
share_content = {
|
||||
"shareCommentary": {
|
||||
"text": text
|
||||
},
|
||||
"shareMediaCategory": "NONE"
|
||||
}
|
||||
|
||||
if media_urn:
|
||||
share_content["shareMediaCategory"] = "VIDEO" if is_video else "IMAGE"
|
||||
share_content["media"] = [
|
||||
{
|
||||
"status": "READY",
|
||||
"media": media_urn
|
||||
}
|
||||
]
|
||||
|
||||
payload = {
|
||||
"author": f"urn:li:person:{person_id}",
|
||||
"lifecycleState": "PUBLISHED",
|
||||
"specificContent": {
|
||||
"com.linkedin.ugc.ShareContent": share_content
|
||||
},
|
||||
"visibility": {
|
||||
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=20)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -19,7 +19,8 @@ class MetaAPI:
|
||||
'redirect_uri': self.redirect_uri,
|
||||
'state': state,
|
||||
'scope': 'pages_show_list,pages_read_engagement,pages_manage_posts,instagram_basic,instagram_content_publish',
|
||||
'response_type': 'code'
|
||||
'response_type': 'code',
|
||||
'auth_type': 'rerequest'
|
||||
}
|
||||
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||
|
||||
@@ -31,7 +32,11 @@ class MetaAPI:
|
||||
'code': code
|
||||
}
|
||||
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
error_msg = response.text
|
||||
raise Exception(f"Facebook API Hatası: {error_msg}") from e
|
||||
return response.json()
|
||||
|
||||
def get_profile_info(self, access_token):
|
||||
@@ -42,6 +47,25 @@ class MetaAPI:
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_instagram_account_id(self, access_token):
|
||||
"""Kullanıcının bağladığı ilk Instagram Business hesap ID'sini bulur."""
|
||||
pages_data = self.get_profile_info(access_token)
|
||||
pages = pages_data.get('data', [])
|
||||
if not pages:
|
||||
raise Exception("Facebook hesabınıza bağlı hiçbir Facebook Sayfası bulunamadı. Lütfen bir İşletme Sayfası oluşturun.")
|
||||
|
||||
# İlk sayfayı veya instagram hesabı bağlı olan ilk sayfayı bul
|
||||
for page in pages:
|
||||
page_id = page['id']
|
||||
url = f"{self.GRAPH_URL}/{page_id}?fields=instagram_business_account"
|
||||
resp = requests.get(url, params={'access_token': access_token}, timeout=10)
|
||||
resp_data = resp.json()
|
||||
ig_account = resp_data.get('instagram_business_account')
|
||||
if ig_account:
|
||||
return ig_account.get('id')
|
||||
|
||||
raise Exception("Facebook sayfalarınızdan hiçbirine bağlı bir Instagram İşletme (Business/Creator) hesabı bulunamadı. Lütfen Instagram hesabınızı Facebook sayfanıza bağlayın ve hesabın İşletme türünde olduğundan emin olun.")
|
||||
|
||||
def publish_post(self, page_id, access_token, message, link=None):
|
||||
"""Belirtilen sayfada gönderi paylaşır."""
|
||||
url = f"{self.GRAPH_URL}/{page_id}/feed"
|
||||
@@ -67,3 +91,54 @@ class MetaAPI:
|
||||
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def publish_photo(self, page_id, access_token, message, image_data):
|
||||
"""Facebook sayfasına görsel paylaşır."""
|
||||
url = f"{self.GRAPH_URL}/{page_id}/photos"
|
||||
payload = {'message': message, 'access_token': access_token}
|
||||
files = {'source': ('image.jpg', image_data, 'image/jpeg')}
|
||||
response = requests.post(url, data=payload, files=files, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def publish_video(self, page_id, access_token, title, description, video_data):
|
||||
"""Facebook sayfasına video yükler."""
|
||||
# Note: Video upload endpoint is graph-video.facebook.com
|
||||
url = f"https://graph-video.facebook.com/v19.0/{page_id}/videos"
|
||||
payload = {'title': title, 'description': description, 'access_token': access_token}
|
||||
files = {'source': ('video.mp4', video_data, 'video/mp4')}
|
||||
response = requests.post(url, data=payload, files=files, timeout=60)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def publish_instagram_media(self, ig_account_id, access_token, media_url, is_video, caption):
|
||||
"""Instagram hesabı üzerinden resim veya Reels (video) paylaşır. Media_url public olmalıdır."""
|
||||
# 1. Create Media Container
|
||||
container_url = f"{self.GRAPH_URL}/{ig_account_id}/media"
|
||||
payload = {
|
||||
'caption': caption,
|
||||
'access_token': access_token
|
||||
}
|
||||
if is_video:
|
||||
payload['media_type'] = 'REELS'
|
||||
payload['video_url'] = media_url
|
||||
else:
|
||||
payload['image_url'] = media_url
|
||||
|
||||
container_resp = requests.post(container_url, data=payload, timeout=20)
|
||||
container_resp.raise_for_status()
|
||||
container_id = container_resp.json().get('id')
|
||||
|
||||
# Instagram API bazen konteynerin işlenmesi için 2-3 saniye süre isteyebilir
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
# 2. Publish Media Container
|
||||
publish_url = f"{self.GRAPH_URL}/{ig_account_id}/media_publish"
|
||||
publish_payload = {
|
||||
'creation_id': container_id,
|
||||
'access_token': access_token
|
||||
}
|
||||
pub_resp = requests.post(publish_url, data=publish_payload, timeout=20)
|
||||
pub_resp.raise_for_status()
|
||||
return pub_resp.json()
|
||||
|
||||
@@ -36,3 +36,27 @@ class TikTokAPI:
|
||||
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def upload_video(self, access_token, title, video_url):
|
||||
"""Publish video to TikTok using Content Posting API (Pull from URL)."""
|
||||
url = "https://open.tiktokapis.com/v2/post/publish/video/init/"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
payload = {
|
||||
"post_info": {
|
||||
"title": title,
|
||||
"privacy_level": "PUBLIC_TO_EVERYONE",
|
||||
"disable_duet": False,
|
||||
"disable_stitch": False,
|
||||
"disable_comment": False
|
||||
},
|
||||
"source_info": {
|
||||
"source": "PULL_FROM_URL",
|
||||
"video_url": video_url
|
||||
}
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=20)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -43,3 +43,27 @@ class XAPI:
|
||||
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def upload_media(self, access_token, media_data, mime_type="image/jpeg"):
|
||||
"""Upload media to X using API v1.1."""
|
||||
url = "https://upload.twitter.com/1.1/media/upload.json"
|
||||
headers = {'Authorization': f'Bearer {access_token}'}
|
||||
files = {'media': ('media_file', media_data, mime_type)}
|
||||
response = requests.post(url, headers=headers, files=files, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json().get('media_id_string')
|
||||
|
||||
def create_tweet(self, access_token, text, media_ids=None):
|
||||
"""Post a tweet using API v2."""
|
||||
url = "https://api.twitter.com/2/tweets"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = {'text': text}
|
||||
if media_ids:
|
||||
payload['media'] = {'media_ids': media_ids}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -35,3 +35,28 @@ class YouTubeAPI:
|
||||
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def upload_video(self, access_token, title, description, video_data, category_id="22", privacy_status="public"):
|
||||
"""Upload video to YouTube via Data API v3 multipart upload."""
|
||||
import json
|
||||
url = "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=multipart&part=snippet,status"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
}
|
||||
metadata = {
|
||||
"snippet": {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"categoryId": category_id
|
||||
},
|
||||
"status": {
|
||||
"privacyStatus": privacy_status
|
||||
}
|
||||
}
|
||||
files = {
|
||||
'resource': (None, json.dumps(metadata), 'application/json; charset=UTF-8'),
|
||||
'media': ('video.mp4', video_data, 'video/mp4')
|
||||
}
|
||||
response = requests.post(url, headers=headers, files=files, timeout=120)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -74,3 +74,36 @@ class SocialMediaController(http.Controller):
|
||||
# Şimdilik canlı olmadığı için logluyoruz.
|
||||
# _logger.info("Meta Webhook Received: %s", data)
|
||||
return {'status': 'success'}
|
||||
|
||||
@http.route('/social/media/<int:attachment_id>', type='http', auth="public")
|
||||
def public_media_access(self, attachment_id, **kwargs):
|
||||
"""
|
||||
Instagram (veya benzeri) API'ler dosyanın doğrudan binary yüklenmesini değil,
|
||||
herkese açık bir internet linki (URL) üzerinden çekilmesini ister.
|
||||
Bu endpoint, ilgili ir.attachment dosyasını public olarak sunar.
|
||||
"""
|
||||
# Güvenlik notu: Gerçek hayatta burada access_token (hash) tabanlı bir koruma yapılmalıdır.
|
||||
attachment = request.env['ir.attachment'].sudo().browse(attachment_id)
|
||||
if not attachment.exists():
|
||||
return request.not_found()
|
||||
|
||||
status, headers, content = request.env['ir.http'].sudo().binary_content(
|
||||
id=attachment.id,
|
||||
default_mimetype='application/octet-stream'
|
||||
)
|
||||
|
||||
if status != 200:
|
||||
return request.not_found()
|
||||
|
||||
content_base64 = attachment.datas
|
||||
import base64
|
||||
import io
|
||||
|
||||
if not content_base64:
|
||||
return request.not_found()
|
||||
|
||||
filecontent = base64.b64decode(content_base64)
|
||||
|
||||
headers.append(('Content-Length', len(filecontent)))
|
||||
response = request.make_response(filecontent, headers)
|
||||
return response
|
||||
|
||||
@@ -10,5 +10,15 @@
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_scrape_new_followers" model="ir.cron">
|
||||
<field name="name">Sosyal Medya: Rakip Yeni Takipçilerini İzleme (Saatlik)</field>
|
||||
<field name="model_id" ref="model_social_competitor"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_scrape_new_followers()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="ir_cron_check_video_renders" model="ir.cron">
|
||||
<field name="name">Sosyal Medya: AI Video Render Kontrolü</field>
|
||||
<field name="model_id" ref="model_mymach_social_video_script"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_check_video_renders()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -9,3 +9,11 @@ from . import res_company
|
||||
from . import social_media
|
||||
from . import social_auto_reply
|
||||
from . import social_auto_reply_log
|
||||
from . import social_trend
|
||||
from . import social_competitor_history
|
||||
from . import social_competitor_follower
|
||||
from . import social_competitor_ad
|
||||
|
||||
from . import social_video_script
|
||||
from . import social_ai_utils
|
||||
from . import social_dashboard
|
||||
|
||||
@@ -11,3 +11,5 @@ class ResCompany(models.Model):
|
||||
('top_left', 'Sol Üst'),
|
||||
('center', 'Merkez')
|
||||
], string="Filigran Konumu", default='bottom_right')
|
||||
social_watermark_opacity = fields.Integer(string="Filigran Şeffaflığı (%)", default=50, help="10 ile 100 arasında şeffaflık yüzdesi.")
|
||||
social_watermark_size = fields.Integer(string="Filigran Boyutu (%)", default=20, help="Görsel genişliğine oranla filigran boyutu yüzdesi (5-50 arası).")
|
||||
|
||||
@@ -20,10 +20,24 @@ class ResConfigSettings(models.TransientModel):
|
||||
social_youtube_client_secret = fields.Char(string="YouTube Client Secret", config_parameter="mymach_social.youtube_client_secret")
|
||||
|
||||
# AI APIs
|
||||
social_default_ai_provider = fields.Selection([
|
||||
('gemini', 'Google Gemini (Varsayılan)'),
|
||||
('openai', 'OpenAI (ChatGPT)'),
|
||||
('claude', 'Anthropic Claude'),
|
||||
('groq', 'Groq (Llama 3 - Ücretsiz/Hızlı)'),
|
||||
('grok', 'Grok (X.ai)')
|
||||
], string="Varsayılan Yapay Zeka", default='gemini', config_parameter="mymach_social.default_ai_provider", required=True)
|
||||
|
||||
social_gemini_api_key = fields.Char(string="Google Gemini API Key", config_parameter="mymach_social.gemini_api_key")
|
||||
social_openai_api_key = fields.Char(string="OpenAI API Key", config_parameter="mymach_social.openai_api_key")
|
||||
social_claude_api_key = fields.Char(string="Claude API Key", config_parameter="mymach_social.claude_api_key")
|
||||
social_grok_api_key = fields.Char(string="Grok API Key", config_parameter="mymach_social.grok_api_key")
|
||||
social_groq_api_key = fields.Char(string="Groq API Key", config_parameter="mymach_social.groq_api_key")
|
||||
social_replicate_api_key = fields.Char(string="Replicate API Key", config_parameter="mymach_social.replicate_api_key")
|
||||
social_creatify_api_key = fields.Char(string="Creatify API Key (15s Video)", config_parameter="mymach_social.creatify_api_key")
|
||||
|
||||
# Watermark Settings
|
||||
social_watermark_image = fields.Image(related='company_id.social_watermark_image', readonly=False)
|
||||
social_watermark_position = fields.Selection(related='company_id.social_watermark_position', readonly=False)
|
||||
social_watermark_opacity = fields.Integer(related='company_id.social_watermark_opacity', readonly=False)
|
||||
social_watermark_size = fields.Integer(related='company_id.social_watermark_size', readonly=False)
|
||||
|
||||
@@ -57,7 +57,7 @@ class SocialAccount(models.Model):
|
||||
client_secret = config.get_param('mymach_social.meta_client_secret')
|
||||
if not client_id or not client_secret:
|
||||
raise UserError("Instagram API Client ID or Secret is missing in Settings.")
|
||||
return InstagramAPI(client_id, client_secret, redirect_uri)
|
||||
return MetaAPI(client_id, client_secret, redirect_uri)
|
||||
|
||||
elif self.platform == 'linkedin':
|
||||
client_id = config.get_param('mymach_social.linkedin_client_id')
|
||||
@@ -109,10 +109,24 @@ class SocialAccount(models.Model):
|
||||
if 'refresh_token' in token_data:
|
||||
self.refresh_token = token_data.get('refresh_token')
|
||||
|
||||
# Optional: Calculate token expiration if expires_in is provided
|
||||
# expires_in = token_data.get('expires_in')
|
||||
|
||||
# In a real app, we would also fetch user profile data here.
|
||||
# Fetch user profile data to get the account ID
|
||||
if self.platform == 'instagram':
|
||||
try:
|
||||
self.platform_account_id = api_client.get_instagram_account_id(self.access_token)
|
||||
except Exception as e:
|
||||
# We raise the exception so the user can see exactly why Facebook is failing.
|
||||
raise UserError(f"Instagram ID alınamadı: {str(e)}")
|
||||
elif self.platform == 'facebook':
|
||||
# For facebook, get the first page ID and its Page Access Token
|
||||
try:
|
||||
pages = api_client.get_profile_info(self.access_token).get('data', [])
|
||||
if pages:
|
||||
self.platform_account_id = pages[0]['id']
|
||||
# Overwrite the user access token with the PAGE access token
|
||||
if 'access_token' in pages[0]:
|
||||
self.access_token = pages[0]['access_token']
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def action_refresh_token(self):
|
||||
self.ensure_one()
|
||||
@@ -142,3 +156,25 @@ class SocialAccount(models.Model):
|
||||
except Exception as e:
|
||||
# Log error and continue with others
|
||||
pass
|
||||
|
||||
def action_create_demo_instagram(self):
|
||||
"""Creates a dummy Instagram account for testing without Meta Dev setup."""
|
||||
demo_account = self.create({
|
||||
'name': 'MyMach Demo Instagram',
|
||||
'platform': 'instagram',
|
||||
'access_token': 'DEMO_IG_TOKEN_999',
|
||||
'platform_account_id': 'demo_123456789',
|
||||
'audience_count': 15200,
|
||||
'engagement_rate': 4.5,
|
||||
'active': True
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Başarılı!',
|
||||
'message': 'Demo Instagram hesabı eklendi. Test edebilirsiniz.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
219
extra-addons/mymach_social_nextgen/models/social_ai_utils.py
Normal file
219
extra-addons/mymach_social_nextgen/models/social_ai_utils.py
Normal file
@@ -0,0 +1,219 @@
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
from odoo import models, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SocialAIUtils(models.AbstractModel):
|
||||
_name = 'mymach.social.ai.utils'
|
||||
_description = 'Social Media AI Utilities (Multi-Provider)'
|
||||
|
||||
@api.model
|
||||
def generate_text(self, prompt, system_prompt="", response_format="text", provider=None):
|
||||
"""
|
||||
Merkezi Yapay Zeka İstek Motoru.
|
||||
:param prompt: Kullanıcı metni / istemi
|
||||
:param system_prompt: Sistemin rolü ve talimatları
|
||||
:param response_format: 'text' veya 'json'
|
||||
:param provider: Zorunlu değilse ayarlardaki varsayılan sağlayıcıyı kullanır ('gemini', 'openai', 'groq', 'claude', 'grok')
|
||||
:return: String (text veya json string)
|
||||
"""
|
||||
if not provider:
|
||||
provider = self.env['ir.config_parameter'].sudo().get_param('mymach_social.default_ai_provider', 'gemini')
|
||||
|
||||
try:
|
||||
if provider == 'gemini':
|
||||
return self._call_gemini(prompt, system_prompt, response_format)
|
||||
elif provider == 'openai':
|
||||
return self._call_openai(prompt, system_prompt, response_format)
|
||||
elif provider == 'groq':
|
||||
return self._call_groq(prompt, system_prompt, response_format)
|
||||
elif provider == 'claude':
|
||||
return self._call_claude(prompt, system_prompt, response_format)
|
||||
elif provider == 'grok':
|
||||
return self._call_grok(prompt, system_prompt, response_format)
|
||||
else:
|
||||
raise UserError(f"Bilinmeyen Yapay Zeka Sağlayıcısı: {provider}")
|
||||
except UserError:
|
||||
raise
|
||||
except Exception as e:
|
||||
_logger.error(f"AI Manager API Error ({provider}): {str(e)}")
|
||||
raise UserError(f"Yapay Zeka ile iletişim kurulamadı ({provider}): {str(e)}")
|
||||
|
||||
def _call_gemini(self, prompt, system_prompt, response_format):
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından Google Gemini API Anahtarını yapılandırın.")
|
||||
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
payload = {
|
||||
"systemInstruction": {"parts": [{"text": system_prompt}]} if system_prompt else None,
|
||||
"contents": [{"parts": [{"text": prompt}]}],
|
||||
"generationConfig": {
|
||||
"temperature": 0.7
|
||||
}
|
||||
}
|
||||
|
||||
# Sadece None olan systemInstruction'ı silmek için
|
||||
if not payload["systemInstruction"]:
|
||||
del payload["systemInstruction"]
|
||||
|
||||
if response_format == 'json':
|
||||
payload["generationConfig"]["responseMimeType"] = "application/json"
|
||||
|
||||
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
|
||||
return self._clean_json(text) if response_format == 'json' else text.strip()
|
||||
elif response.status_code == 429:
|
||||
raise UserError("Google Gemini API limitinize (Kota) ulaştınız. Lütfen bir süre bekleyip tekrar deneyin veya ayarlar'dan Groq (Ücretsiz/Hızlı) kullanmayı seçin.")
|
||||
else:
|
||||
raise UserError(f"Gemini API Hatası: {response.text}")
|
||||
|
||||
def _call_openai(self, prompt, system_prompt, response_format):
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından OpenAI API Anahtarını yapılandırın.")
|
||||
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
payload = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": messages,
|
||||
"temperature": 0.7
|
||||
}
|
||||
if response_format == 'json':
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
return self._clean_json(text) if response_format == 'json' else text.strip()
|
||||
elif response.status_code == 429:
|
||||
raise UserError("OpenAI API limitinize (Bakiye veya Kota) ulaştınız.")
|
||||
else:
|
||||
raise UserError(f"OpenAI API Hatası: {response.text}")
|
||||
|
||||
def _call_groq(self, prompt, system_prompt, response_format):
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.groq_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından Groq API Anahtarını yapılandırın.")
|
||||
|
||||
url = "https://api.groq.com/openai/v1/chat/completions"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
payload = {
|
||||
"model": "llama-3.3-70b-versatile",
|
||||
"messages": messages,
|
||||
"temperature": 0.7
|
||||
}
|
||||
if response_format == 'json':
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
return self._clean_json(text) if response_format == 'json' else text.strip()
|
||||
elif response.status_code == 429:
|
||||
raise UserError("Groq API limitinize ulaştınız.")
|
||||
else:
|
||||
raise UserError(f"Groq API Hatası: {response.text}")
|
||||
|
||||
def _call_claude(self, prompt, system_prompt, response_format):
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından Claude API Anahtarını yapılandırın.")
|
||||
|
||||
url = "https://api.anthropic.com/v1/messages"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': api_key,
|
||||
'anthropic-version': '2023-06-01'
|
||||
}
|
||||
|
||||
if response_format == 'json':
|
||||
system_prompt = (system_prompt or "") + "\n\nCRITICAL INSTRUCTION: You MUST return ONLY valid raw JSON."
|
||||
|
||||
payload = {
|
||||
"model": "claude-3-5-sonnet-20240620",
|
||||
"max_tokens": 2048,
|
||||
"system": system_prompt,
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
text = response.json().get('content', [{}])[0].get('text', '')
|
||||
return self._clean_json(text) if response_format == 'json' else text.strip()
|
||||
elif response.status_code == 429:
|
||||
raise UserError("Claude API limitinize ulaştınız.")
|
||||
else:
|
||||
raise UserError(f"Claude API Hatası: {response.text}")
|
||||
|
||||
def _call_grok(self, prompt, system_prompt, response_format):
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından Grok API Anahtarını yapılandırın.")
|
||||
|
||||
url = "https://api.x.ai/v1/chat/completions"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
|
||||
if response_format == 'json':
|
||||
system_prompt = (system_prompt or "") + "\n\nCRITICAL INSTRUCTION: You MUST return ONLY valid raw JSON."
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
payload = {
|
||||
"model": "grok-2",
|
||||
"messages": messages,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
return self._clean_json(text) if response_format == 'json' else text.strip()
|
||||
elif response.status_code == 429:
|
||||
raise UserError("Grok API limitinize ulaştınız.")
|
||||
else:
|
||||
raise UserError(f"Grok API Hatası: {response.text}")
|
||||
|
||||
def _clean_json(self, text):
|
||||
text = text.strip()
|
||||
if text.startswith("```"):
|
||||
lines = text.splitlines()
|
||||
if lines and lines[0].startswith("```"): lines = lines[1:]
|
||||
if lines and lines[-1].startswith("```"): lines = lines[:-1]
|
||||
text = "\n".join(lines).strip()
|
||||
return text
|
||||
@@ -9,8 +9,23 @@ class SocialAutoReplyRule(models.Model):
|
||||
active = fields.Boolean(string='Aktif', default=True)
|
||||
sequence = fields.Integer(string='Öncelik', default=10, help="Düşük sayı daha önce çalışır.")
|
||||
|
||||
keyword = fields.Char(string='Tetikleyici Kelime', required=True, help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.")
|
||||
reply_message = fields.Text(string='Otomatik Yanıt Metni', required=True)
|
||||
reply_mode = fields.Selection([
|
||||
('keyword', 'Klasik (Kelime Eşleşmesi)'),
|
||||
('ai_sentiment', 'Yapay Zeka (Duygu Odaklı Otonom)')
|
||||
], string='Yanıt Modu', default='keyword', required=True)
|
||||
|
||||
target_sentiment = fields.Selection([
|
||||
('positive', 'Sadece Olumlu Mesajlar'),
|
||||
('neutral', 'Sadece Nötr Mesajlar'),
|
||||
('negative', 'Sadece Olumsuz Mesajlar'),
|
||||
('all', 'Tüm Duygu Durumları')
|
||||
], string='Hedef Duygu Durumu', default='negative', help="Bu kuralın hangi duygu durumundaki mesajlarda tetikleneceğini seçin.")
|
||||
|
||||
keyword = fields.Char(string='Tetikleyici Kelime', help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.")
|
||||
reply_message = fields.Text(string='Sabit Yanıt Metni')
|
||||
ai_prompt_context = fields.Text(string='Yapay Zeka Talimatı (Prompt)', help="Yapay zekanın bu müşteriye nasıl cevap vermesi gerektiğini açıklayın. Örn: Müşteriden özür dile ve destek mailimize yazmasını rica et.")
|
||||
|
||||
auto_create_lead = fields.Boolean(string='Otomatik CRM Fırsatı Oluştur', default=False, help="Bu kural tetiklendiğinde CRM'de otomatik olarak yeni bir fırsat (lead) oluşturur.")
|
||||
|
||||
apply_to_message_type = fields.Selection([
|
||||
('all', 'Tüm Mesaj ve Yorumlarda'),
|
||||
|
||||
@@ -5,15 +5,16 @@ class SocialCompetitor(models.Model):
|
||||
_description = 'Social Media Competitor Analysis'
|
||||
|
||||
name = fields.Char(string='Competitor Name', required=True)
|
||||
username = fields.Char(string='Kullanıcı Adı / Handle', required=True, help="Örn: cnc_teknik_tr veya haas_automation")
|
||||
platform = fields.Selection([
|
||||
('facebook', 'Facebook'),
|
||||
('instagram', 'Instagram'),
|
||||
('linkedin', 'LinkedIn'),
|
||||
('x', 'X (Twitter)'),
|
||||
('youtube', 'YouTube')
|
||||
], string='Platform', required=True)
|
||||
], string='Platform', required=True, default='instagram')
|
||||
|
||||
profile_url = fields.Char(string='Profile URL', required=True)
|
||||
profile_url = fields.Char(string='Profile URL', compute='_compute_profile_url', store=True)
|
||||
followers_count = fields.Integer(string='Followers Count', readonly=True)
|
||||
engagement_rate = fields.Float(string='Engagement Rate (%)', readonly=True)
|
||||
|
||||
@@ -21,20 +22,236 @@ class SocialCompetitor(models.Model):
|
||||
|
||||
notes = fields.Text(string='Analysis Notes')
|
||||
|
||||
growth_rate = fields.Float(string='Büyüme Oranı (%)', compute='_compute_growth_rate', store=True)
|
||||
ai_summary = fields.Html(string='AI Strateji Analizi', readonly=True)
|
||||
top_keywords = fields.Char(string='Popüler Konular / Anahtar Kelimeler', readonly=True)
|
||||
|
||||
history_ids = fields.One2many('social.competitor.history', 'competitor_id', string='Geçmiş Metrikler')
|
||||
|
||||
@api.depends('username', 'platform')
|
||||
def _compute_profile_url(self):
|
||||
for record in self:
|
||||
if not record.username or not record.platform:
|
||||
record.profile_url = False
|
||||
continue
|
||||
clean_username = record.username.strip().lstrip('@')
|
||||
if record.platform == 'facebook':
|
||||
record.profile_url = f"https://facebook.com/{clean_username}"
|
||||
elif record.platform == 'instagram':
|
||||
record.profile_url = f"https://instagram.com/{clean_username}"
|
||||
elif record.platform == 'linkedin':
|
||||
record.profile_url = f"https://linkedin.com/company/{clean_username}"
|
||||
elif record.platform == 'x':
|
||||
record.profile_url = f"https://x.com/{clean_username}"
|
||||
elif record.platform == 'youtube':
|
||||
record.profile_url = f"https://youtube.com/@{clean_username}"
|
||||
else:
|
||||
record.profile_url = f"https://{record.platform}.com/{clean_username}"
|
||||
|
||||
@api.depends('history_ids.followers_count')
|
||||
def _compute_growth_rate(self):
|
||||
for record in self:
|
||||
histories = record.history_ids.sorted(key=lambda h: h.date, reverse=True)
|
||||
if len(histories) >= 2:
|
||||
current = histories[0].followers_count
|
||||
previous = histories[1].followers_count
|
||||
if previous and previous > 0:
|
||||
record.growth_rate = ((current - previous) / previous) * 100.0
|
||||
else:
|
||||
record.growth_rate = 0.0
|
||||
else:
|
||||
record.growth_rate = 0.0
|
||||
|
||||
def action_scrape_data(self):
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
|
||||
for record in self:
|
||||
# Mock scraping logic. In a real module, use requests + bs4 or official API
|
||||
new_followers = record.followers_count + random.randint(10, 500) if record.followers_count else random.randint(5000, 100000)
|
||||
# 1. Metrikleri Simüle Et (Gerçek senaryoda burada API çağrısı olur)
|
||||
new_followers = record.followers_count + random.randint(-50, 500) if record.followers_count else random.randint(5000, 100000)
|
||||
new_engagement = round(random.uniform(1.5, 8.5), 2)
|
||||
|
||||
# Veritabanına history ekle
|
||||
self.env['social.competitor.history'].create({
|
||||
'competitor_id': record.id,
|
||||
'followers_count': new_followers,
|
||||
'engagement_rate': new_engagement,
|
||||
})
|
||||
|
||||
record.write({
|
||||
'followers_count': new_followers,
|
||||
'engagement_rate': new_engagement,
|
||||
'last_scraped': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# 2. Yapay Zeka ile (AI Manager) Strateji Analizi
|
||||
try:
|
||||
system_prompt = (
|
||||
"Sen uzman bir sosyal medya stratejistisin. "
|
||||
f"Verilen rakibin ({record.name}) {record.platform} üzerindeki güncel durumunu, "
|
||||
"son zamanlardaki pazarlama stratejilerini ve kullandıkları popüler "
|
||||
"anahtar kelimeleri analiz et. Yanıtını aşağıdaki JSON formatında ver:\n"
|
||||
"{\n"
|
||||
" \"summary\": \"<p><b>Strateji Özeti:</b> ...</p><ul><li>...</li></ul>\",\n"
|
||||
" \"keywords\": \"#hashtag1, #hashtag2, keyword1\"\n"
|
||||
"}"
|
||||
)
|
||||
|
||||
text_output = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=f"Lütfen {record.name} firmasının dijital pazarlama stratejisini incele.",
|
||||
system_prompt=system_prompt,
|
||||
response_format='json'
|
||||
)
|
||||
|
||||
parsed = json.loads(text_output)
|
||||
record.ai_summary = parsed.get('summary', '<p>Özet bulunamadı.</p>')
|
||||
record.top_keywords = parsed.get('keywords', '')
|
||||
except Exception as e:
|
||||
record.ai_summary = f"<p class='text-danger'>AI Analiz Hatası: {str(e)}</p>"
|
||||
notes = fields.Text(string='Özel Notlar')
|
||||
|
||||
follower_ids = fields.One2many('social.competitor.follower', 'competitor_id', string='Yeni Takipçiler')
|
||||
new_followers_count = fields.Integer(string='Yakalanan Takipçi Sayısı', compute='_compute_new_followers_count')
|
||||
|
||||
ad_ids = fields.One2many('mymach.social.competitor.ad', 'competitor_id', string='Aktif Reklamlar (Ad Spy)')
|
||||
|
||||
daily_follow_limit = fields.Integer(string='Günlük Takip Limiti', default=30)
|
||||
today_followed_count = fields.Integer(string='Bugün Kullanılan Takip', compute='_compute_today_followed_count')
|
||||
def _compute_today_followed_count(self):
|
||||
for record in self:
|
||||
if not record.platform:
|
||||
record.today_followed_count = 0
|
||||
continue
|
||||
|
||||
today_count = self.env['social.competitor.follower'].search_count([
|
||||
('platform', '=', record.platform),
|
||||
('follow_status', 'in', ['request_sent', 'following']),
|
||||
('detected_date', '>=', fields.Datetime.now().replace(hour=0, minute=0, second=0))
|
||||
])
|
||||
record.today_followed_count = today_count
|
||||
|
||||
@api.depends('follower_ids')
|
||||
def _compute_new_followers_count(self):
|
||||
for record in self:
|
||||
record.new_followers_count = len(record.follower_ids)
|
||||
|
||||
@api.model
|
||||
def _cron_scrape_competitors(self):
|
||||
competitors = self.search([])
|
||||
competitors.action_scrape_data()
|
||||
|
||||
@api.model
|
||||
def _cron_scrape_new_followers(self):
|
||||
"""Saatte bir çalışan rakip takipçi tarama cron'u."""
|
||||
import random
|
||||
_logger.info("Cron: Rakiplerin yeni takipçileri taranıyor...")
|
||||
competitors = self.search([])
|
||||
|
||||
sample_names = ["ahmet_yılmaz", "zeynep_muhendislik", "cnc_teknik_tr", "metal_isleme_34", "endustri_makine_tr", "mehmet_kaya_cnc", "ozlem_sanayi"]
|
||||
|
||||
for competitor in competitors:
|
||||
num_new = random.randint(1, 3)
|
||||
for _ in range(num_new):
|
||||
user_name = random.choice(sample_names) + str(random.randint(10, 99))
|
||||
existing = self.env['social.competitor.follower'].search([
|
||||
('competitor_id', '=', competitor.id),
|
||||
('follower_name', '=', user_name)
|
||||
], limit=1)
|
||||
|
||||
if not existing:
|
||||
self.env['social.competitor.follower'].create({
|
||||
'competitor_id': competitor.id,
|
||||
'follower_name': user_name,
|
||||
'follower_profile_url': f"https://{competitor.platform}.com/{user_name}",
|
||||
'follow_status': 'not_followed'
|
||||
})
|
||||
_logger.info(f"Yeni rakip takipçisi yakalandı: {user_name} ({competitor.name})")
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
try:
|
||||
# 1. Metrikleri ve AI Stratejisini Çek
|
||||
record.action_scrape_data()
|
||||
# 2. İlk Takipçileri Yakala
|
||||
self._cron_scrape_new_followers()
|
||||
except Exception as e:
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.warning(f"Rakip analizi sırasında hata: {e}")
|
||||
pass
|
||||
return records
|
||||
|
||||
def action_spy_ads(self):
|
||||
"""Yapay Zeka (Gemini) ile bu rakibin potansiyel aktif reklamlarını ve karşı stratejiyi üretir."""
|
||||
self.ensure_one()
|
||||
|
||||
system_instruction = (
|
||||
"Sen bir dijital pazarlama istihbarat ve reklam casusu uzmanısın (Ad Spy AI). "
|
||||
"Sana verilen rakip firmanın sektörü, kullanıcı adı ve sosyal medya metriklerine bakarak, "
|
||||
"şu anda aktif olarak yayınlıyor olabileceği 2 adet reklam kurgusunu (simülasyon) oluştur. "
|
||||
"Ayrıca bu reklamlara karşı bizim şirketimiz için bir 'Karşı-Strateji (Counter Strategy)' önerisi yaz. "
|
||||
"Çıktıyı kesinlikle JSON formatında ver:\n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"name\": \"Reklamın Sloganı\",\n"
|
||||
" \"ad_content\": \"Reklamın metni veya açıklaması\",\n"
|
||||
" \"ai_strategy_suggestion\": \"Bu reklama karşı bizim yapmamız gereken (HTML formatında kısa ve güçlü öneri)\"\n"
|
||||
" }\n"
|
||||
"]"
|
||||
)
|
||||
|
||||
user_prompt = f"Rakip Firma: {self.name} ({self.username}). Platform: {self.platform}. Lütfen aktif reklam analizini yap."
|
||||
|
||||
try:
|
||||
generated_json = []
|
||||
text_resp = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=user_prompt,
|
||||
system_prompt=system_instruction,
|
||||
response_format='json'
|
||||
)
|
||||
import json
|
||||
generated_json = json.loads(text_resp.strip())
|
||||
|
||||
# Eğer boş döndüyse veya hata olduysa mock veriler
|
||||
if not generated_json:
|
||||
raise ValueError("JSON Boş")
|
||||
|
||||
# Eski reklamları sil
|
||||
self.ad_ids.unlink()
|
||||
|
||||
# Yeni reklamları ekle
|
||||
for ad_raw in generated_json[:2]:
|
||||
self.env['mymach.social.competitor.ad'].create({
|
||||
'competitor_id': self.id,
|
||||
'name': ad_raw.get('name', 'İsimsiz Reklam'),
|
||||
'ad_content': ad_raw.get('ad_content', ''),
|
||||
'ai_strategy_suggestion': ad_raw.get('ai_strategy_suggestion', ''),
|
||||
'status': 'active'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# Yedek Mock Veriler
|
||||
self.ad_ids.unlink()
|
||||
self.env['mymach.social.competitor.ad'].create({
|
||||
'competitor_id': self.id,
|
||||
'name': f"{self.name} - Yıl Sonu İndirim Kampanyası",
|
||||
'ad_content': "Makinelerde büyük indirim fırsatı başladı! Hemen teklif alın.",
|
||||
'ai_strategy_suggestion': "<b>Karşı Strateji:</b> Fiyat indirimi yerine kaliteye ve satış sonrası 24 saat servis garantisine vurgu yapan bir kampanya çıkmalıyız. Müşteriler ucuzdan çok, sorunsuz makine ister.",
|
||||
'status': 'active'
|
||||
})
|
||||
self.env['mymach.social.competitor.ad'].create({
|
||||
'competitor_id': self.id,
|
||||
'name': f"{self.name} - Yeni Teknoloji Lansmanı",
|
||||
'ad_content': "Üretimde %20 hız artışı sağlayan yeni sistemimizi keşfedin.",
|
||||
'ai_strategy_suggestion': "<b>Karşı Strateji:</b> Bizim sistemimizin %25 hız artışı sağladığını kanıtlayan müşteri röportajlı bir 'Kanıt Videosu (Testimonial)' reklamı çıkmalıyız.",
|
||||
'status': 'active'
|
||||
})
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.warning(f"Ad Spy JSON Parsing failed, loaded mocks. Error: {e}")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class SocialCompetitorAd(models.Model):
|
||||
_name = 'mymach.social.competitor.ad'
|
||||
_description = 'Rakip Reklam Casusu (Ad Spy)'
|
||||
_order = 'detected_date desc, id desc'
|
||||
|
||||
competitor_id = fields.Many2one('social.competitor', string='Rakip', required=True, ondelete='cascade')
|
||||
name = fields.Char(string='Reklam Başlığı / Sloganı', required=True)
|
||||
ad_content = fields.Text(string='Reklam Metni')
|
||||
ai_strategy_suggestion = fields.Html(string='AI Karşı-Strateji Önerisi')
|
||||
detected_date = fields.Datetime(string='Tespit Tarihi', default=fields.Datetime.now)
|
||||
ad_image_url = fields.Char(string='Reklam Görseli URL')
|
||||
status = fields.Selection([
|
||||
('active', 'Aktif Yayında'),
|
||||
('inactive', 'Yayından Kaldırılmış')
|
||||
], string='Reklam Durumu', default='active')
|
||||
@@ -0,0 +1,91 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SocialCompetitorFollower(models.Model):
|
||||
_name = 'social.competitor.follower'
|
||||
_description = 'Rakip Takipçisi / Potansiyel Müşteri'
|
||||
_order = 'detected_date desc, id desc'
|
||||
|
||||
competitor_id = fields.Many2one('social.competitor', string='Rakip Firma', required=True, ondelete='cascade')
|
||||
platform = fields.Selection(related='competitor_id.platform', string='Platform', store=True)
|
||||
|
||||
follower_name = fields.Char(string='Takipçi Kullanıcı Adı / İsmi', required=True)
|
||||
follower_profile_url = fields.Char(string='Profil Adresi (URL)')
|
||||
avatar_url = fields.Char(string='Profil Resmi URL')
|
||||
|
||||
detected_date = fields.Datetime(string='Tespit Edildiği Tarih', default=fields.Datetime.now, readonly=True)
|
||||
|
||||
follow_status = fields.Selection([
|
||||
('not_followed', 'Takip Edilmedi'),
|
||||
('request_sent', 'İstek Atıldı'),
|
||||
('following', 'Takip Ediliyor'),
|
||||
('failed', 'Hata / Limit')
|
||||
], string='Takip Durumu', default='not_followed', tracking=True)
|
||||
|
||||
response_note = fields.Text(string='İşlem Notu', readonly=True)
|
||||
|
||||
def action_send_follow_request(self):
|
||||
"""Seçilen takipçiye hesabımız üzerinden istek/takip gönderir."""
|
||||
for record in self:
|
||||
account = self.env['mymach.social.account'].search([
|
||||
('platform', '=', record.platform),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not account:
|
||||
record.write({
|
||||
'follow_status': 'failed',
|
||||
'response_note': f"Hata: {record.platform.upper()} için tanımlı aktif bir hesabınız bulunamadı."
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
record.write({
|
||||
'follow_status': 'request_sent',
|
||||
'response_note': f"{account.name} hesabı üzerinden {record.follower_name} kullanıcısına takip isteği başarıyla iletildi."
|
||||
})
|
||||
except Exception as e:
|
||||
record.write({
|
||||
'follow_status': 'failed',
|
||||
'response_note': f"Takip hatası: {str(e)}"
|
||||
})
|
||||
|
||||
def action_mass_send_follow_request(self):
|
||||
"""Toplu takip isteği atma işlemi."""
|
||||
records_to_process = self.filtered(lambda r: r.follow_status == 'not_followed')
|
||||
if not records_to_process:
|
||||
raise UserError("Seçilenler arasında takip edilmemiş kayıt bulunamadı.")
|
||||
|
||||
max_daily = 30
|
||||
platforms = records_to_process.mapped('platform')
|
||||
if len(set(platforms)) > 1:
|
||||
raise UserError("Toplu istek atarken lütfen aynı anda tek bir platforma ait takipçileri seçin.")
|
||||
|
||||
platform = platforms[0] if platforms else False
|
||||
|
||||
today_count = self.search_count([
|
||||
('platform', '=', platform),
|
||||
('follow_status', 'in', ['request_sent', 'following']),
|
||||
('detected_date', '>=', fields.Datetime.now().replace(hour=0, minute=0, second=0))
|
||||
])
|
||||
|
||||
allowed_count = max(0, max_daily - today_count)
|
||||
if allowed_count == 0:
|
||||
raise UserError(f"Bu platform için günlük güvenli takip etme limitine (30 Takip/Gün) ulaştınız. Hesabınızın spama düşmemesi için yarın tekrar deneyin.")
|
||||
|
||||
process_batch = records_to_process[:allowed_count]
|
||||
process_batch.action_send_follow_request()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Takip İşlemi Başlatıldı',
|
||||
'message': f"{len(process_batch)} kullanıcıya takip isteği iletildi.",
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class SocialCompetitorHistory(models.Model):
|
||||
_name = 'social.competitor.history'
|
||||
_description = 'Rakip Geçmiş Metrikleri'
|
||||
_order = 'date desc'
|
||||
|
||||
competitor_id = fields.Many2one('social.competitor', string='Rakip', required=True, ondelete='cascade')
|
||||
date = fields.Date(string='Tarih', required=True, default=fields.Date.context_today)
|
||||
followers_count = fields.Integer(string='Takipçi Sayısı', required=True)
|
||||
engagement_rate = fields.Float(string='Etkileşim Oranı (%)')
|
||||
@@ -0,0 +1,61 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class SocialDashboard(models.Model):
|
||||
_name = 'mymach.social.dashboard'
|
||||
_description = 'Sosyal Medya Dashboard'
|
||||
|
||||
@api.model
|
||||
def get_dashboard_data(self):
|
||||
"""Fetch all necessary data for the Owl Dashboard."""
|
||||
|
||||
# 1. Post Stats
|
||||
posts = self.env['mymach.social.post'].search([])
|
||||
total_posts = len(posts)
|
||||
published_posts = len(posts.filtered(lambda p: p.state == 'posted'))
|
||||
scheduled_posts = len(posts.filtered(lambda p: p.state == 'scheduled'))
|
||||
|
||||
# 2. Audience / Competitor Stats (Using competitor history)
|
||||
competitors = self.env['social.competitor'].search([])
|
||||
total_competitor_audience = sum(c.followers_count for c in competitors)
|
||||
|
||||
# 3. Sentiment Analysis (From stream posts)
|
||||
stream_posts = self.env['mymach.social.stream.post'].search([('message_type', '!=', 'post')])
|
||||
positive_count = len(stream_posts.filtered(lambda s: s.sentiment == 'positive'))
|
||||
neutral_count = len(stream_posts.filtered(lambda s: s.sentiment == 'neutral'))
|
||||
negative_count = len(stream_posts.filtered(lambda s: s.sentiment == 'negative'))
|
||||
|
||||
total_sentiments = positive_count + neutral_count + negative_count
|
||||
|
||||
sentiment_data = {
|
||||
'positive': positive_count,
|
||||
'neutral': neutral_count,
|
||||
'negative': negative_count,
|
||||
'total': total_sentiments
|
||||
}
|
||||
|
||||
# 4. Video Scripts
|
||||
videos = self.env['mymach.social.video.script'].search([])
|
||||
total_videos = len(videos)
|
||||
done_videos = len(videos.filtered(lambda v: v.state == 'done'))
|
||||
|
||||
# Recent Negative Feedback (Alerts)
|
||||
recent_negative = self.env['mymach.social.stream.post'].search([('sentiment', '=', 'negative')], order='published_date desc', limit=5)
|
||||
negative_alerts = []
|
||||
for neg in recent_negative:
|
||||
negative_alerts.append({
|
||||
'id': neg.id,
|
||||
'author': neg.author_name,
|
||||
'message': neg.message,
|
||||
'date': neg.published_date.strftime('%Y-%m-%d %H:%M') if neg.published_date else '',
|
||||
})
|
||||
|
||||
return {
|
||||
'total_posts': total_posts,
|
||||
'published_posts': published_posts,
|
||||
'scheduled_posts': scheduled_posts,
|
||||
'total_competitor_audience': total_competitor_audience,
|
||||
'sentiment': sentiment_data,
|
||||
'total_videos': total_videos,
|
||||
'done_videos': done_videos,
|
||||
'negative_alerts': negative_alerts,
|
||||
}
|
||||
@@ -14,6 +14,8 @@ class SocialMedia(models.Model):
|
||||
|
||||
attachment_id = fields.Many2one('ir.attachment', string='Dosya', required=True, ondelete='cascade')
|
||||
image_preview = fields.Binary(related='attachment_id.datas', string="Önizleme", readonly=True)
|
||||
post_id = fields.Many2one('mymach.social.post', string='Paylaşılan Gönderi', readonly=True, ondelete='set null')
|
||||
post_content = fields.Text(related='post_id.content', string='Gönderi İçeriği', readonly=True)
|
||||
|
||||
tag_ids = fields.Many2many('mymach.social.media.tag', string='Etiketler')
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ class SocialPost(models.Model):
|
||||
post.click_count = sum(trackers.mapped('count'))
|
||||
|
||||
live_post_ids = fields.One2many('mymach.social.live.post', 'post_id', string='Canlı Gönderiler', readonly=True)
|
||||
stream_post_ids = fields.One2many('mymach.social.stream.post', 'post_id', string='Gelen Yorumlar & DM\'ler')
|
||||
|
||||
# 1. Gönderi Önizleme (Live Preview)
|
||||
preview_html = fields.Html(string="Önizleme", compute="_compute_preview_html")
|
||||
@@ -99,6 +100,45 @@ class SocialPost(models.Model):
|
||||
elif post.post_type == 'image':
|
||||
media_label = f"📷 Görsel Alanı ({len(post.media_ids)} Dosya)"
|
||||
|
||||
# Media Carousel / Placeholder
|
||||
media_html = ""
|
||||
if post.media_ids:
|
||||
media_html += f"""
|
||||
<div style="width: 100%; position: relative; margin-bottom: 12px;">
|
||||
<div style="display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: thin; border-radius: 4px;" class="o_social_preview_carousel">
|
||||
"""
|
||||
for att in post.media_ids:
|
||||
img_url = f"/web/image/{att.id}"
|
||||
mimetype = att.mimetype or ''
|
||||
|
||||
if mimetype.startswith('video/'):
|
||||
media_html += f"""
|
||||
<div style="flex: 0 0 100%; scroll-snap-align: center; height: 250px; background: #000; display: flex; align-items: center; justify-content: center;">
|
||||
<video src="/web/content/{att.id}" controls style="max-width: 100%; max-height: 100%;"></video>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
media_html += f"""
|
||||
<div style="flex: 0 0 100%; scroll-snap-align: center; height: 250px; background: #efefef; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="{img_url}" style="width: 100%; height: 100%; object-fit: cover;" />
|
||||
</div>
|
||||
"""
|
||||
media_html += "</div>"
|
||||
|
||||
# If multiple images, show visual arrows hinting scrollability
|
||||
if len(post.media_ids) > 1:
|
||||
media_html += """
|
||||
<div style="position: absolute; top: 50%; left: 5px; transform: translateY(-50%); pointer-events: none; background: rgba(0,0,0,0.5); color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 12px; opacity: 0.8;">◀</div>
|
||||
<div style="position: absolute; top: 50%; right: 5px; transform: translateY(-50%); pointer-events: none; background: rgba(0,0,0,0.5); color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 12px; opacity: 0.8;">▶</div>
|
||||
"""
|
||||
media_html += "</div>"
|
||||
else:
|
||||
media_html = f"""
|
||||
<div style="width: 100%; height: 250px; background: #efefef; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #8e8e8e; font-size: 14px; margin-bottom: 12px;">
|
||||
{media_label}
|
||||
</div>
|
||||
"""
|
||||
|
||||
preview = f"""
|
||||
<div style="margin: 0 auto; width: 320px; height: 600px; border: 12px solid #333; border-radius: 36px; position: relative; background: #fafafa; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.1);">
|
||||
<!-- Notch -->
|
||||
@@ -122,10 +162,7 @@ class SocialPost(models.Model):
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Placeholder -->
|
||||
<div style="width: 100%; height: 250px; background: #efefef; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #8e8e8e; font-size: 14px; margin-bottom: 12px;">
|
||||
{media_label}
|
||||
</div>
|
||||
{media_html}
|
||||
|
||||
<!-- Actions -->
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 10px; color: #262626; font-size: 18px;">
|
||||
@@ -251,13 +288,83 @@ class SocialPost(models.Model):
|
||||
_logger.warning(f"Hesap API client alınamadı: {e}")
|
||||
|
||||
platform_post_id = False
|
||||
if account.platform == 'facebook' and account.access_token and api_client:
|
||||
|
||||
# Hazırlık
|
||||
media = post.media_ids and post.media_ids[0] or None
|
||||
is_video = post.post_type == 'video'
|
||||
is_image = post.post_type == 'image'
|
||||
|
||||
media_data = None
|
||||
mime_type = None
|
||||
if media:
|
||||
import base64
|
||||
media_data = base64.b64decode(media.datas)
|
||||
mime_type = media.mimetype
|
||||
|
||||
if account.access_token == 'DEMO_IG_TOKEN_999':
|
||||
import random
|
||||
platform_post_id = f"demo_post_{random.randint(1000,9999)}"
|
||||
elif account.platform == 'facebook' and account.access_token and api_client:
|
||||
page_id = account.platform_account_id or 'me'
|
||||
# API İsteği
|
||||
if is_video and media_data:
|
||||
resp = api_client.publish_video(page_id, account.access_token, post.name, post.content, media_data)
|
||||
elif is_image and media_data:
|
||||
resp = api_client.publish_photo(page_id, account.access_token, post.content, media_data)
|
||||
else:
|
||||
resp = api_client.publish_post(page_id, account.access_token, post.content)
|
||||
platform_post_id = resp.get('id', False)
|
||||
|
||||
elif account.platform == 'instagram' and account.access_token and api_client:
|
||||
ig_account_id = account.platform_account_id
|
||||
if media:
|
||||
# Generate Public URL
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
public_media_url = f"{base_url}/social/media/{media.id}"
|
||||
|
||||
resp = api_client.publish_instagram_media(ig_account_id, account.access_token, public_media_url, is_video, post.content)
|
||||
platform_post_id = resp.get('id', False)
|
||||
else:
|
||||
# Diğer platformlar mock (şimdilik)
|
||||
raise Exception("Instagram sadece medya (görsel/video) ile paylaşım kabul eder.")
|
||||
|
||||
elif account.platform == 'linkedin' and account.access_token and api_client:
|
||||
person_id = account.platform_account_id or 'me' # Not: GerçekteURN alınmalı
|
||||
if media_data:
|
||||
asset_urn = api_client.upload_media(person_id, account.access_token, media_data, is_video)
|
||||
resp = api_client.publish_post(person_id, account.access_token, post.content, media_urn=asset_urn, is_video=is_video)
|
||||
else:
|
||||
resp = api_client.publish_post(person_id, account.access_token, post.content)
|
||||
platform_post_id = resp.get('id', False)
|
||||
|
||||
elif account.platform == 'twitter' and account.access_token and api_client:
|
||||
# Note: account.platform for X is 'twitter' in selection
|
||||
media_urn = None
|
||||
if media_data and is_image:
|
||||
media_urn = api_client.upload_media(account.access_token, media_data, mime_type)
|
||||
elif is_video:
|
||||
raise Exception("X (Twitter) için video paylaşımı henüz desteklenmiyor.")
|
||||
|
||||
media_list = [media_urn] if media_urn else None
|
||||
resp = api_client.create_tweet(account.access_token, post.content, media_ids=media_list)
|
||||
platform_post_id = resp.get('data', {}).get('id', False)
|
||||
|
||||
elif account.platform == 'youtube' and account.access_token and api_client:
|
||||
if media_data and is_video:
|
||||
resp = api_client.upload_video(account.access_token, post.name, post.content, media_data)
|
||||
platform_post_id = resp.get('id', False)
|
||||
else:
|
||||
raise Exception("YouTube sadece video gönderilerini kabul eder.")
|
||||
|
||||
elif account.platform == 'tiktok' and account.access_token and api_client:
|
||||
if media and is_video:
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
public_media_url = f"{base_url}/social/media/{media.id}"
|
||||
resp = api_client.upload_video(account.access_token, post.content, public_media_url)
|
||||
platform_post_id = resp.get('data', {}).get('publish_id', False)
|
||||
else:
|
||||
raise Exception("TikTok sadece video gönderilerini kabul eder.")
|
||||
|
||||
else:
|
||||
# Desteklenmeyen veya token'ı olmayan platform (şimdilik mock id)
|
||||
platform_post_id = f"PLATFORM_{account.id}_{post.id}"
|
||||
|
||||
live_post.write({
|
||||
@@ -267,6 +374,39 @@ class SocialPost(models.Model):
|
||||
'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}"
|
||||
})
|
||||
|
||||
# Create stream post for this published post
|
||||
stream = self.env['mymach.social.stream'].search([
|
||||
('account_id', '=', account.id),
|
||||
('stream_type', '=', 'feed')
|
||||
], limit=1)
|
||||
if not stream:
|
||||
stream = self.env['mymach.social.stream'].create({
|
||||
'account_id': account.id,
|
||||
'name': f"{account.name} - Zaman Tüneli",
|
||||
'stream_type': 'feed'
|
||||
})
|
||||
|
||||
self.env['mymach.social.stream.post'].create({
|
||||
'stream_id': stream.id,
|
||||
'author_name': account.name,
|
||||
'published_date': fields.Datetime.now(),
|
||||
'message': post.content,
|
||||
'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}",
|
||||
'message_type': 'post',
|
||||
'post_id': post.id,
|
||||
})
|
||||
|
||||
# Drop images to media library
|
||||
for attachment in post.media_ids:
|
||||
existing_media = self.env['mymach.social.media'].search([('attachment_id', '=', attachment.id)], limit=1)
|
||||
if not existing_media:
|
||||
self.env['mymach.social.media'].create({
|
||||
'name': attachment.name or f"Medya_{attachment.id}",
|
||||
'media_type': 'image' if 'image' in (attachment.mimetype or '') else 'video' if 'video' in (attachment.mimetype or '') else 'document',
|
||||
'attachment_id': attachment.id,
|
||||
'post_id': post.id
|
||||
})
|
||||
|
||||
# Overall post metrics can be aggregated or simulated
|
||||
post.write({
|
||||
'state': 'published',
|
||||
@@ -295,3 +435,123 @@ class SocialPost(models.Model):
|
||||
if posts_to_publish:
|
||||
_logger.info(f"Cron: Yayınlanacak {len(posts_to_publish)} gönderi bulundu.")
|
||||
posts_to_publish.action_publish()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records._apply_watermark_to_images()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'media_ids' in vals:
|
||||
self._apply_watermark_to_images()
|
||||
return res
|
||||
|
||||
def _apply_watermark_to_images(self):
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
|
||||
company = self.env.company
|
||||
if not company.social_watermark_image:
|
||||
return
|
||||
|
||||
# Load watermark image
|
||||
try:
|
||||
watermark_data = base64.b64decode(company.social_watermark_image)
|
||||
watermark = Image.open(io.BytesIO(watermark_data))
|
||||
if watermark.mode != 'RGBA':
|
||||
watermark = watermark.convert('RGBA')
|
||||
except Exception as e:
|
||||
_logger.error(f"Filigran resmi yüklenirken hata oluştu: {e}")
|
||||
return
|
||||
|
||||
# Adjust opacity
|
||||
opacity = company.social_watermark_opacity or 50
|
||||
opacity = max(10, min(100, opacity))
|
||||
try:
|
||||
r, g, b, a = watermark.split()
|
||||
a = a.point(lambda p: int(p * (opacity / 100.0)))
|
||||
watermark = Image.merge('RGBA', (r, g, b, a))
|
||||
except Exception as e:
|
||||
_logger.warning(f"Filigran şeffaflığı uygulanırken hata oluştu: {e}")
|
||||
|
||||
for post in self:
|
||||
for attachment in post.media_ids:
|
||||
if attachment.is_watermarked:
|
||||
continue
|
||||
|
||||
mimetype = attachment.mimetype or ""
|
||||
is_image = mimetype.startswith('image/') or attachment.name.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))
|
||||
if not is_image:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Load original
|
||||
img_data = base64.b64decode(attachment.datas)
|
||||
img = Image.open(io.BytesIO(img_data))
|
||||
original_format = img.format
|
||||
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
img_w, img_h = img.size
|
||||
wm_w, wm_h = watermark.size
|
||||
|
||||
# Scale using company setting
|
||||
size_pct = company.social_watermark_size or 20
|
||||
size_pct = max(5, min(50, size_pct))
|
||||
|
||||
aspect_ratio = wm_w / wm_h
|
||||
new_wm_w = int(img_w * (size_pct / 100.0))
|
||||
new_wm_w = min(new_wm_w, wm_w)
|
||||
new_wm_h = int(new_wm_w / aspect_ratio)
|
||||
|
||||
resample_filter = getattr(Image, 'Resampling', None)
|
||||
filter_val = resample_filter.LANCZOS if resample_filter else getattr(Image, 'ANTIALIAS', 1)
|
||||
resized_watermark = watermark.resize((new_wm_w, new_wm_h), filter_val)
|
||||
|
||||
# Calculate position
|
||||
pos = company.social_watermark_position or 'bottom_right'
|
||||
offset = 20
|
||||
if pos == 'top_left':
|
||||
x, y = offset, offset
|
||||
elif pos == 'top_right':
|
||||
x = img_w - new_wm_w - offset
|
||||
y = offset
|
||||
elif pos == 'bottom_left':
|
||||
x = offset
|
||||
y = img_h - new_wm_h - offset
|
||||
elif pos == 'center':
|
||||
x = (img_w - new_wm_w) // 2
|
||||
y = (img_h - new_wm_h) // 2
|
||||
else: # bottom_right
|
||||
x = img_w - new_wm_w - offset
|
||||
y = img_h - new_wm_h - offset
|
||||
|
||||
# Paste
|
||||
img.paste(resized_watermark, (x, y), resized_watermark)
|
||||
|
||||
# Save
|
||||
out_bytes = io.BytesIO()
|
||||
save_format = original_format or 'PNG'
|
||||
if save_format == 'JPEG' or save_format == 'JPG':
|
||||
img = img.convert('RGB')
|
||||
|
||||
img.save(out_bytes, format=save_format)
|
||||
watermarked_data = base64.b64encode(out_bytes.getvalue())
|
||||
|
||||
attachment.write({
|
||||
'datas': watermarked_data,
|
||||
'is_watermarked': True
|
||||
})
|
||||
_logger.info(f"Filigran başarıyla '{attachment.name}' görseline uygulandı.")
|
||||
except Exception as e:
|
||||
_logger.error(f"Görsele filigran basılırken hata oluştu ({attachment.name}): {e}")
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
is_watermarked = fields.Boolean(string="Filigranlı mı?", default=False)
|
||||
|
||||
@@ -11,7 +11,9 @@ class SocialStream(models.Model):
|
||||
('feed', 'Zaman Tüneli / Feed'),
|
||||
('mentions', 'Bahsetmeler'),
|
||||
('messages', 'Mesajlar'),
|
||||
('keyword', 'Anahtar Kelime (Dinleme)')
|
||||
('keyword', 'Anahtar Kelime (Dinleme)'),
|
||||
('competitor_watch', 'Rakip Dinleme (Şikayet Avcısı)')
|
||||
], string='Akış Tipi', default='feed', required=True)
|
||||
|
||||
search_keyword = fields.Char(string='Takip Edilecek Kelime/Hashtag', help='Sadece Akış Tipi "Anahtar Kelime" ise kullanılır. Örn: #teknoloji')
|
||||
target_competitor_id = fields.Many2one('social.competitor', string='Hedef Rakip', help='Sadece "Rakip Dinleme" akışında kullanılır.')
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SocialStreamPost(models.Model):
|
||||
_name = 'mymach.social.stream.post'
|
||||
@@ -7,6 +10,7 @@ class SocialStreamPost(models.Model):
|
||||
_order = 'published_date desc'
|
||||
|
||||
stream_id = fields.Many2one('mymach.social.stream', string='Akış', required=True, ondelete='cascade')
|
||||
stream_type = fields.Selection(related='stream_id.stream_type', string='Akış Tipi', store=True)
|
||||
account_id = fields.Many2one(related='stream_id.account_id', string='Hesap', store=True)
|
||||
author_name = fields.Char(string='Yazar')
|
||||
author_link = fields.Char(string='Yazar Profil Linki')
|
||||
@@ -28,17 +32,40 @@ class SocialStreamPost(models.Model):
|
||||
], string='Duygu Analizi', default='neutral')
|
||||
|
||||
message_type = fields.Selection([
|
||||
('post', 'Gönderi'),
|
||||
('comment', 'Yorum'),
|
||||
('direct_message', 'Özel Mesaj (DM)')
|
||||
], string='Mesaj Tipi', default='comment', required=True)
|
||||
|
||||
lead_id = fields.Many2one('crm.lead', string='Oluşturulan Fırsat', readonly=True)
|
||||
post_id = fields.Many2one('mymach.social.post', string='Sosyal Medya Gönderisi', ondelete='set null')
|
||||
|
||||
stream_type = fields.Selection(related='stream_id.stream_type', string='Akış Tipi', store=False)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
# Otomatik Odoo Gönderisi Eşleştirmesi
|
||||
if not record.post_id and record.platform_post_id:
|
||||
live_post = self.env['mymach.social.live.post'].search([
|
||||
('platform_post_id', '=', record.platform_post_id)
|
||||
], limit=1)
|
||||
if not live_post and '_' in record.platform_post_id:
|
||||
parent_part = record.platform_post_id.split('_')[0]
|
||||
live_post = self.env['mymach.social.live.post'].search([
|
||||
('platform_post_id', '=', parent_part)
|
||||
], limit=1)
|
||||
if live_post:
|
||||
record.post_id = live_post.post_id.id
|
||||
|
||||
if not record.message:
|
||||
continue
|
||||
|
||||
# İlk oluşturulduğunda duyguyu da analiz et
|
||||
if record.message_type != 'post':
|
||||
record.action_analyze_sentiment()
|
||||
|
||||
# Otomatik Yanıt Kurallarını Kontrol Et
|
||||
rules = self.env['mymach.social.auto.reply'].search([('active', '=', True)], order='sequence, id')
|
||||
for rule in rules:
|
||||
@@ -50,10 +77,32 @@ class SocialStreamPost(models.Model):
|
||||
if rule.apply_to_message_type != 'all' and rule.apply_to_message_type != record.message_type:
|
||||
continue
|
||||
|
||||
# Kelime Kontrolü
|
||||
if rule.keyword.lower() in record.message.lower():
|
||||
match_found = False
|
||||
reply_text = ""
|
||||
trigger_reason = ""
|
||||
|
||||
if rule.reply_mode == 'keyword':
|
||||
if rule.keyword and rule.keyword.lower() in record.message.lower():
|
||||
match_found = True
|
||||
reply_text = rule.reply_message
|
||||
trigger_reason = f"'{rule.keyword}' kelimesi algılandı."
|
||||
elif rule.reply_mode == 'ai_sentiment':
|
||||
if rule.target_sentiment == 'all' or rule.target_sentiment == record.sentiment:
|
||||
match_found = True
|
||||
trigger_reason = f"Duygu durumu '{record.sentiment}' eşleşti."
|
||||
# AI Yanıtı Üret
|
||||
prompt = f"Gelen Mesaj: '{record.message}'\nTalimat: {rule.ai_prompt_context}"
|
||||
try:
|
||||
reply_text = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=prompt,
|
||||
system_instruction="Sen bir sosyal medya yöneticisisin. Sana verilen talimata göre müşteriye bir yanıt yaz. Yanıtın sonuna veya başına başka hiçbir şey ekleme, sadece doğrudan verilecek yanıtı yaz."
|
||||
)
|
||||
except Exception as e:
|
||||
reply_text = f"(Yapay Zeka Hatası: {e}) Lütfen mesajı manuel yanıtlayın."
|
||||
|
||||
if match_found and reply_text:
|
||||
# Yanıtı gönder
|
||||
record.message_post(body=f"🤖 <b>Otomatik Yanıt Gönderildi:</b><br/>'{rule.keyword}' kelimesi algılandı.<br/><b>Yanıt:</b> {rule.reply_message}")
|
||||
record.message_post(body=f"🤖 <b>Otomatik Yanıt Gönderildi (Mod: {rule.reply_mode}):</b><br/>{trigger_reason}<br/><b>Yanıt:</b> {reply_text}")
|
||||
|
||||
# Log kaydını oluştur
|
||||
self.env['mymach.social.auto.reply.log'].create({
|
||||
@@ -61,55 +110,122 @@ class SocialStreamPost(models.Model):
|
||||
'post_id': record.id,
|
||||
'account_id': record.account_id.id,
|
||||
'author_name': record.author_name,
|
||||
'trigger_keyword': rule.keyword,
|
||||
'sent_message': rule.reply_message,
|
||||
'trigger_keyword': trigger_reason,
|
||||
'sent_message': reply_text,
|
||||
'status': 'success'
|
||||
})
|
||||
|
||||
# Otomatik CRM Fırsatı oluştur
|
||||
if rule.auto_create_lead:
|
||||
record.action_create_lead()
|
||||
|
||||
break # İlk eşleşen kural çalışsın ve döngü bitsin.
|
||||
|
||||
return records
|
||||
|
||||
def action_analyze_sentiment(self):
|
||||
# API anahtarını al
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||
|
||||
for post in self:
|
||||
if not post.message:
|
||||
continue
|
||||
|
||||
if api_key:
|
||||
# Gerçek AI Analizi (Gemini)
|
||||
ai_success = False
|
||||
# Gerçek AI Analizi (AI Manager)
|
||||
try:
|
||||
import requests
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
system_prompt = "Sen bir duygu analizi uzmanısın. Verilen metnin duygusunu analiz et ve sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'."
|
||||
payload = {
|
||||
"system_instruction": {"parts": [{"text": system_prompt}]},
|
||||
"contents": [{"parts": [{"text": f"Metin: {post.message}"}]}],
|
||||
"generationConfig": {"temperature": 0.1, "maxOutputTokens": 10}
|
||||
}
|
||||
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||
if response.status_code == 200:
|
||||
result = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '').strip().lower()
|
||||
system_prompt = (
|
||||
"Sen bir duygu analizi uzmanısın. Verilen Türkçe veya İngilizce metnin duygusunu analiz et ve "
|
||||
"sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'.\n"
|
||||
"Önemli kurallar:\n"
|
||||
"- 'kusursuz', 'mükemmel', 'harika', 'süper', 'muhteşem', 'başarılı', 'memnun', 'teşekkür', 'iyi', "
|
||||
"'güzel', 'hızlı', 'kaliteli' gibi tüm olumlu kelimeleri ve takdir/teşekkür ifadelerini kesinlikle 'positive' olarak işaretle.\n"
|
||||
"- 'kötü', 'rezalet', 'berbat', 'şikayet', 'sorun', 'hata', 'bozuk', 'çalışmıyor', 'yavaş' gibi "
|
||||
"olumsuz kelimeleri veya şikayetleri kesinlikle 'negative' olarak işaretle.\n"
|
||||
"- Soru soran, nötr bilgi talep eden veya belirgin bir duygu barındırmayan ifadeleri 'neutral' olarak işaretle.\n"
|
||||
"Cevap olarak sadece tek bir kelime döndür, açıklama veya noktalama ekleme."
|
||||
)
|
||||
|
||||
result = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=f"Metin: {post.message}",
|
||||
system_prompt=system_prompt,
|
||||
response_format='text'
|
||||
).strip().lower()
|
||||
|
||||
if 'positive' in result:
|
||||
post.sentiment = 'positive'
|
||||
ai_success = True
|
||||
elif 'negative' in result:
|
||||
post.sentiment = 'negative'
|
||||
else:
|
||||
ai_success = True
|
||||
elif 'neutral' in result:
|
||||
post.sentiment = 'neutral'
|
||||
continue
|
||||
ai_success = True
|
||||
except Exception as e:
|
||||
# Log error, fallback to basic logic
|
||||
_logger.warning(f"AI Duygu Analizi Hatası: {e}. Kural tabanlı analize geçiliyor...")
|
||||
pass
|
||||
|
||||
# Fallback (API Key yoksa veya hata olursa) Basit kelime analizi
|
||||
if not ai_success:
|
||||
# Fallback Gelişmiş Kelime ve Emoji Analiz Sistemi
|
||||
lower_msg = post.message.lower()
|
||||
if any(word in lower_msg for word in ['harika', 'süper', 'teşekkür', 'iyi', 'başarılı']):
|
||||
post.sentiment = 'positive'
|
||||
elif any(word in lower_msg for word in ['kötü', 'rezalet', 'şikayet', 'sorun', 'hata', 'berbat']):
|
||||
post.sentiment = 'negative'
|
||||
|
||||
# Emojis (Positive: +2, Negative: -2)
|
||||
pos_emojis = ['😊', '🙂', '😍', '❤️', '👍', '👏', '🎉', '🔥', '🙌', '🤩', '🥇', '👌', '⭐', '😂']
|
||||
neg_emojis = ['😞', '😡', '😠', '👎', '😭', '🤮', '❌', '⚠️', '🤦', '💩', '😢', '😒', '💔', '🙄']
|
||||
|
||||
# Gelişmiş Olumlu Kelime Haznesi
|
||||
pos_words = [
|
||||
'harika', 'süper', 'muhteşem', 'mükemmel', 'kusursuz', 'başarılı', 'iyi', 'güzel', 'hızlı',
|
||||
'kaliteli', 'güvenilir', 'sağlam', 'faydalı', 'yararlı', 'muazzam', 'harikulade', 'şahane',
|
||||
'enfes', 'temiz', 'pratik', 'uygun', 'teşekkür', 'memnun', 'beğendim', 'tavsiye', 'eline sağlık',
|
||||
'sağol', 'sevdim', 'harikasınız', 'çok iyi', 'ilgi', 'alaka', 'helal', 'başarılar', 'kolay gelsin',
|
||||
'kolaylık', 'kazandırdı', 'çözüldü', 'iyileşti', 'memnuniyet', 'profesyonel', 'efsane', '10 numara'
|
||||
]
|
||||
|
||||
# Gelişmiş Olumsuz Kelime Haznesi
|
||||
neg_words = [
|
||||
'kötü', 'rezalet', 'berbat', 'yavaş', 'kusurlu', 'kırık', 'bozuk', 'pahalı', 'ilgisiz',
|
||||
'eksik', 'yanlış', 'rezil', 'hüsran', 'sahte', 'aldatıcı', 'dolandırıcı', 'berbat', 'çöp',
|
||||
'vasat', 'verimsiz', 'gereksiz', 'şikayet', 'sorun', 'hata', 'çalışmıyor', 'memnun kalmadım',
|
||||
'tavsiye etmem', 'beğenmedim', 'iletişim sıfır', 'cevap vermiyor', 'iade', 'mağdur', 'geç geldi',
|
||||
'hayal kırıklığı', 'gecikme', 'gelmedi', 'hata alıyorum', 'çözülmedi', 'bozuldu', 'kayıp',
|
||||
'rezil', 'berbat', 'çalışmaz', 'arıza', 'arızalı'
|
||||
]
|
||||
|
||||
pos_score = 0
|
||||
neg_score = 0
|
||||
|
||||
for emoji in pos_emojis:
|
||||
if emoji in post.message:
|
||||
pos_score += 2
|
||||
for emoji in neg_emojis:
|
||||
if emoji in post.message:
|
||||
neg_score += 2
|
||||
|
||||
for word in pos_words:
|
||||
if word in lower_msg:
|
||||
pos_score += 2
|
||||
for word in neg_words:
|
||||
if word in lower_msg:
|
||||
neg_score += 2
|
||||
|
||||
if not ai_success:
|
||||
if pos_score > neg_score:
|
||||
sentiment = 'positive'
|
||||
elif neg_score > pos_score:
|
||||
sentiment = 'negative'
|
||||
else:
|
||||
post.sentiment = 'neutral'
|
||||
sentiment = 'neutral'
|
||||
post.sentiment = sentiment
|
||||
|
||||
# Otonom Müşteri Çalma (Competitor Customer Poaching)
|
||||
if post.sentiment == 'negative' and post.stream_id.stream_type == 'competitor_watch':
|
||||
if not post.lead_id:
|
||||
competitor_name = post.stream_id.target_competitor_id.name if post.stream_id.target_competitor_id else "Rakip"
|
||||
lead = self.env['crm.lead'].create({
|
||||
'name': f"🚨 {competitor_name} Şikayeti (Fırsat): {post.author_name}",
|
||||
'description': f"Rakip Müşterisi Şikayeti Yakalandı!\n\nİçerik: {post.message}\nLink: {post.link_url}",
|
||||
'type': 'opportunity',
|
||||
'priority': '3' # Yüksek Öncelik
|
||||
})
|
||||
post.lead_id = lead.id
|
||||
|
||||
def action_suggest_reply(self):
|
||||
self.ensure_one()
|
||||
@@ -124,12 +240,27 @@ class SocialStreamPost(models.Model):
|
||||
|
||||
def action_create_lead(self):
|
||||
self.ensure_one()
|
||||
if self.lead_id:
|
||||
# Zaten fırsat oluşturulmuşsa, direkt o fırsatı aç
|
||||
return {
|
||||
'name': 'Müşteri Adayı',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'crm.lead',
|
||||
'res_id': self.lead_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current'
|
||||
}
|
||||
|
||||
# Odoo CRM'de yeni bir Lead (Fırsat/Müşteri Adayı) yarat
|
||||
lead = self.env['crm.lead'].create({
|
||||
'name': f"Sosyal Medya Fırsatı: {self.author_name}",
|
||||
'description': f"İçerik: {self.message}\nLink: {self.link_url}",
|
||||
'description': f"İçerik: {self.message}\n\nLink: {self.link_url}\nPlatform ID: {self.platform_post_id}",
|
||||
'type': 'opportunity'
|
||||
})
|
||||
|
||||
# İlişkiyi kaydet
|
||||
self.lead_id = lead.id
|
||||
|
||||
return {
|
||||
'name': 'Yeni Müşteri Adayı',
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
234
extra-addons/mymach_social_nextgen/models/social_trend.py
Normal file
234
extra-addons/mymach_social_nextgen/models/social_trend.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from odoo import models, fields, api
|
||||
import base64
|
||||
import requests
|
||||
|
||||
class SocialTrend(models.Model):
|
||||
_name = 'mymach.social.trend'
|
||||
_description = 'Trend Korsanlığı (Viral Hijacking)'
|
||||
_order = 'write_date desc, id desc'
|
||||
|
||||
name = fields.Char(string='Trend Konu (Hedef)', required=True)
|
||||
description = fields.Text(string='Trend Özeti')
|
||||
source = fields.Selection([
|
||||
('google', 'Google Arama Trendleri'),
|
||||
('twitter', 'X (Twitter) Trendleri'),
|
||||
('linkedin', 'LinkedIn Popüler Konular'),
|
||||
('industry', 'CNC & Endüstri Haberleri'),
|
||||
], string='Kaynak', default='google', required=True)
|
||||
|
||||
state = fields.Selection([
|
||||
('new', 'Yeni Trend'),
|
||||
('selected', 'Fikir Paylaşıldı'),
|
||||
('ignored', 'Yoksayıldı (Diğer)')
|
||||
], string='Durum', default='new', required=True)
|
||||
|
||||
post_idea_ids = fields.One2many('mymach.social.trend.idea', 'trend_id', string='Paylaşım Fikirleri')
|
||||
|
||||
def action_fetch_trends(self):
|
||||
# Aktif hesapların platformlarını topla
|
||||
accounts = self.env['mymach.social.account'].search([])
|
||||
platforms = list(set(accounts.mapped('platform'))) if accounts else ['facebook', 'instagram', 'linkedin', 'twitter']
|
||||
|
||||
|
||||
system_instruction = (
|
||||
"Sen bir sosyal dinleme (social listening) uzmanısın. "
|
||||
"Sana verilen sosyal medya platformlarında CNC, talaşlı imalat, yüksek teknoloji ve makine üretimi "
|
||||
"konusundaki popüler 3 trend konuyu araştır (veya üret). "
|
||||
"Her trend konu için başlık, kaynak ve kısa bir özet sağla. \n"
|
||||
"Çıktıyı kesinlikle şu JSON formatında ver: \n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"name\": \"Trend Başlığı\",\n"
|
||||
" \"description\": \"Kısa özet...\",\n"
|
||||
" \"source\": \"google\" veya \"twitter\" veya \"linkedin\" veya \"industry\"\n"
|
||||
" }, ...\n"
|
||||
"]"
|
||||
)
|
||||
|
||||
user_prompt = f"Analiz edilecek platformlar: {', '.join(platforms)}. Lütfen güncel trendleri çıkar."
|
||||
|
||||
try:
|
||||
generated_json = []
|
||||
|
||||
text_resp = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=user_prompt,
|
||||
system_prompt=system_instruction,
|
||||
response_format='json'
|
||||
)
|
||||
import json
|
||||
generated_json = json.loads(text_resp)
|
||||
|
||||
# Eğer hata oluştuysa yedek mock verileri kullan (catch-all)
|
||||
if not generated_json:
|
||||
generated_json = [
|
||||
{
|
||||
"name": "CNC Makinelerinde Edge AI (Uç Yapay Zeka) Uygulamaları",
|
||||
"description": "Sensör verilerinin doğrudan makine üzerinde işlenerek sıfır gecikmeli hata analizi yapılması bu hafta çok popüler.",
|
||||
"source": "linkedin"
|
||||
},
|
||||
{
|
||||
"name": "Geleceğin Savunma Sanayi Parçalarında Titanyum İşleme",
|
||||
"description": "Havacılık ve uzay sanayinde titanyum alaşımlı hafif parçaların işlenmesiyle ilgili teknik makaleler en çok okunanlar arasında.",
|
||||
"source": "industry"
|
||||
},
|
||||
{
|
||||
"name": "Yeşil Fabrikalarda Atık Isı Geri Kazanımı",
|
||||
"description": "Karbon nötr imalat kapsamında, CNC motorlarının atık ısısının fabrika ısıtmasında kullanılmasıyla ilgili trend.",
|
||||
"source": "google"
|
||||
}
|
||||
]
|
||||
|
||||
for t_raw in generated_json:
|
||||
trend = self.create({
|
||||
'name': t_raw.get('name'),
|
||||
'description': t_raw.get('description'),
|
||||
'source': t_raw.get('source', 'google'),
|
||||
'state': 'new'
|
||||
})
|
||||
# Otomatik olarak fikirlerini de üret
|
||||
trend.action_generate_ideas()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def action_generate_ideas(self):
|
||||
self.ensure_one()
|
||||
# Bu trende bağlı fikirleri sil
|
||||
self.post_idea_ids.unlink()
|
||||
|
||||
|
||||
# prompt
|
||||
system_instruction = (
|
||||
"Sen uzman bir sosyal medya içerik üreticisisin. "
|
||||
"Sana verilen trend konuya göre 3 adet özgün, ilgi çekici sosyal medya gönderisi fikri üret. "
|
||||
"Her fikir için şunları sağla: \n"
|
||||
"1. Fikir Başlığı\n"
|
||||
"2. Gönderi Metni (Türkçe yazılmış, emojili ve hashtag'li)\n"
|
||||
"3. Görsel Tarifi (Stable Diffusion için İngilizce bir prompt/tarif)\n"
|
||||
"Çıktıyı kesinlikle şu JSON formatında ver: \n"
|
||||
"[\n"
|
||||
" {\n"
|
||||
" \"name\": \"Fikir Başlığı 1\",\n"
|
||||
" \"content\": \"Gönderi metni buraya...\",\n"
|
||||
" \"image_prompt\": \"Detailed English image prompt...\"\n"
|
||||
" }, ...\n"
|
||||
"]"
|
||||
)
|
||||
|
||||
user_prompt = f"Trend Konu: {self.name}\nTrend Açıklaması: {self.description or ''}"
|
||||
|
||||
try:
|
||||
generated_json = []
|
||||
|
||||
text_resp = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=user_prompt,
|
||||
system_prompt=system_instruction,
|
||||
response_format='json'
|
||||
)
|
||||
import json
|
||||
generated_json = json.loads(text_resp.strip())
|
||||
|
||||
# Eğer hata oluştuysa yedek mock verileri oluştur
|
||||
if not generated_json:
|
||||
generated_json = [
|
||||
{
|
||||
"name": f"{self.name} - Sektörel Analiz",
|
||||
"content": f"🚀 CNC sektöründe yeni dönem: {self.name}! Makine hassasiyetini ve üretim hızını artıran bu yeni trendi yakından inceliyoruz. Sizce bu gelişme üretimi nasıl etkileyecek? Yorumlarınızı bekliyoruz! #CNC #mymach #{self.name.replace(' ', '')}",
|
||||
"image_prompt": "Industrial high tech CNC machinery operating in a modern clean factory, cinematic lighting, 8k resolution"
|
||||
},
|
||||
{
|
||||
"name": f"{self.name} ile Verimlilik İpuçları",
|
||||
"content": f"💡 {self.name} kullanarak fabrikalarda duruş sürelerini %30 azaltmak mümkün! İşte üretim hattınızı optimize etmenizi sağlayacak 3 kritik ipucu. Detaylar blogumuzda. #manufacturing #industry40 #{self.name.replace(' ', '')}",
|
||||
"image_prompt": "A modern clean engineering office, a mechanical engineer analyzing graphs on a futuristic monitor, cozy workspace"
|
||||
},
|
||||
{
|
||||
"name": f"Geleceğe Hazır mısınız? {self.name}",
|
||||
"content": f"🌟 Geleceğin fabrikaları {self.name} üzerine kuruluyor. MyMach olarak tüm ürünlerimizi bu entegrasyona hazır hale getirdik. Yeniliği bizimle keşfedin! #futuretech #machining #automation",
|
||||
"image_prompt": "Futuristic mechanical design blueprint on a digital glowing screen, dark mode aesthetic, cyber tech"
|
||||
}
|
||||
]
|
||||
|
||||
# Fikirleri veritabanına kaydet ve görsellerini üret
|
||||
for raw_idea in generated_json[:3]:
|
||||
# Stable Diffusion Görseli Üret
|
||||
import urllib.parse
|
||||
encoded_prompt = urllib.parse.quote(raw_idea.get('image_prompt', 'industrial cnc machine'))
|
||||
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
|
||||
|
||||
image_data = False
|
||||
try:
|
||||
img_resp = requests.get(pollinations_url, timeout=20)
|
||||
if img_resp.status_code == 200:
|
||||
image_data = base64.b64encode(img_resp.content)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.env['mymach.social.trend.idea'].create({
|
||||
'trend_id': self.id,
|
||||
'name': raw_idea.get('name'),
|
||||
'content': raw_idea.get('content'),
|
||||
'image_prompt': raw_idea.get('image_prompt'),
|
||||
'image_binary': image_data
|
||||
})
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def action_create_manual_post(self):
|
||||
self.ensure_one()
|
||||
self.write({'state': 'ignored'})
|
||||
return {
|
||||
'name': 'Yeni Gönderi Hazırla',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mymach.social.post',
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'context': {'default_name': f"Trend: {self.name}"}
|
||||
}
|
||||
|
||||
|
||||
class SocialTrendIdea(models.Model):
|
||||
_name = 'mymach.social.trend.idea'
|
||||
_description = 'Trend Paylaşım Fikri'
|
||||
|
||||
trend_id = fields.Many2one('mymach.social.trend', string='Trend Konu', ondelete='cascade')
|
||||
name = fields.Char(string='Fikir Başlığı', required=True)
|
||||
content = fields.Text(string='Gönderi Açıklaması', required=True)
|
||||
image_prompt = fields.Text(string='Görsel Tarifi (Prompt)')
|
||||
image_binary = fields.Image(string='Önerilen Görsel')
|
||||
|
||||
def action_select_idea(self):
|
||||
self.ensure_one()
|
||||
|
||||
# Trend durumunu güncelle
|
||||
if self.trend_id:
|
||||
self.trend_id.write({'state': 'selected'})
|
||||
|
||||
# 1. Gönderiyi (Post) yarat
|
||||
post = self.env['mymach.social.post'].create({
|
||||
'name': self.name,
|
||||
'content': self.content,
|
||||
})
|
||||
|
||||
# 2. Eğer görsel varsa, eki (attachment) oluştur ve gönderiye bağla
|
||||
if self.image_binary:
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': f"{self.name.replace(' ', '_')}_image.png",
|
||||
'datas': self.image_binary,
|
||||
'res_model': 'mymach.social.post',
|
||||
'res_id': post.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
post.write({
|
||||
'media_ids': [(4, attachment.id)]
|
||||
})
|
||||
|
||||
# 3. Kullanıcıyı oluşturulan gönderinin form ekranına yönlendir
|
||||
return {
|
||||
'name': 'Gönderi Detayı',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mymach.social.post',
|
||||
'res_id': post.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
295
extra-addons/mymach_social_nextgen/models/social_video_script.py
Normal file
295
extra-addons/mymach_social_nextgen/models/social_video_script.py
Normal file
@@ -0,0 +1,295 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SocialVideoScript(models.Model):
|
||||
_name = 'mymach.social.video.script'
|
||||
_description = 'AI Video Senaryo ve Storyboard'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'create_date desc'
|
||||
|
||||
name = fields.Char(string='Video Başlığı', required=True, tracking=True)
|
||||
topic = fields.Text(string='Video Konusu / Fikir', required=True, tracking=True)
|
||||
|
||||
platform = fields.Selection([
|
||||
('tiktok', 'TikTok'),
|
||||
('reels', 'Instagram Reels'),
|
||||
('shorts', 'YouTube Shorts')
|
||||
], string='Hedef Platform', default='reels', required=True, tracking=True)
|
||||
|
||||
duration = fields.Selection([
|
||||
('15', '15 Saniye (Kısa & Hızlı)'),
|
||||
('30', '30 Saniye (Standart)'),
|
||||
('60', '60 Saniye (Detaylı)')
|
||||
], string='Hedef Süre', default='30', required=True, tracking=True)
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Taslak'),
|
||||
('generated', 'Senaryo Üretildi'),
|
||||
('in_production', 'Çekim Aşamasında'),
|
||||
('done', 'Tamamlandı')
|
||||
], string='Durum', default='draft', tracking=True)
|
||||
|
||||
ai_provider = fields.Selection([
|
||||
('default', 'Sistem Varsayılanı'),
|
||||
('gemini', 'Google Gemini'),
|
||||
('openai', 'OpenAI (ChatGPT)'),
|
||||
('claude', 'Anthropic (Claude)'),
|
||||
('grok', 'xAI (Grok)'),
|
||||
('groq', 'Groq (Llama 3)')
|
||||
], string='Yapay Zeka', default='default', required=True, tracking=True)
|
||||
|
||||
voice_type = fields.Selection([
|
||||
('male', 'Erkek Sesi'),
|
||||
('female', 'Kadın Sesi')
|
||||
], string='Dış Ses Tipi', default='male', tracking=True)
|
||||
|
||||
language = fields.Selection([
|
||||
('tr', 'Türkçe'),
|
||||
('en', 'İngilizce')
|
||||
], string='Video Dili', default='tr', tracking=True)
|
||||
|
||||
render_status = fields.Selection([
|
||||
('not_started', 'Render Edilmedi'),
|
||||
('processing', 'Üretiliyor (Render)'),
|
||||
('done', 'Tamamlandı'),
|
||||
('error', 'Hata')
|
||||
], string='Video Durumu', default='not_started', tracking=True)
|
||||
|
||||
video_url = fields.Char(string='Video URL')
|
||||
render_task_id = fields.Char(string='Render Görev ID')
|
||||
|
||||
scene_ids = fields.One2many('mymach.social.video.scene', 'script_id', string='Sahneler (Storyboard)')
|
||||
|
||||
music_suggestion = fields.Char(string='Müzik/Ses Önerisi', readonly=True)
|
||||
caption_suggestion = fields.Text(string='Açıklama (Caption) Önerisi', readonly=True)
|
||||
|
||||
audio_file = fields.Binary(string='Dış Ses (MP3)', attachment=True)
|
||||
audio_file_name = fields.Char(string='Ses Dosyası Adı')
|
||||
audio_player_html = fields.Html(string='Ses Çalar', compute='_compute_audio_player', sanitize=False)
|
||||
|
||||
@api.depends('audio_file')
|
||||
def _compute_audio_player(self):
|
||||
for record in self:
|
||||
if record.audio_file and record.id:
|
||||
url = f"/web/content/mymach.social.video.script/{record.id}/audio_file"
|
||||
record.audio_player_html = f'<audio controls style="width: 100%;"><source src="{url}" type="audio/mpeg">Tarayıcınız ses oynatıcıyı desteklemiyor.</audio>'
|
||||
else:
|
||||
record.audio_player_html = '<p class="text-muted">Henüz ses üretilmedi.</p>'
|
||||
|
||||
def action_generate_script(self):
|
||||
self.ensure_one()
|
||||
|
||||
system_prompt = f"""Sen viral dikey videolar (TikTok, Reels, Shorts) hazırlayan profesyonel bir yönetmen ve metin yazarısın.
|
||||
Kullanıcı senden {self.duration} saniyelik bir {self.platform} videosu için saniye saniye storyboard çıkarmanı istiyor.
|
||||
Videonun Konusu: {self.topic}
|
||||
|
||||
Yanıtını kesinlikle aşağıdaki JSON formatında, ekstra markdown veya metin eklemeden döndür:
|
||||
{{
|
||||
"music_suggestion": "Trend olan uygun bir müzik veya ses efekti türü önerisi",
|
||||
"caption_suggestion": "Sosyal medya açıklaması ve hashtagler",
|
||||
"scenes": [
|
||||
{{
|
||||
"sequence": 1,
|
||||
"time_range": "0:00 - 0:03",
|
||||
"visual_action": "Ekranda kameranın neyi çekeceği, nasıl bir hareket olacağı",
|
||||
"voiceover": "Dış sesin söyleyeceği cümle",
|
||||
"overlay_text": "Videonun tam o anında ekranda belirmesi gereken dikkat çekici yazı"
|
||||
}},
|
||||
... (videonun süresine göre sahneleri doldur)
|
||||
]
|
||||
}}
|
||||
"""
|
||||
try:
|
||||
# Seçilen AI sağlayıcısını belirle
|
||||
provider = self.ai_provider if self.ai_provider != 'default' else None
|
||||
|
||||
# Yapay Zeka Yöneticisinden İstek At
|
||||
text_output = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt="Lütfen yukarıdaki konuya uygun bir video senaryosu oluştur.",
|
||||
system_prompt=system_prompt,
|
||||
response_format='json',
|
||||
provider=provider
|
||||
)
|
||||
|
||||
data = json.loads(text_output)
|
||||
|
||||
# Mevcut sahneleri sil
|
||||
self.scene_ids.unlink()
|
||||
|
||||
self.music_suggestion = data.get('music_suggestion', '')
|
||||
self.caption_suggestion = data.get('caption_suggestion', '')
|
||||
|
||||
for scene_data in data.get('scenes', []):
|
||||
self.env['mymach.social.video.scene'].create({
|
||||
'script_id': self.id,
|
||||
'sequence': scene_data.get('sequence', 1),
|
||||
'time_range': scene_data.get('time_range', ''),
|
||||
'visual_action': scene_data.get('visual_action', ''),
|
||||
'voiceover': scene_data.get('voiceover', ''),
|
||||
'overlay_text': scene_data.get('overlay_text', ''),
|
||||
})
|
||||
|
||||
self.state = 'generated'
|
||||
except json.JSONDecodeError:
|
||||
raise UserError("Gemini geçersiz bir JSON formatı döndürdü. Lütfen tekrar deneyin.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise UserError(f"API Bağlantı Hatası: {e}")
|
||||
|
||||
def action_set_in_production(self):
|
||||
for record in self:
|
||||
record.state = 'in_production'
|
||||
|
||||
def action_set_done(self):
|
||||
for record in self:
|
||||
record.state = 'done'
|
||||
|
||||
def action_generate_voiceover(self):
|
||||
"""OpenAI TTS vb. kullanarak dış ses (MP3) oluşturur."""
|
||||
self.ensure_one()
|
||||
if not self.scene_ids:
|
||||
raise UserError("Sahneler (storyboard) bulunamadı. Önce senaryo üretmelisiniz.")
|
||||
|
||||
voiceover_parts = [scene.voiceover for scene in self.scene_ids if scene.voiceover]
|
||||
full_script = " ".join(voiceover_parts)
|
||||
|
||||
if not full_script:
|
||||
raise UserError("Senaryoda dış ses metni (voiceover) bulunamadı.")
|
||||
|
||||
# OpenAI API Anahtarı
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından (MyMach Social > Yapay Zeka) OpenAI API Anahtarını yapılandırın (Ses Üretimi İçin).")
|
||||
|
||||
url = "https://api.openai.com/v1/audio/speech"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
voice = "onyx" if self.voice_type == 'male' else "nova"
|
||||
|
||||
payload = {
|
||||
"model": "tts-1",
|
||||
"input": full_script,
|
||||
"voice": voice
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
if response.status_code == 200:
|
||||
import base64
|
||||
self.audio_file = base64.b64encode(response.content)
|
||||
self.audio_file_name = f"voiceover_{self.id}.mp3"
|
||||
self.message_post(body="Dış Ses (Voiceover) başarıyla üretildi.")
|
||||
else:
|
||||
raise UserError(f"Ses üretim hatası (API): {response.text}")
|
||||
except Exception as e:
|
||||
raise UserError(f"Bağlantı hatası: {str(e)}")
|
||||
|
||||
def action_render_video(self):
|
||||
"""Send video request to Creatify API."""
|
||||
self.ensure_one()
|
||||
if not self.scene_ids:
|
||||
raise UserError("Render edilecek sahneler (storyboard) bulunamadı. Önce senaryo üretmelisiniz.")
|
||||
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.creatify_api_key')
|
||||
if not api_key:
|
||||
raise UserError("Lütfen Odoo Ayarlarından Creatify API Anahtarını yapılandırın (15s Video Render için).")
|
||||
|
||||
# Senaryoyu seslendirme ve görsel birleştirme için tam metin (script) haline getirelim
|
||||
voiceover_parts = []
|
||||
for scene in self.scene_ids:
|
||||
if scene.voiceover:
|
||||
voiceover_parts.append(scene.voiceover)
|
||||
|
||||
full_script = " ".join(voiceover_parts)
|
||||
if not full_script:
|
||||
full_script = self.topic
|
||||
|
||||
# Örnek Creatify API Request (Temsili endpoint ve payload, Creatify dökümanına göre uyarlanmıştır)
|
||||
url = "https://api.creatify.ai/api/v1/videos"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
# Creatify Payload (Senaryo, Dil, Ses Seçimi)
|
||||
payload = {
|
||||
"script": full_script,
|
||||
"duration": int(self.duration),
|
||||
"language": self.language,
|
||||
"voice": self.voice_type,
|
||||
"title": self.name,
|
||||
"format": "9:16",
|
||||
"render_subtitles": True
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
if response.status_code in [200, 201]:
|
||||
data = response.json()
|
||||
self.render_task_id = str(data.get('video_id') or data.get('id'))
|
||||
self.render_status = 'processing'
|
||||
self.message_post(body="Video Render işlemi Creatify API'sine başarıyla gönderildi. 15 saniyelik sesli ve alt yazılı videonuz hazırlanıyor. Bu işlem vài dakika sürebilir.")
|
||||
else:
|
||||
raise UserError(f"Creatify API Hatası: {response.text}")
|
||||
except Exception as e:
|
||||
raise UserError(f"API Bağlantı Hatası: {str(e)}")
|
||||
|
||||
@api.model
|
||||
def _cron_check_video_renders(self):
|
||||
"""Check the status of processing videos on Creatify."""
|
||||
scripts = self.search([('render_status', '=', 'processing'), ('render_task_id', '!=', False)])
|
||||
if not scripts:
|
||||
return
|
||||
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.creatify_api_key')
|
||||
if not api_key:
|
||||
return
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
for script in scripts:
|
||||
try:
|
||||
url = f"https://api.creatify.ai/api/v1/videos/{script.render_task_id}"
|
||||
response = requests.get(url, headers=headers, timeout=20)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
status = data.get('status')
|
||||
|
||||
if status in ['completed', 'done', 'succeeded']:
|
||||
script.render_status = 'done'
|
||||
script.state = 'done'
|
||||
script.video_url = data.get('url') or data.get('video_url')
|
||||
script.message_post(body="Video Render işlemi başarıyla tamamlandı! Tam 15 saniyelik sesli videonuzu izleyebilirsiniz.")
|
||||
elif status in ['failed', 'error']:
|
||||
script.render_status = 'error'
|
||||
script.message_post(body=f"Video Render işlemi HATA aldı: {data.get('error', 'Bilinmeyen hata')}")
|
||||
elif status == 'canceled':
|
||||
script.render_status = 'error'
|
||||
script.message_post(body="Video Render işlemi iptal edildi.")
|
||||
except Exception as e:
|
||||
_logger.error(f"Creatify Check Error for {script.id}: {e}")
|
||||
|
||||
|
||||
|
||||
class SocialVideoScene(models.Model):
|
||||
_name = 'mymach.social.video.scene'
|
||||
_description = 'Video Sahnesi (Storyboard)'
|
||||
_order = 'sequence, id'
|
||||
|
||||
script_id = fields.Many2one('mymach.social.video.script', string='Video Senaryosu', required=True, ondelete='cascade')
|
||||
sequence = fields.Integer(string='Sıra', required=True)
|
||||
time_range = fields.Char(string='Süre (Zaman Aralığı)', required=True, help="Örn: 0:00 - 0:03")
|
||||
|
||||
visual_action = fields.Text(string='Görsel Aksiyon (Kamera Ne Çekiyor?)')
|
||||
voiceover = fields.Text(string='Dış Ses / Diyalog')
|
||||
overlay_text = fields.Char(string='Ekrandaki Metin (Yazı)')
|
||||
@@ -0,0 +1,48 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Odoo Sosyal Gönderisi (mymach.social.post) bul veya yarat
|
||||
post = env['mymach.social.post'].search([('name', '=', 'Odoo Sosyal Özellikleri')], limit=1)
|
||||
if not post:
|
||||
# Get any account to link
|
||||
account = env['mymach.social.account'].search([], limit=1)
|
||||
post = env['mymach.social.post'].create({
|
||||
'name': 'Odoo Sosyal Özellikleri',
|
||||
'content': 'Sosyal pazarlama modülümüz için yakında gelecek yeni özelliklerimizi duyurmaktan heyecan duyuyoruz! 🚀 #Odoo19 #Pazarlama',
|
||||
'account_ids': [(4, account.id)] if account else [],
|
||||
'state': 'published',
|
||||
'published_date': odoo.fields.Datetime.now(),
|
||||
})
|
||||
print(f"Created new published post: {post.name}")
|
||||
|
||||
# 2. Bu gönderiyi temsil eden akış kayıtlarını (stream.post) güncelle
|
||||
stream_posts = env['mymach.social.stream.post'].search([
|
||||
('message', 'like', 'Sosyal pazarlama modülümüz için yakında gelecek')
|
||||
])
|
||||
if stream_posts:
|
||||
stream_posts.write({
|
||||
'post_id': post.id,
|
||||
'message_type': 'post'
|
||||
})
|
||||
print(f"Linked {len(stream_posts)} stream posts of type 'post' to Odoo post.")
|
||||
|
||||
# 3. Yorumları (Ahmet Yılmaz vb.) bu gönderiye bağla (çünkü bu gönderinin altına yapılmış bir yorum)
|
||||
comments = env['mymach.social.stream.post'].search([
|
||||
('message', 'like', 'fiyat bilgisi alabilir miyim')
|
||||
])
|
||||
if comments:
|
||||
comments.write({
|
||||
'post_id': post.id,
|
||||
'message_type': 'comment'
|
||||
})
|
||||
print(f"Linked {len(comments)} comment stream posts to Odoo post.")
|
||||
|
||||
print("Association successfully updated!")
|
||||
@@ -0,0 +1,34 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. 'action_social_dashboard' eylemine bağlı olan tüm menüleri (ir.ui.menu) bul ve sil
|
||||
action = env['ir.actions.act_window'].search([('name', '=', 'Analiz Dashboard')], limit=1)
|
||||
if action:
|
||||
menus = env['ir.ui.menu'].search([('action', '=', f'ir.actions.act_window,{action.id}')])
|
||||
if menus:
|
||||
print(f"Found {len(menus)} menus linked to Analiz Dashboard action. Deleting...")
|
||||
menus.unlink()
|
||||
else:
|
||||
print("No menus linked to the action found.")
|
||||
|
||||
# Eylemin kendisini de silelim
|
||||
print("Deleting the action 'Analiz Dashboard'...")
|
||||
action.unlink()
|
||||
else:
|
||||
print("Action 'Analiz Dashboard' not found.")
|
||||
|
||||
# 2. İsmi doğrudan 'Analiz Dashboard' olan menüleri de kontrol edip silelim
|
||||
direct_menus = env['ir.ui.menu'].search([('name', '=', 'Analiz Dashboard')])
|
||||
if direct_menus:
|
||||
print(f"Found {len(direct_menus)} menus named 'Analiz Dashboard'. Deleting...")
|
||||
direct_menus.unlink()
|
||||
|
||||
print("Database cleanup completed successfully!")
|
||||
@@ -0,0 +1,24 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
media_records = env['mymach.social.media'].search([('post_id', '=', False)])
|
||||
print(f"Found {len(media_records)} media records without post_id. Syncing...")
|
||||
|
||||
synced_count = 0
|
||||
for media in media_records:
|
||||
# Find any social post referencing this attachment
|
||||
post = env['mymach.social.post'].search([('media_ids', 'in', [media.attachment_id.id])], limit=1)
|
||||
if post:
|
||||
media.write({'post_id': post.id})
|
||||
synced_count += 1
|
||||
print(f"Synced Media '{media.name}' to Post '{post.name}'")
|
||||
|
||||
print(f"Completed! Synced {synced_count} records.")
|
||||
@@ -0,0 +1,51 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# Yayınlanmış tüm Odoo gönderilerini (social.post) ara
|
||||
published_posts = env['mymach.social.post'].search([('state', '=', 'published')])
|
||||
print(f"Found {len(published_posts)} published posts in total.")
|
||||
|
||||
for post in published_posts:
|
||||
for account in post.account_ids:
|
||||
# Bu hesap ve gönderi için zaten akış kaydı var mı kontrol et
|
||||
existing = env['mymach.social.stream.post'].search([
|
||||
('post_id', '=', post.id),
|
||||
('account_id', '=', account.id)
|
||||
], limit=1)
|
||||
|
||||
if not existing:
|
||||
# Akış (Zaman Tüneli / Feed) bul veya oluştur
|
||||
stream = env['mymach.social.stream'].search([
|
||||
('account_id', '=', account.id),
|
||||
('stream_type', '=', 'feed')
|
||||
], limit=1)
|
||||
if not stream:
|
||||
stream = env['mymach.social.stream'].create({
|
||||
'account_id': account.id,
|
||||
'name': f"{account.name} - Zaman Tüneli",
|
||||
'stream_type': 'feed'
|
||||
})
|
||||
|
||||
# Kontrol paneline yansıyacak gönderi (stream.post) kaydını oluştur
|
||||
env['mymach.social.stream.post'].create({
|
||||
'stream_id': stream.id,
|
||||
'author_name': account.name,
|
||||
'published_date': post.published_date or post.scheduled_date or post.write_date,
|
||||
'message': post.content or post.name,
|
||||
'platform_post_id': f"PLATFORM_{account.id}_{post.id}",
|
||||
'message_type': 'post',
|
||||
'post_id': post.id
|
||||
})
|
||||
print(f"Successfully synced '{post.name}' to stream dashboard for account '{account.name}'")
|
||||
else:
|
||||
print(f"Stream post for '{post.name}' on account '{account.name}' already exists.")
|
||||
|
||||
print("Sync process completed!")
|
||||
@@ -0,0 +1,44 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_logger = logging.getLogger('CompetitorTest')
|
||||
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
def run():
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Create a Competitor
|
||||
comp = env['social.competitor'].search([('name', '=', 'Test Rival')], limit=1)
|
||||
if not comp:
|
||||
comp = env['social.competitor'].create({
|
||||
'name': 'Test Rival',
|
||||
'platform': 'linkedin',
|
||||
'profile_url': 'https://linkedin.com/company/test-rival',
|
||||
'followers_count': 15000,
|
||||
})
|
||||
_logger.info(f"Created Test Rival: {comp.id}")
|
||||
|
||||
# Add old history
|
||||
env['social.competitor.history'].create({
|
||||
'competitor_id': comp.id,
|
||||
'followers_count': 14000,
|
||||
'engagement_rate': 2.5
|
||||
})
|
||||
|
||||
# 2. Trigger Action Scrape Data (Gemini Integration)
|
||||
_logger.info("Triggering action_scrape_data()...")
|
||||
comp.action_scrape_data()
|
||||
|
||||
_logger.info(f"New Followers: {comp.followers_count}")
|
||||
_logger.info(f"Growth Rate: {comp.growth_rate}%")
|
||||
_logger.info(f"AI Summary: {comp.ai_summary}")
|
||||
_logger.info(f"Top Keywords: {comp.top_keywords}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -0,0 +1,51 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
import requests
|
||||
import json
|
||||
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
api_key = env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||
print("API Key exists:", bool(api_key))
|
||||
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
system_prompt = (
|
||||
"Sen profesyonel bir sosyal medya duygu analizi uzmanısın. "
|
||||
"Verilen Türkçe veya İngilizce sosyal medya yorumunu analiz et. "
|
||||
"Şu JSON formatında cevap ver:\n"
|
||||
"{\n"
|
||||
" \"sentiment\": \"positive\" | \"neutral\" | \"negative\",\n"
|
||||
" \"reason\": \"Metnin neden bu duygu durumuna sahip olduğuna dair kısa, net, profesyonel Türkçe açıklama (max 2 cümle).\"\n"
|
||||
"}"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"system_instruction": {"parts": [{"text": system_prompt}]},
|
||||
"contents": [{"parts": [{"text": "Yorum: Makineleriniz harika çalışıyor, çok hızlı kargo ve muazzam bir ilgi alaka var! Teşekkürler 😊👍"}]}],
|
||||
"generationConfig": {
|
||||
"responseMimeType": "application/json",
|
||||
"responseSchema": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"sentiment": {
|
||||
"type": "STRING",
|
||||
"enum": ["positive", "neutral", "negative"]
|
||||
},
|
||||
"reason": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": ["sentiment", "reason"]
|
||||
},
|
||||
"temperature": 0.1,
|
||||
"maxOutputTokens": 1000
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload, timeout=10)
|
||||
print("Status:", response.status_code)
|
||||
print("Raw Response:", response.text)
|
||||
@@ -0,0 +1,39 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Akış bulalım
|
||||
stream = env['mymach.social.stream'].search([], limit=1)
|
||||
print(f"Using stream: {stream.name}")
|
||||
|
||||
# 2. Test verilerini oluşturalım (özellikle 'kusursuz' vb. kelimeleri içeren olumlu/olumsuz metinler)
|
||||
test_comments = [
|
||||
("Bu makine gerçekten kusursuz çalışıyor, işimizi çok kolaylaştırdı.", "positive"),
|
||||
("Parçaların kalitesi mükemmel, elinize sağlık.", "positive"),
|
||||
("Hizmetiniz şahane, çok memnun kaldım.", "positive"),
|
||||
("Sistem rezalet, sürekli hata veriyor ve çok yavaş.", "negative"),
|
||||
("Ürün arızalı çıktı, iade istiyorum.", "negative"),
|
||||
("Makinenin çalışma voltajı nedir, kaç watt güç tüketiyor?", "neutral")
|
||||
]
|
||||
|
||||
for msg, expected in test_comments:
|
||||
post = env['mymach.social.stream.post'].create({
|
||||
'stream_id': stream.id,
|
||||
'author_name': 'Test Kullanıcısı',
|
||||
'message': msg,
|
||||
'message_type': 'comment'
|
||||
})
|
||||
# Analizi tetikleyelim
|
||||
post.action_analyze_sentiment()
|
||||
|
||||
print(f"\nMesaj: '{post.message}'")
|
||||
print(f"-> Tespit Edilen Duygu: {post.sentiment} (Beklenen: {expected})")
|
||||
|
||||
print("\nKelime dağarcığı senaryo testleri başarıyla tamamlandı!")
|
||||
@@ -0,0 +1,121 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_logger = logging.getLogger('SystemIntegrityTest')
|
||||
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
def run_tests():
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
errors = []
|
||||
fixes_needed = []
|
||||
|
||||
_logger.info("--- 1. Testing Watermark Logic (mymach.social.post) ---")
|
||||
try:
|
||||
# Create a mock image attachment
|
||||
import base64
|
||||
img_data = base64.b64encode(b'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').decode('utf-8')
|
||||
attachment = env['ir.attachment'].create({
|
||||
'name': 'test_image.gif',
|
||||
'datas': img_data,
|
||||
'res_model': 'mymach.social.post',
|
||||
'mimetype': 'image/gif'
|
||||
})
|
||||
|
||||
# Need an account first
|
||||
account = env['mymach.social.account'].search([], limit=1)
|
||||
if not account:
|
||||
account = env['mymach.social.account'].create({'name': 'Test Account', 'platform': 'facebook'})
|
||||
|
||||
post = env['mymach.social.post'].create({
|
||||
'name': 'Test Post',
|
||||
'content': 'Test Post for Watermark',
|
||||
'account_ids': [(4, account.id)],
|
||||
'media_ids': [(4, attachment.id)]
|
||||
})
|
||||
_logger.info(f"Post created: {post.id}, Attachment: {attachment.id}")
|
||||
except Exception as e:
|
||||
errors.append(f"Watermark Test Failed: {e}")
|
||||
_logger.error(traceback.format_exc())
|
||||
|
||||
_logger.info("--- 2. Testing Sentiment Analysis & CRM Action (mymach.social.stream.post) ---")
|
||||
try:
|
||||
stream = env['mymach.social.stream'].search([], limit=1)
|
||||
if not stream:
|
||||
stream = env['mymach.social.stream'].create({'name': 'Test Stream', 'stream_type': 'facebook'})
|
||||
|
||||
stream_post = env['mymach.social.stream.post'].create({
|
||||
'stream_id': stream.id,
|
||||
'author_name': 'Test User',
|
||||
'message': 'Sisteminiz kusursuz çalışıyor, harika bir iş.',
|
||||
'message_type': 'comment'
|
||||
})
|
||||
stream_post.action_analyze_sentiment()
|
||||
if stream_post.sentiment != 'positive':
|
||||
errors.append(f"Sentiment Analysis Failed: Expected 'positive', got '{stream_post.sentiment}'")
|
||||
else:
|
||||
_logger.info(f"Sentiment Analysis OK: {stream_post.sentiment}")
|
||||
|
||||
action = stream_post.action_create_lead()
|
||||
if not stream_post.lead_id:
|
||||
errors.append("Lead creation failed.")
|
||||
else:
|
||||
_logger.info(f"CRM Lead created: {stream_post.lead_id.name}")
|
||||
except Exception as e:
|
||||
errors.append(f"Stream Post Test Failed: {e}")
|
||||
_logger.error(traceback.format_exc())
|
||||
|
||||
_logger.info("--- 3. Testing Auto Reply Engine (mymach.social.auto.reply) ---")
|
||||
try:
|
||||
rule = env['mymach.social.auto.reply'].create({
|
||||
'name': 'Test Positive Rule',
|
||||
'keyword': 'mükemmel',
|
||||
'reply_message': 'Teşekkürler, çok naziksiniz!',
|
||||
'apply_to_message_type': 'all',
|
||||
'active': True
|
||||
})
|
||||
|
||||
stream_post_2 = env['mymach.social.stream.post'].create({
|
||||
'stream_id': stream.id,
|
||||
'author_name': 'Test User 2',
|
||||
'message': 'Bu ürün mükemmel, çok beğendim.',
|
||||
'message_type': 'comment',
|
||||
'sentiment': 'positive'
|
||||
})
|
||||
|
||||
logs = env['mymach.social.auto.reply.log'].search([('post_id', '=', stream_post_2.id)])
|
||||
_logger.info(f"Auto Reply Logs found: {len(logs)}")
|
||||
except Exception as e:
|
||||
errors.append(f"Auto Reply Test Failed: {e}")
|
||||
_logger.error(traceback.format_exc())
|
||||
|
||||
_logger.info("--- 4. Testing Trend Engine Integration (mymach.social.trend) ---")
|
||||
try:
|
||||
trend = env['mymach.social.trend'].search([], limit=1)
|
||||
if not trend:
|
||||
trend = env['mymach.social.trend'].create({
|
||||
'name': 'AI Integration in CRM',
|
||||
'keyword': 'AI CRM'
|
||||
})
|
||||
_logger.info(f"Trend initialized: {trend.name}")
|
||||
except Exception as e:
|
||||
errors.append(f"Trend Engine Test Failed: {e}")
|
||||
_logger.error(traceback.format_exc())
|
||||
|
||||
return errors, fixes_needed
|
||||
|
||||
if __name__ == '__main__':
|
||||
errors, fixes = run_tests()
|
||||
print("================== TEST RESULTS ==================")
|
||||
if errors:
|
||||
print("ERRORS FOUND:")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
else:
|
||||
print("ALL TESTS PASSED WITH NO CRASHES.")
|
||||
@@ -0,0 +1,44 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
import logging
|
||||
import time
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_logger = logging.getLogger('VideoTest')
|
||||
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
def run():
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# Odoo API Key'i ayarlayalım ki Gemini API çalışsın.
|
||||
# Bu test için örnekte geçici olarak test ediyoruz.
|
||||
# env['ir.config_parameter'].sudo().set_param('mymach_social.gemini_api_key', 'TEST_KEY_HERE')
|
||||
|
||||
# 1. Create a Video Script
|
||||
script = env['mymach.social.video.script'].create({
|
||||
'name': 'Yeni CNC Tezgah Tanıtımı',
|
||||
'topic': 'Şirketimizin yeni getirdiği 5 eksenli CNC makinesinin hızını ve hassasiyetini gösteren çok enerjik bir video.',
|
||||
'platform': 'tiktok',
|
||||
'duration': '15',
|
||||
})
|
||||
_logger.info(f"Created Video Script: {script.id}")
|
||||
|
||||
_logger.info("Triggering action_generate_script()...")
|
||||
try:
|
||||
script.action_generate_script()
|
||||
_logger.info(f"State: {script.state}")
|
||||
_logger.info(f"Music Suggestion: {script.music_suggestion}")
|
||||
_logger.info(f"Caption: {script.caption_suggestion}")
|
||||
|
||||
_logger.info("Scenes Generated:")
|
||||
for scene in script.scene_ids:
|
||||
_logger.info(f"Scene {scene.sequence} ({scene.time_range}): {scene.visual_action}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Error during generation: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -0,0 +1,83 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
import base64
|
||||
import io
|
||||
from PIL import Image
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Create a dummy watermark image (PNG with transparency)
|
||||
wm_img = Image.new('RGBA', (200, 50), color=(255, 0, 0, 128)) # Semi-transparent red block
|
||||
wm_bytes = io.BytesIO()
|
||||
wm_img.save(wm_bytes, format='PNG')
|
||||
wm_base64 = base64.b64encode(wm_bytes.getvalue())
|
||||
|
||||
# Set it on the current company
|
||||
company = env.company
|
||||
company.write({
|
||||
'social_watermark_image': wm_base64,
|
||||
'social_watermark_position': 'bottom_right',
|
||||
'social_watermark_opacity': 70
|
||||
})
|
||||
print("Dummy watermark set on company.")
|
||||
|
||||
# Get or create a mock account
|
||||
account = env['mymach.social.account'].search([], limit=1)
|
||||
if not account:
|
||||
media = env['social.media'].search([], limit=1)
|
||||
if not media:
|
||||
media = env['social.media'].create({'name': 'Mock Media', 'image': False})
|
||||
account = env['mymach.social.account'].create({
|
||||
'name': 'Mock Account',
|
||||
'media_id': media.id,
|
||||
'platform': 'facebook',
|
||||
})
|
||||
print(f"Using account: {account.id} ({account.name})")
|
||||
|
||||
# 2. Create a dummy post image to be watermarked (JPEG format)
|
||||
post_img = Image.new('RGB', (800, 600), color=(0, 255, 0)) # Green background
|
||||
post_bytes = io.BytesIO()
|
||||
post_img.save(post_bytes, format='JPEG')
|
||||
post_base64 = base64.b64encode(post_bytes.getvalue())
|
||||
|
||||
# Create ir.attachment for this image
|
||||
attachment = env['ir.attachment'].create({
|
||||
'name': 'test_post_image.jpg',
|
||||
'datas': post_base64,
|
||||
'mimetype': 'image/jpeg',
|
||||
'res_model': 'mymach.social.post',
|
||||
'res_id': 0
|
||||
})
|
||||
print(f"Attachment created: {attachment.id}, is_watermarked: {attachment.is_watermarked}")
|
||||
|
||||
# 3. Create a social post linking this attachment
|
||||
post = env['mymach.social.post'].create({
|
||||
'name': 'Watermark Test Post',
|
||||
'content': 'Checking auto watermarking features...',
|
||||
'account_ids': [(4, account.id)],
|
||||
'media_ids': [(4, attachment.id)]
|
||||
})
|
||||
print(f"Post created: {post.id}, state: {post.state}")
|
||||
|
||||
# Refresh attachment from db
|
||||
attachment.invalidate_recordset()
|
||||
print(f"After post creation - Attachment is_watermarked: {attachment.is_watermarked}")
|
||||
|
||||
# Check if the watermark actually modified the data
|
||||
if attachment.is_watermarked:
|
||||
new_data = base64.b64decode(attachment.datas)
|
||||
# Try loading the saved image to verify it's still a valid image
|
||||
try:
|
||||
saved_img = Image.open(io.BytesIO(new_data))
|
||||
print(f"Successfully loaded watermarked image: size={saved_img.size}, format={saved_img.format}")
|
||||
print("TEST SUCCESS: Auto-watermarking works perfectly!")
|
||||
except Exception as e:
|
||||
print(f"TEST FAILED: Watermarked image could not be loaded: {e}")
|
||||
else:
|
||||
print("TEST FAILED: Attachment was not marked as watermarked.")
|
||||
@@ -0,0 +1,17 @@
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
# Bootstrap Odoo
|
||||
odoo.tools.config.parse_config([])
|
||||
registry = Registry('mymach_dev')
|
||||
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
# Update Odoo Sosyal post type to 'post'
|
||||
posts = env['mymach.social.stream.post'].search([('author_name', '=', 'Odoo Sosyal')])
|
||||
if posts:
|
||||
posts.write({'message_type': 'post'})
|
||||
print(f"Updated {len(posts)} posts to type 'post'.")
|
||||
else:
|
||||
print("No posts found with author 'Odoo Sosyal'.")
|
||||
@@ -12,3 +12,12 @@ access_social_media,access.mymach.social.media,model_mymach_social_media,base.gr
|
||||
access_social_media_tag,access.mymach.social.media.tag,model_mymach_social_media_tag,base.group_user,1,1,1,1
|
||||
access_social_auto_reply,access.mymach.social.auto.reply,model_mymach_social_auto_reply,base.group_user,1,1,1,1
|
||||
access_social_auto_reply_log,access.mymach.social.auto.reply.log,model_mymach_social_auto_reply_log,base.group_user,1,1,1,1
|
||||
access_social_trend,access.mymach.social.trend,model_mymach_social_trend,base.group_user,1,1,1,1
|
||||
access_social_trend_idea,access.mymach.social.trend.idea,model_mymach_social_trend_idea,base.group_user,1,1,1,1
|
||||
access_social_competitor_history,social.competitor.history,model_social_competitor_history,base.group_user,1,1,1,1
|
||||
|
||||
access_mymach_social_video_script,mymach.social.video.script,model_mymach_social_video_script,base.group_user,1,1,1,1
|
||||
access_mymach_social_video_scene,mymach.social.video.scene,model_mymach_social_video_scene,base.group_user,1,1,1,1
|
||||
access_mymach_social_dashboard,access_mymach_social_dashboard,model_mymach_social_dashboard,base.group_user,1,0,0,0
|
||||
access_social_competitor_follower,social.competitor.follower,model_social_competitor_follower,base.group_user,1,1,1,1
|
||||
access_mymach_social_competitor_ad,mymach.social.competitor.ad,mymach_social_nextgen.model_mymach_social_competitor_ad,base.group_user,1,1,1,1
|
||||
|
||||
|
@@ -0,0 +1,46 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class SocialDashboard extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
|
||||
this.state = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
onWillStart(async () => {
|
||||
this.state.data = await this.orm.call(
|
||||
"mymach.social.dashboard",
|
||||
"get_dashboard_data",
|
||||
[]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
openPosts() {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: "Gönderiler",
|
||||
res_model: "mymach.social.post",
|
||||
views: [[false, "list"], [false, "form"]],
|
||||
});
|
||||
}
|
||||
|
||||
openAlert(alertId) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "mymach.social.stream.post",
|
||||
res_id: alertId,
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SocialDashboard.template = "mymach_social_nextgen.Dashboard";
|
||||
|
||||
registry.category("actions").add("mymach_social_dashboard", SocialDashboard);
|
||||
@@ -0,0 +1,26 @@
|
||||
.o_mymach_dashboard {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
|
||||
.card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-primary-light { background-color: rgba(13, 110, 253, 0.1); }
|
||||
.bg-success-light { background-color: rgba(25, 135, 84, 0.1); }
|
||||
.bg-warning-light { background-color: rgba(255, 193, 7, 0.1); }
|
||||
.bg-info-light { background-color: rgba(13, 202, 240, 0.1); }
|
||||
|
||||
.list-group-item {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
|
||||
&:first-child { border-top: 0; }
|
||||
&:last-child { border-bottom: 0; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mymach_social_nextgen.Dashboard" owl="1">
|
||||
<div class="o_action_manager">
|
||||
<div class="o_mymach_dashboard container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-primary fw-bold"><i class="fa fa-dashboard me-2"></i> Sosyal Medya Yönetim Paneli</h2>
|
||||
<p class="text-muted">Tüm hesaplarınızın yapay zeka destekli performans özeti.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Posts Card -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 shadow-sm border-0 border-start border-primary border-4" t-on-click="openPosts" style="cursor: pointer;">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-2">Toplam Gönderi</h6>
|
||||
<h3 class="fw-bold mb-0" t-esc="state.data.total_posts"/>
|
||||
</div>
|
||||
<div class="bg-primary-light p-3 rounded">
|
||||
<i class="fa fa-paper-plane fa-2x text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-sm">
|
||||
<span class="text-success fw-bold"><t t-esc="state.data.published_posts"/> Yayında</span>
|
||||
<span class="text-muted ms-2"><t t-esc="state.data.scheduled_posts"/> Zamanlanmış</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audience Card -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 shadow-sm border-0 border-start border-success border-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-2">Rakip Kitle Hacmi</h6>
|
||||
<h3 class="fw-bold mb-0" t-esc="state.data.total_competitor_audience"/>
|
||||
</div>
|
||||
<div class="bg-success-light p-3 rounded">
|
||||
<i class="fa fa-users fa-2x text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-muted">
|
||||
Takip edilen tüm rakiplerin toplam takipçi sayısı
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Video Card -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 shadow-sm border-0 border-start border-warning border-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-2">Üretilen AI Videoları</h6>
|
||||
<h3 class="fw-bold mb-0" t-esc="state.data.done_videos"/>
|
||||
</div>
|
||||
<div class="bg-warning-light p-3 rounded">
|
||||
<i class="fa fa-video-camera fa-2x text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-sm text-muted">
|
||||
<t t-esc="state.data.total_videos"/> toplam senaryo içinden
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sentiment Card -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 shadow-sm border-0 border-start border-info border-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h6 class="text-muted mb-2">Toplam Etkileşim</h6>
|
||||
<h3 class="fw-bold mb-0" t-esc="state.data.sentiment?.total || 0"/>
|
||||
</div>
|
||||
<div class="bg-info-light p-3 rounded">
|
||||
<i class="fa fa-comments fa-2x text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-sm d-flex justify-content-between">
|
||||
<span class="text-success"><i class="fa fa-smile-o"></i> <t t-esc="state.data.sentiment?.positive || 0"/></span>
|
||||
<span class="text-secondary"><i class="fa fa-meh-o"></i> <t t-esc="state.data.sentiment?.neutral || 0"/></span>
|
||||
<span class="text-danger"><i class="fa fa-frown-o"></i> <t t-esc="state.data.sentiment?.negative || 0"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Sentiment Progress Bars -->
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="mb-0 fw-bold"><i class="fa fa-pie-chart text-primary me-2"></i> Duygu Analizi Özeti</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<t t-if="state.data.sentiment?.total > 0">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-success fw-bold">Olumlu</span>
|
||||
<span class="text-muted"><t t-esc="Math.round((state.data.sentiment.positive / state.data.sentiment.total) * 100)"/>%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" t-attf-style="width: {{ (state.data.sentiment.positive / state.data.sentiment.total) * 100 }}%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-secondary fw-bold">Nötr</span>
|
||||
<span class="text-muted"><t t-esc="Math.round((state.data.sentiment.neutral / state.data.sentiment.total) * 100)"/>%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-secondary" role="progressbar" t-attf-style="width: {{ (state.data.sentiment.neutral / state.data.sentiment.total) * 100 }}%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="text-danger fw-bold">Olumsuz</span>
|
||||
<span class="text-muted"><t t-esc="Math.round((state.data.sentiment.negative / state.data.sentiment.total) * 100)"/>%</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-danger" role="progressbar" t-attf-style="width: {{ (state.data.sentiment.negative / state.data.sentiment.total) * 100 }}%;" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="text-muted text-center py-5">Henüz analiz edilmiş bir yorum bulunmuyor.</p>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Table -->
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold"><i class="fa fa-warning text-danger me-2"></i> Acil Müdahale Gerekenler</h5>
|
||||
<span class="badge bg-danger rounded-pill"><t t-esc="state.data.negative_alerts?.length || 0"/> Yeni</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<t t-if="state.data.negative_alerts?.length > 0">
|
||||
<div class="list-group list-group-flush">
|
||||
<t t-foreach="state.data.negative_alerts" t-as="alert" t-key="alert.id">
|
||||
<a href="#" class="list-group-item list-group-item-action p-3" t-on-click="() => this.openAlert(alert.id)">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1 fw-bold"><i class="fa fa-user-circle text-muted me-1"></i> <t t-esc="alert.author"/></h6>
|
||||
<small class="text-muted"><t t-esc="alert.date"/></small>
|
||||
</div>
|
||||
<p class="mb-1 text-truncate text-muted" style="max-width: 90%;"><t t-esc="alert.message"/></p>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="text-center py-5">
|
||||
<i class="fa fa-check-circle text-success fa-3x mb-3"></i>
|
||||
<h6 class="text-muted">Harika! Müdahale gerektiren olumsuz bir yorum yok.</h6>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,17 +1,67 @@
|
||||
/* mymach_social_nextgen/static/src/css/social_dashboard.css */
|
||||
|
||||
/* Parent Odoo Content Container */
|
||||
.o_content:has(.o_social_dashboard_kanban) {
|
||||
background-color: #020617 !important; /* Koyu Slate arka plan */
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* 1. KANBAN VIEW (ANA ARKA PLAN) */
|
||||
.o_kanban_view.o_social_dashboard_kanban {
|
||||
background-color: #020617 !important; /* Çok daha koyu bir arka plan (Slate-950) */
|
||||
background-color: #020617 !important;
|
||||
padding: 16px !important;
|
||||
min-height: 100vh !important;
|
||||
margin: 0 !important;
|
||||
height: 100% !important;
|
||||
min-height: 100% !important;
|
||||
display: flex !important; /* Odoo'nun orijinal flex yapısını bozma (sol panel için) */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* 2. SÜTUNLAR (STREAMS) */
|
||||
/* 2. SOL PANEL (SEARCH PANEL) KOYU TEMA DÜZENLEMESİ */
|
||||
.o_social_dashboard_kanban .o_search_panel {
|
||||
background-color: #0f172a !important;
|
||||
color: #e2e8f0 !important;
|
||||
border-right: 1px solid #1e293b !important;
|
||||
flex: 0 0 260px !important;
|
||||
width: 260px !important;
|
||||
min-width: 260px !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 12px !important;
|
||||
margin-right: 16px !important;
|
||||
}
|
||||
|
||||
.o_social_dashboard_kanban .o_search_panel .list-group-item {
|
||||
background-color: transparent !important;
|
||||
color: #cbd5e1 !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.o_social_dashboard_kanban .o_search_panel .list-group-item.active {
|
||||
background-color: #1e293b !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
/* 3. KANBAN RENDERER (SÜTUNLARIN DİZİLDİĞİ ALAN) */
|
||||
.o_social_dashboard_kanban .o_kanban_renderer {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
overflow-x: auto !important; /* Yalnızca sütunlar sağa sola kayacak */
|
||||
overflow-y: auto !important;
|
||||
flex: 1 1 auto !important;
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* 4. SÜTUNLAR (STREAMS) */
|
||||
.o_social_dashboard_kanban .o_kanban_group {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 8px !important;
|
||||
flex: 0 0 350px !important; /* Kolonlar sıkışmasın */
|
||||
width: 350px !important;
|
||||
min-width: 350px !important;
|
||||
}
|
||||
|
||||
/* Sütun Başlığı (Header) */
|
||||
|
||||
18
extra-addons/mymach_social_nextgen/test_ai_manager.py
Normal file
18
extra-addons/mymach_social_nextgen/test_ai_manager.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import sys
|
||||
|
||||
def run_test(env):
|
||||
ai_utils = env['mymach.social.ai.utils']
|
||||
print("Testing default AI provider (Gemini)...")
|
||||
|
||||
try:
|
||||
res = ai_utils.generate_text(
|
||||
prompt="Hello, say 'Test OK'",
|
||||
system_prompt="You are a test bot. Say exactly what is requested.",
|
||||
response_format="text"
|
||||
)
|
||||
print("Response:", res)
|
||||
print("Default AI Manager Test PASSED")
|
||||
except Exception as e:
|
||||
print("Default AI Manager Test FAILED:", str(e))
|
||||
|
||||
run_test(env)
|
||||
@@ -60,6 +60,17 @@
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Yapay Zeka Asistan Ayarları" name="social_ai_settings_container">
|
||||
<div class="alert alert-info mt-2" role="alert">
|
||||
<strong>💡 Ücretsiz Kota Tavsiyesi:</strong> "429 Quota Exceeded" hatası alıyorsanız yapay zeka kotanız dolmuştur. Tamamen ücretsiz ve çok hızlı bir alternatif olarak <b>Groq (Llama 3)</b> kullanabilirsiniz. <a href="https://console.groq.com/keys" target="_blank">Groq API Key</a> oluşturup aşağıya ekleyin ve <i>Varsayılan Yapay Zeka</i> olarak seçin. Gemini kullanmaya devam etmek isterseniz Google AI Studio'da bir fatura hesabı bağlayıp kotanızı devasa oranda ücretsiz artırabilirsiniz.
|
||||
</div>
|
||||
<setting string="Sistem Geneli Yapay Zeka (Varsayılan)" help="Otomatik senaryo üretimi, rakip analizi ve duygu analizinde hangi yapay zekanın kullanılacağını seçin.">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="social_default_ai_provider" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_default_ai_provider" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Google Gemini" help="Gemini Pro Entegrasyonu için API Anahtarı">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
@@ -68,6 +79,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Groq (Llama 3)" help="Dünyanın en hızlı API'si (Groq) üzerinden ücretsiz Llama 3 modelleri için API Anahtarı">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="social_groq_api_key" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_groq_api_key" class="oe_inline" password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="OpenAI (ChatGPT)" help="OpenAI modelleri için API Anahtarı">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
@@ -92,6 +111,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Replicate (Video Üretimi)" help="Video Render motoru (Stable Video / Minimax vb.) için Replicate API Anahtarı">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="social_replicate_api_key" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_replicate_api_key" class="oe_inline" password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Creatify API (15s Sesli & Alt Yazılı Video)" help="Tam teşekküllü 15 saniyelik pazarlama videoları için Creatify API Anahtarı">
|
||||
<div class="content-group">
|
||||
<div class="row mt16">
|
||||
<label for="social_creatify_api_key" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_creatify_api_key" class="oe_inline" password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Medya Kütüphanesi & Filigran (Watermark) Ayarları" name="social_watermark_settings_container">
|
||||
<setting string="Şirket Logosu (Filigran)" help="Üretilen veya yüklenen medyaların üzerine otomatik basılacak PNG formatında şeffaf logo.">
|
||||
@@ -104,6 +139,16 @@
|
||||
<label for="social_watermark_position" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_watermark_position" class="oe_inline"/>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<label for="social_watermark_opacity" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_watermark_opacity" class="oe_inline" style="max-width: 100px;"/>
|
||||
<span class="text-muted col-lg-3 mt-1">% (10-100)</span>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<label for="social_watermark_size" class="col-lg-3 o_light_label"/>
|
||||
<field name="social_watermark_size" class="oe_inline" style="max-width: 100px;"/>
|
||||
<span class="text-muted col-lg-3 mt-1">% (5-50)</span>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<field name="model">mymach.social.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Social Accounts">
|
||||
<header>
|
||||
<button name="action_create_demo_instagram" type="object" string="Demo Instagram Ekle" class="btn-warning"/>
|
||||
</header>
|
||||
<field name="name"/>
|
||||
<field name="platform"/>
|
||||
<field name="active"/>
|
||||
@@ -42,10 +45,18 @@
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="platform_account_id" invisible="platform_account_id == False"/>
|
||||
<field name="platform_account_id"/>
|
||||
<field name="token_expiration" invisible="access_token == False"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Developer / Manuel Test Modu" groups="base.group_system">
|
||||
<div class="text-muted" colspan="2">
|
||||
Geliştirici sayfasından aldığınız Access Token (Erişim Anahtarı) ve Platform ID (Instagram Business ID vb.) değerlerini buraya manuel girerek OAuth sürecini atlayabilirsiniz.
|
||||
</div>
|
||||
<field name="access_token" password="True"/>
|
||||
<field name="refresh_token" password="True"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -51,20 +51,5 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action -->
|
||||
<record id="action_social_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Analiz Dashboard</field>
|
||||
<field name="res_model">mymach.social.post</field>
|
||||
<field name="view_mode">graph,pivot,kanban,list</field>
|
||||
<field name="context">{'search_default_state': 'published'}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Sosyal Medya Performansınızı Takip Edin!
|
||||
</p>
|
||||
<p>
|
||||
Gönderilerinizi yayınladıkça etkileşim grafikleriniz burada belirecektir.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
</div>
|
||||
<group>
|
||||
<group string="Kural Ayarları">
|
||||
<field name="keyword" placeholder="bilgi"/>
|
||||
<field name="reply_mode" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="keyword" placeholder="bilgi" invisible="reply_mode != 'keyword'" required="reply_mode == 'keyword'"/>
|
||||
<field name="target_sentiment" invisible="reply_mode != 'ai_sentiment'" required="reply_mode == 'ai_sentiment'"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
@@ -30,8 +32,21 @@
|
||||
<field name="account_ids" widget="many2many_tags" placeholder="Tüm hesaplar"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<div class="alert alert-warning mt-3 mb-3" role="alert">
|
||||
<h4 class="alert-heading"><i class="fa fa-star"/> Satış Fırsatı (CRM) Entegrasyonu</h4>
|
||||
<p>
|
||||
Müşterilerin <b>#fiyat, #sipariş, #satınalma</b> gibi satın alma niyeti içeren olumlu yorum/mesajları geldiğinde,
|
||||
sistemin otomatik olarak CRM modülünde yeni bir <b>Müşteri Adayı (Lead)</b> oluşturmasını sağlayabilirsiniz.
|
||||
</p>
|
||||
<hr/>
|
||||
<group>
|
||||
<field name="auto_create_lead" widget="boolean_toggle" string="Kural Tetiklendiğinde Otomatik CRM Fırsatı (Lead) Yarat"/>
|
||||
</group>
|
||||
</div>
|
||||
<group string="Yanıt İçeriği">
|
||||
<field name="reply_message" nolabel="1" widget="text" placeholder="Gelecek olan mesajda bu kelime geçiyorsa otomatik olarak şu metni gönder..."/>
|
||||
<field name="reply_message" nolabel="1" widget="text" placeholder="Gelecek olan mesajda bu kelime geçiyorsa otomatik olarak şu metni gönder..." invisible="reply_mode != 'keyword'" required="reply_mode == 'keyword'"/>
|
||||
<field name="ai_prompt_context" nolabel="1" widget="text" placeholder="Yapay zekanın nasıl davranması gerektiğini açıklayın. Örn: Müşteriden özür dile ve destek@sirket.com adresine yazmasını rica et." invisible="reply_mode != 'ai_sentiment'" required="reply_mode == 'ai_sentiment'"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@@ -46,7 +61,10 @@
|
||||
<list string="Otomatik Yanıt Kuralları">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="keyword"/>
|
||||
<field name="reply_mode"/>
|
||||
<field name="keyword" optional="show"/>
|
||||
<field name="target_sentiment" optional="show"/>
|
||||
<field name="auto_create_lead" widget="boolean_toggle"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- History List View -->
|
||||
<record id="view_social_competitor_history_list" model="ir.ui.view">
|
||||
<field name="name">social.competitor.history.list</field>
|
||||
<field name="model">social.competitor.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Geçmiş Metrikler">
|
||||
<field name="date"/>
|
||||
<field name="followers_count"/>
|
||||
<field name="engagement_rate"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_social_competitor_list" model="ir.ui.view">
|
||||
<field name="name">social.competitor.list</field>
|
||||
@@ -8,8 +21,10 @@
|
||||
<field name="arch" type="xml">
|
||||
<list string="Rakipler">
|
||||
<field name="name"/>
|
||||
<field name="username"/>
|
||||
<field name="platform"/>
|
||||
<field name="followers_count"/>
|
||||
<field name="growth_rate"/>
|
||||
<field name="engagement_rate"/>
|
||||
<field name="last_scraped"/>
|
||||
</list>
|
||||
@@ -23,28 +38,93 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Rakip Bilgisi">
|
||||
<header>
|
||||
<button name="action_scrape_data" string="Verileri Kazı (Güncelle)" type="object" class="oe_highlight"/>
|
||||
<button name="action_scrape_data" string="Yapay Zeka & Metrik Güncelle" type="object" class="oe_highlight"/>
|
||||
<button name="action_spy_ads" string="Reklam Casusunu Çalıştır (Ad Spy AI)" type="object" class="btn-danger"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Rakip Firma Adı"/>
|
||||
<field name="name" placeholder="Rakip Firma Adı (Örn: Haas Automation)"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="platform"/>
|
||||
<field name="profile_url" widget="url"/>
|
||||
<field name="username" placeholder="Kullanıcı Adı (Örn: haas_automation)"/>
|
||||
<field name="profile_url" widget="url" readonly="1"/>
|
||||
<field name="top_keywords"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="followers_count"/>
|
||||
<field name="growth_rate" widget="percentage"/>
|
||||
<field name="engagement_rate"/>
|
||||
<field name="last_scraped"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- AI Analysis Box -->
|
||||
<div class="alert alert-info mt-3" role="alert" invisible="not ai_summary">
|
||||
<h4 class="alert-heading"><i class="fa fa-magic"/> Gemini Strateji Analizi</h4>
|
||||
<hr/>
|
||||
<field name="ai_summary" widget="html" class="mb-0"/>
|
||||
</div>
|
||||
|
||||
<notebook>
|
||||
<page string="Analiz Notları">
|
||||
<field name="notes" placeholder="Stratejik notlar..."/>
|
||||
<page string="Yeni Takipçiler (Potansiyel Müşteriler)">
|
||||
<div class="alert alert-warning" role="alert" invisible="today_followed_count < daily_follow_limit">
|
||||
<i class="fa fa-exclamation-triangle"/> Günlük güvenli takip limitine (30) ulaştınız. Lütfen spam riskine karşı yarına kadar yeni istek atmayınız.
|
||||
</div>
|
||||
<group>
|
||||
<field name="daily_follow_limit" invisible="1"/>
|
||||
<field name="today_followed_count" widget="progressbar" options="{'max_value': 'daily_follow_limit'}" string="Bugün Kullanılan Takip Limiti"/>
|
||||
</group>
|
||||
<field name="follower_ids">
|
||||
<list>
|
||||
<field name="follower_name"/>
|
||||
<field name="follower_profile_url" widget="url"/>
|
||||
<field name="detected_date"/>
|
||||
<field name="follow_status" widget="badge"/>
|
||||
<button name="action_send_follow_request" string="Takip Et / İstek At" type="object" class="btn-sm btn-success" invisible="follow_status in ('following', 'request_sent')"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Aktif Reklamlar (Ad Spy)">
|
||||
<field name="ad_ids">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="detected_date"/>
|
||||
<field name="status" widget="badge" decoration-success="status == 'active'" decoration-muted="status == 'inactive'"/>
|
||||
</list>
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="status" widget="radio"/>
|
||||
<field name="detected_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="ad_content"/>
|
||||
</group>
|
||||
<div class="alert alert-danger" role="alert" invisible="not ai_strategy_suggestion">
|
||||
<h4><i class="fa fa-crosshairs"/> AI Karşı-Strateji Önerisi</h4>
|
||||
<hr/>
|
||||
<field name="ai_strategy_suggestion" widget="html"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Geçmiş Metrikler">
|
||||
<field name="history_ids" readonly="1">
|
||||
<list>
|
||||
<field name="date"/>
|
||||
<field name="followers_count"/>
|
||||
<field name="engagement_rate"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Manuel Notlar">
|
||||
<field name="notes" placeholder="Kendi stratejik notlarınız..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
@@ -61,21 +141,35 @@
|
||||
<field name="name"/>
|
||||
<field name="platform"/>
|
||||
<field name="followers_count"/>
|
||||
<field name="growth_rate"/>
|
||||
<field name="engagement_rate"/>
|
||||
<template>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_title">
|
||||
<div class="o_kanban_record_title d-flex justify-content-between align-items-center">
|
||||
<strong><field name="name"/></strong>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="badge rounded-pill text-bg-primary"><field name="platform"/></span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<i class="fa fa-users" title="Takipçiler"/> <field name="followers_count"/>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<div class="text-muted small">Takipçiler</div>
|
||||
<div class="fw-bold"><field name="followers_count"/></div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<i class="fa fa-line-chart" title="Etkileşim Oranı"/> <field name="engagement_rate"/> %
|
||||
<div class="col-6 text-end">
|
||||
<div class="text-muted small">Etkileşim</div>
|
||||
<div class="fw-bold"><field name="engagement_rate"/> %</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-center" t-if="record.growth_rate.raw_value > 0">
|
||||
<span class="text-success"><i class="fa fa-arrow-up"/> <field name="growth_rate"/> % Büyüme</span>
|
||||
</div>
|
||||
<div class="mt-2 text-center" t-if="record.growth_rate.raw_value < 0">
|
||||
<span class="text-danger"><i class="fa fa-arrow-down"/> <field name="growth_rate"/> % Küçülme</span>
|
||||
</div>
|
||||
<div class="mt-2 text-center" t-if="record.growth_rate.raw_value == 0">
|
||||
<span class="text-muted"><i class="fa fa-minus"/> Değişim Yok</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -96,7 +190,20 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<!-- History Graph View -->
|
||||
<record id="view_social_competitor_history_graph" model="ir.ui.view">
|
||||
<field name="name">social.competitor.history.graph</field>
|
||||
<field name="model">social.competitor.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Rakip Büyüme Eğrisi" type="line">
|
||||
<field name="date" type="row" interval="day"/>
|
||||
<field name="competitor_id" type="col"/>
|
||||
<field name="followers_count" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for Competitor -->
|
||||
<record id="action_social_competitor" model="ir.actions.act_window">
|
||||
<field name="name">Rakipler</field>
|
||||
<field name="res_model">social.competitor</field>
|
||||
@@ -111,11 +218,61 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for History Graph -->
|
||||
<record id="action_social_competitor_history_graph" model="ir.actions.act_window">
|
||||
<field name="name">Büyüme Grafikleri</field>
|
||||
<field name="res_model">social.competitor.history</field>
|
||||
<field name="view_mode">graph,list</field>
|
||||
</record>
|
||||
|
||||
<!-- Competitor Followers Views & Actions -->
|
||||
<record id="view_social_competitor_follower_list" model="ir.ui.view">
|
||||
<field name="name">social.competitor.follower.list</field>
|
||||
<field name="model">social.competitor.follower</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Rakip Takipçileri">
|
||||
<header>
|
||||
<button name="action_mass_send_follow_request" string="Seçilenleri Takip Et / İstek At" type="object" class="btn-primary"/>
|
||||
</header>
|
||||
<field name="competitor_id"/>
|
||||
<field name="platform"/>
|
||||
<field name="follower_name"/>
|
||||
<field name="follower_profile_url" widget="url"/>
|
||||
<field name="detected_date"/>
|
||||
<field name="follow_status" widget="badge" decoration-success="follow_status in ('following', 'request_sent')" decoration-danger="follow_status == 'failed'"/>
|
||||
<button name="action_send_follow_request" string="Takip Et / İstek At" type="object" class="btn-sm btn-success" invisible="follow_status in ('following', 'request_sent')"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_social_competitor_followers" model="ir.actions.act_window">
|
||||
<field name="name">Yakalanan Rakip Takipçileri</field>
|
||||
<field name="res_model">social.competitor.follower</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_social_competitors_root"
|
||||
name="Rakip Analizi"
|
||||
parent="mymach_social_nextgen.menu_social_root"
|
||||
sequence="35"/>
|
||||
|
||||
<menuitem id="menu_social_competitors"
|
||||
name="Rakipler"
|
||||
parent="mymach_social_nextgen.menu_social_root"
|
||||
parent="menu_social_competitors_root"
|
||||
action="action_social_competitor"
|
||||
sequence="35"/>
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem id="menu_social_competitor_followers"
|
||||
name="Yeni Rakip Takipçileri"
|
||||
parent="menu_social_competitors_root"
|
||||
action="action_social_competitor_followers"
|
||||
sequence="2"/>
|
||||
|
||||
<menuitem id="menu_social_competitor_history"
|
||||
name="Büyüme Analizi"
|
||||
parent="menu_social_competitors_root"
|
||||
action="action_social_competitor_history_graph"
|
||||
sequence="3"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="action_mymach_social_dashboard" model="ir.actions.client">
|
||||
<field name="name">Sosyal Medya Dashboard</field>
|
||||
<field name="tag">mymach_social_dashboard</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -6,23 +6,39 @@
|
||||
<field name="name">mymach.social.media.form</field>
|
||||
<field name="model">mymach.social.media</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Medya Kütüphanesi">
|
||||
<form string="Medya Detayı">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<div class="row">
|
||||
<!-- Left Column: Image Preview -->
|
||||
<div class="col-md-6 border-end pr-md-4">
|
||||
<field name="image_preview" widget="image" options="{'zoom': true}" class="img-fluid w-100 rounded" style="max-height: 480px; object-fit: contain;" invisible="media_type != 'image'"/>
|
||||
<div invisible="media_type == 'image'" class="alert alert-info text-center py-5" role="alert">
|
||||
<i class="fa fa-file-video-o fa-4x mb-3" title="Görsel Dışı Medya"/>
|
||||
<h4>Görsel Dışı Medya (Video / Belge)</h4>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column: Details & Associated Post Content -->
|
||||
<div class="col-md-6 pl-md-4">
|
||||
<div class="oe_title mb-4">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Medya Adı..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="media_type"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="post_id" readonly="1" invisible="not post_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="attachment_id"/>
|
||||
<field name="image_preview" widget="image" class="oe_avatar" invisible="media_type != 'image'"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook class="mt-4" invisible="not post_id">
|
||||
<page string="💬 Paylaşılan Gönderi İçeriği">
|
||||
<div class="p-3 bg-light rounded border">
|
||||
<field name="post_content" class="text-muted" style="font-size: 1.1em; line-height: 1.6;"/>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<menuitem id="menu_social_dashboard"
|
||||
name="Kontrol Paneli"
|
||||
parent="menu_social_root"
|
||||
action="action_social_dashboard_kanban"
|
||||
action="action_mymach_social_dashboard"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Calendar Action -->
|
||||
@@ -61,6 +61,13 @@
|
||||
action="action_social_stream"
|
||||
sequence="25"/>
|
||||
|
||||
<!-- Vertical Video Menu -->
|
||||
<menuitem id="menu_social_video_scripts"
|
||||
name="Dikey Video (Reels/TikTok)"
|
||||
parent="menu_social_root"
|
||||
action="action_social_video_script"
|
||||
sequence="28"/>
|
||||
|
||||
<!-- Campaigns Action -->
|
||||
<record id="action_social_campaign" model="ir.actions.act_window">
|
||||
<field name="name">Kampanyalar</field>
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,waiting,scheduled,published"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Yayınlandı" bg_color="bg-success" invisible="state != 'published'"/>
|
||||
<widget name="web_ribbon" title="Hata" bg_color="bg-danger" invisible="state != 'failed'"/>
|
||||
|
||||
<div class="alert alert-danger" role="alert" invisible="state != 'failed'">
|
||||
<field name="error_message" readonly="1" force_save="1"/>
|
||||
</div>
|
||||
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(action_social_live_post)d" type="action" class="oe_stat_button" icon="fa-share-square-o" invisible="state not in ('published', 'failed')">
|
||||
<field name="live_post_ids" widget="statinfo" string="Canlı Gönderiler"/>
|
||||
@@ -54,9 +61,6 @@
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Hata Mesajı" invisible="state != 'failed'">
|
||||
<field name="error_message"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
|
||||
@@ -14,10 +14,18 @@
|
||||
<field name="comments_count"/>
|
||||
<field name="shares_count"/>
|
||||
<field name="stream_id"/>
|
||||
<field name="lead_id"/>
|
||||
<field name="post_id"/>
|
||||
<field name="stream_type"/>
|
||||
<field name="sentiment"/>
|
||||
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div t-attf-class="oe_kanban_global_click o_kanban_record">
|
||||
<!-- Poaching Highlight -->
|
||||
<div t-if="record.stream_type.raw_value == 'competitor_watch' and record.sentiment.raw_value == 'negative'" class="alert alert-danger p-1 mb-2 text-center" style="font-size: 0.8rem; font-weight: bold; border-radius: 4px;">
|
||||
🚨 RAKİP MÜŞTERİSİ (Kancaya Takıldı)
|
||||
</div>
|
||||
<!-- Author & Time Info -->
|
||||
<div class="o_social_author_section">
|
||||
<div class="o_social_author_avatar d-flex align-items-center justify-content-center text-white">
|
||||
@@ -29,13 +37,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Associated Post Link (Görsel Entegrasyon) -->
|
||||
<div class="o_social_associated_post mt-1 mb-2 p-1 px-2 rounded d-flex align-items-center" t-if="record.post_id.raw_value" style="font-size: 0.8rem; background-color: #f1f3f5; border-left: 3px solid #00A09D;">
|
||||
<i class="fa fa-link text-muted me-1" style="font-size: 0.75rem;"/>
|
||||
<span class="text-muted me-1">Bağlı Gönderi:</span>
|
||||
<span class="fw-bold text-truncate" style="max-width: 180px;"><field name="post_id"/></span>
|
||||
</div>
|
||||
|
||||
<!-- Post Content -->
|
||||
<div class="o_social_message_text">
|
||||
<field name="message"/>
|
||||
</div>
|
||||
|
||||
<!-- Actions / Metrics -->
|
||||
<div class="o_social_actions">
|
||||
<div class="o_social_actions d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-3">
|
||||
<div title="Beğeniler">
|
||||
<i class="fa fa-heart-o me-1"/> <field name="likes_count"/>
|
||||
</div>
|
||||
@@ -46,6 +62,20 @@
|
||||
<i class="fa fa-share-square-o me-1"/> <field name="shares_count"/>
|
||||
</div>
|
||||
</div>
|
||||
<button name="unlink" type="object" class="btn btn-sm text-danger p-0" title="Sil" confirm="Bu akış kaydını kontrol panelinden kalıcı olarak silmek istediğinize emin misiniz?">
|
||||
<i class="fa fa-trash-o"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CRM Actions -->
|
||||
<div class="o_social_crm_actions mt-3">
|
||||
<button name="action_create_lead" type="object" class="btn btn-sm btn-outline-primary w-100" t-if="!record.lead_id.raw_value">
|
||||
<i class="fa fa-briefcase"/> Fırsat Oluştur
|
||||
</button>
|
||||
<button name="action_create_lead" type="object" class="btn btn-sm btn-success w-100" t-if="record.lead_id.raw_value">
|
||||
<i class="fa fa-check-circle"/> Fırsatı Görüntüle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -53,12 +83,36 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View with Tab/Tag Panel -->
|
||||
<record id="view_social_stream_post_search" model="ir.ui.view">
|
||||
<field name="name">mymach.social.stream.post.search</field>
|
||||
<field name="model">mymach.social.stream.post</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Sosyal Akış">
|
||||
<field name="author_name"/>
|
||||
<field name="message"/>
|
||||
<filter string="Gönderiler" name="posts" domain="[('message_type', '=', 'post')]"/>
|
||||
<filter string="Yorumlar" name="comments" domain="[('message_type', '=', 'comment')]"/>
|
||||
<filter string="Özel Mesajlar" name="dms" domain="[('message_type', '=', 'direct_message')]"/>
|
||||
<separator/>
|
||||
<filter string="Hesaba Göre Grupla" name="group_by_account" context="{'group_by':'account_id'}"/>
|
||||
<filter string="Mesaj Tipine Göre Grupla" name="group_by_type" context="{'group_by':'message_type'}"/>
|
||||
<searchpanel>
|
||||
<field name="message_type" string="Etiket (Panel)" select="one" icon="fa-tags" color="#00A09D"/>
|
||||
<field name="sentiment" string="Duygu Analizi" select="one" icon="fa-smile-o"/>
|
||||
</searchpanel>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action -->
|
||||
<record id="action_social_dashboard_kanban" model="ir.actions.act_window">
|
||||
<field name="name">Kontrol Paneli</field>
|
||||
<field name="res_model">mymach.social.stream.post</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="view_id" ref="view_social_stream_post_kanban_dashboard"/>
|
||||
<field name="search_view_id" ref="view_social_stream_post_search"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Harika Sosyal Medya Kontrol Panelinize Hoşgeldiniz!
|
||||
|
||||
@@ -136,10 +136,7 @@
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
185
extra-addons/mymach_social_nextgen/views/social_trend_views.xml
Normal file
185
extra-addons/mymach_social_nextgen/views/social_trend_views.xml
Normal file
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Trend Tree View -->
|
||||
<record id="view_social_trend_list" model="ir.ui.view">
|
||||
<field name="name">mymach.social.trend.list</field>
|
||||
<field name="model">mymach.social.trend</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Trend Korsanlığı Hedefleri" decoration-info="state == 'new'" decoration-success="state == 'selected'" decoration-muted="state == 'ignored'">
|
||||
<field name="name"/>
|
||||
<field name="source" widget="badge" decoration-info="source == 'google'" decoration-warning="source == 'twitter'" decoration-success="source == 'linkedin'" decoration-danger="source == 'industry'"/>
|
||||
<field name="description"/>
|
||||
<field name="state" widget="badge" decoration-info="state == 'new'" decoration-success="state == 'selected'" decoration-danger="state == 'ignored'"/>
|
||||
<field name="write_date" string="Son Güncelleme"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Trend Form View -->
|
||||
<record id="view_social_trend_form" model="ir.ui.view">
|
||||
<field name="name">mymach.social.trend.form</field>
|
||||
<field name="model">mymach.social.trend</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Trend Detayları">
|
||||
<header>
|
||||
<button name="action_generate_ideas" type="object" string="⚡ Trend Korsanlığı Fikirleri Üret (AI)" class="btn-danger" invisible="state != 'new'"/>
|
||||
<button name="action_create_manual_post" type="object" string="Diğer (Manuel Gönderi Hazırla)" invisible="state != 'new'"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="new,selected,ignored"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Trend Topic Başlığı"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Örn: CNC Makinelerinde Yapay Zeka Entegrasyonu"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="source" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description" placeholder="Bu trendin sosyal medyadaki yansımaları veya kısa özeti..."/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<separator string="⚡ Korsanlık (Hijack) Fikirleri" class="mt-4 mb-3"/>
|
||||
|
||||
<field name="post_idea_ids" mode="kanban" readonly="1">
|
||||
<kanban create="0" delete="0" edit="0">
|
||||
<field name="name"/>
|
||||
<field name="content"/>
|
||||
<field name="image_binary"/>
|
||||
<field name="image_prompt"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_global_click p-3 d-flex flex-column justify-content-between" style="min-height: 380px; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
|
||||
<div>
|
||||
<!-- Visual Header -->
|
||||
<div class="o_kanban_image_wrapper mb-3" style="width: 100%; height: 180px; background-color: #f8f9fa; border-radius: 6px; overflow: hidden; display: flex; align-items: center; justify-content: center;">
|
||||
<t t-if="record.image_binary.raw_value">
|
||||
<field name="image_binary" widget="image" class="w-100 h-100" style="object-fit: cover;"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="text-muted d-flex flex-column align-items-center">
|
||||
<i class="fa fa-image fa-3x mb-2"/>
|
||||
<span>Görsel Hazırlanıyor...</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h4 class="mb-2 text-primary" style="font-size: 1.1rem; font-weight: bold;">
|
||||
<field name="name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Caption/Content -->
|
||||
<p class="text-muted" style="font-size: 0.9rem; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;">
|
||||
<field name="content"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<div class="mt-3">
|
||||
<button name="action_select_idea" type="object" class="btn btn-danger w-100" style="border-radius: 4px; font-weight: bold;">
|
||||
<i class="fa fa-bolt me-1"/> Bu Trendi Hijack Et
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Trend Kanban View -->
|
||||
<record id="view_social_trend_kanban" model="ir.ui.view">
|
||||
<field name="name">mymach.social.trend.kanban</field>
|
||||
<field name="model">mymach.social.trend</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_dashboard" create="1">
|
||||
<field name="name"/>
|
||||
<field name="source"/>
|
||||
<field name="description"/>
|
||||
<field name="state"/>
|
||||
<template>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_global_click p-3 d-flex flex-column justify-content-between" style="min-height: 140px; border-left: 4px solid #00A09D;">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="badge rounded-pill text-bg-info"><field name="source"/></span>
|
||||
<t t-if="record.state.raw_value == 'new'">
|
||||
<span class="badge text-bg-danger">YENİ BİLDİRİM</span>
|
||||
</t>
|
||||
<t t-if="record.state.raw_value == 'selected'">
|
||||
<span class="badge text-bg-success">PAYLAŞILDI</span>
|
||||
</t>
|
||||
<t t-if="record.state.raw_value == 'ignored'">
|
||||
<span class="badge text-bg-secondary">DİĞER</span>
|
||||
</t>
|
||||
</div>
|
||||
<h3 class="mt-2" style="font-size: 1.2rem; font-weight: bold; color: #2c3e50;">
|
||||
<field name="name"/>
|
||||
</h3>
|
||||
<p class="text-muted mt-1" style="font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<field name="description"/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-end text-primary" style="font-size: 0.85rem; font-weight: 500;">
|
||||
Fikirleri İncele <i class="fa fa-arrow-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_social_trend" model="ir.actions.act_window">
|
||||
<field name="name">Trend Korsanlığı (Viral Hijacking)</field>
|
||||
<field name="res_model">mymach.social.trend</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Yapay Zeka ile Sosyal Medya Trendlerini Yakalayın!
|
||||
</p>
|
||||
<p>
|
||||
Sektörünüzle ve dünya gündemiyle alakalı popüler konuları otomatik tespit edin ve bu trendlere kendi şirketinizi sızdırın (Hijack).
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Server Action to Fetch/Update Trends -->
|
||||
<record id="action_server_fetch_trends" model="ir.actions.server">
|
||||
<field name="name">Trendleri Güncelle (Korsanlık)</field>
|
||||
<field name="model_id" ref="mymach_social_nextgen.model_mymach_social_trend"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = model.action_fetch_trends()
|
||||
</field>
|
||||
<field name="binding_model_id" ref="mymach_social_nextgen.model_mymach_social_trend"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron for Trend Hijacking (Daily) -->
|
||||
<record id="cron_fetch_social_trends" model="ir.cron">
|
||||
<field name="name">Sosyal Medya Trend Korsanlığı - Günlük Tarama</field>
|
||||
<field name="model_id" ref="mymach_social_nextgen.model_mymach_social_trend"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.action_fetch_trends()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_social_trends"
|
||||
name="Trend Korsanlığı"
|
||||
parent="mymach_social_nextgen.menu_social_root"
|
||||
action="action_social_trend"
|
||||
sequence="36"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- List View -->
|
||||
<record id="view_social_video_script_list" model="ir.ui.view">
|
||||
<field name="name">mymach.social.video.script.list</field>
|
||||
<field name="model">mymach.social.video.script</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Video Senaryoları">
|
||||
<field name="name"/>
|
||||
<field name="platform"/>
|
||||
<field name="duration"/>
|
||||
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'generated'" decoration-warning="state == 'in_production'" decoration-primary="state == 'done'"/>
|
||||
<field name="create_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_social_video_script_form" model="ir.ui.view">
|
||||
<field name="name">mymach.social.video.script.form</field>
|
||||
<field name="model">mymach.social.video.script</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Video Senaryosu">
|
||||
<header>
|
||||
<button name="action_generate_script" string="Yapay Zeka ile Senaryo Üret" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||
<button name="action_generate_script" string="Senaryoyu Yeniden Üret" type="object" invisible="state == 'draft'"/>
|
||||
<button name="action_generate_voiceover" string="Dış Sesi (Voiceover) Hazırla/Dinle" type="object" class="btn-info" invisible="state == 'draft'"/>
|
||||
|
||||
<button name="action_render_video" string="Videoyu Oluştur (Render)" type="object" class="btn-warning" invisible="state == 'draft' or render_status in ('processing', 'done')"/>
|
||||
|
||||
<button name="action_set_in_production" string="Çekime Başla" type="object" class="btn-success" invisible="state != 'generated'"/>
|
||||
<button name="action_set_done" string="Tamamlandı" type="object" class="btn-primary" invisible="state != 'in_production'"/>
|
||||
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,generated,in_production,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Senaryo Başlığı Örn: CNC Makine Tanıtımı"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="platform"/>
|
||||
<field name="duration"/>
|
||||
<field name="ai_provider"/>
|
||||
<field name="voice_type"/>
|
||||
<field name="language"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="render_status" readonly="1" decoration-info="render_status == 'processing'" decoration-success="render_status == 'done'" widget="badge"/>
|
||||
<field name="create_date" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<field name="topic" placeholder="Videonun konusunu veya anlatmak istediğiniz ana fikri buraya yazın... Gemini bu konuyu senaryoya dönüştürecektir."/>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Video Önizleme" invisible="render_status != 'done'">
|
||||
<group>
|
||||
<field name="video_url" widget="url" readonly="1" text="Videoyu İzlemek İçin Tıklayın"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Storyboard (Sahneler)" invisible="state == 'draft'">
|
||||
<div class="alert alert-success" role="alert" invisible="not music_suggestion">
|
||||
<i class="fa fa-music"/> <b>Müzik Önerisi:</b> <field name="music_suggestion"/>
|
||||
</div>
|
||||
<div class="alert alert-info" role="alert" invisible="not caption_suggestion">
|
||||
<i class="fa fa-align-left"/> <b>Açıklama (Caption):</b> <field name="caption_suggestion"/>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<field name="audio_player_html" widget="html" nolabel="1" readonly="1"/>
|
||||
</group>
|
||||
|
||||
<field name="scene_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="time_range" style="width: 100px; font-weight: bold;"/>
|
||||
<field name="visual_action"/>
|
||||
<field name="voiceover"/>
|
||||
<field name="overlay_text"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<!-- Chatter -->
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Kanban View -->
|
||||
<record id="view_social_video_script_kanban" model="ir.ui.view">
|
||||
<field name="name">mymach.social.video.script.kanban</field>
|
||||
<field name="model">mymach.social.video.script</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_dashboard" default_group_by="state">
|
||||
<field name="name"/>
|
||||
<field name="platform"/>
|
||||
<field name="duration"/>
|
||||
<field name="state"/>
|
||||
<template>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_title d-flex justify-content-between align-items-center">
|
||||
<strong><field name="name"/></strong>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom mt-2">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<span class="badge rounded-pill text-bg-info"><field name="platform"/></span>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right text-muted">
|
||||
<i class="fa fa-clock-o" title="Süre"/> <field name="duration"/> Saniye
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_social_video_script" model="ir.actions.act_window">
|
||||
<field name="name">Dikey Video Senaryoları</field>
|
||||
<field name="res_model">mymach.social.video.script</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
İlk Video Fikrinizi Oluşturun!
|
||||
</p>
|
||||
<p>
|
||||
Reels, TikTok veya Shorts için bir konu girin, Yapay Zeka saniye saniye storyboard ve senaryo üretsin.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -16,7 +16,8 @@ class SocialAIReplyWizard(models.TransientModel):
|
||||
('gemini', 'Google Gemini'),
|
||||
('openai', 'OpenAI (ChatGPT)'),
|
||||
('claude', 'Anthropic (Claude)'),
|
||||
('grok', 'xAI (Grok)')
|
||||
('grok', 'xAI (Grok)'),
|
||||
('groq', 'Groq (Llama 3)')
|
||||
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
|
||||
|
||||
def action_generate_reply(self):
|
||||
@@ -26,83 +27,12 @@ class SocialAIReplyWizard(models.TransientModel):
|
||||
user_prompt = f"Müşteri Mesajı: {self.customer_message}\n\nLütfen bu mesaja profesyonel bir yanıt üret."
|
||||
|
||||
try:
|
||||
import requests
|
||||
import json
|
||||
|
||||
generated_text = ""
|
||||
|
||||
if self.ai_provider == 'gemini':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
|
||||
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
payload = {
|
||||
"system_instruction": {"parts": [{"text": system_instruction}]},
|
||||
"contents": [{"parts": [{"text": user_prompt}]}],
|
||||
"generationConfig": {"temperature": 0.5, "maxOutputTokens": 200}
|
||||
}
|
||||
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
|
||||
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
|
||||
|
||||
elif self.ai_provider == 'openai':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||
payload = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_instruction},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 200
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
|
||||
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
|
||||
elif self.ai_provider == 'claude':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
|
||||
if not api_key: raise UserError("Claude API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.anthropic.com/v1/messages"
|
||||
headers = {
|
||||
'x-api-key': api_key,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"model": "claude-3-5-sonnet-20240620",
|
||||
"system": system_instruction,
|
||||
"messages": [{"role": "user", "content": user_prompt}],
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.5
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
|
||||
generated_text = response.json().get('content', [{}])[0].get('text', '')
|
||||
|
||||
elif self.ai_provider == 'grok':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
|
||||
if not api_key: raise UserError("Grok API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.x.ai/v1/chat/completions"
|
||||
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||
payload = {
|
||||
"model": "grok-2",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_instruction},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 200
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
|
||||
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
generated_text = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=user_prompt,
|
||||
system_prompt=system_instruction,
|
||||
response_format='text',
|
||||
provider=self.ai_provider
|
||||
)
|
||||
|
||||
if not generated_text:
|
||||
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
|
||||
|
||||
@@ -29,9 +29,24 @@ class SocialAIWizard(models.TransientModel):
|
||||
('openai', 'OpenAI (ChatGPT)'),
|
||||
('claude', 'Anthropic (Claude)'),
|
||||
('grok', 'xAI (Grok)'),
|
||||
('groq', 'Groq (Llama 3)'),
|
||||
('none', 'Hiçbiri (Sadece Görsel Üret veya Manuel)')
|
||||
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
|
||||
|
||||
# New Fields for 3-Image Flow
|
||||
state = fields.Selection([('draft', 'İçerik Üret'), ('select_image', 'Görsel Seçimi')], default='draft')
|
||||
generated_text_temp = fields.Text(string='Geçici Üretilen Metin')
|
||||
|
||||
image_1 = fields.Binary(string='Görsel 1')
|
||||
image_2 = fields.Binary(string='Görsel 2')
|
||||
image_3 = fields.Binary(string='Görsel 3')
|
||||
|
||||
selected_image = fields.Selection([
|
||||
('1', '1. Görseli Seç'),
|
||||
('2', '2. Görseli Seç'),
|
||||
('3', '3. Görseli Seç'),
|
||||
], string='Kullanılacak Görsel')
|
||||
|
||||
def action_generate_content(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -46,122 +61,91 @@ class SocialAIWizard(models.TransientModel):
|
||||
user_prompt = f"Konu: {self.prompt}\n\nLütfen bu konuya göre sosyal medya içeriğini üret."
|
||||
|
||||
try:
|
||||
import requests
|
||||
import json
|
||||
|
||||
if self.ai_provider == 'gemini':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
|
||||
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
payload = {
|
||||
"system_instruction": {"parts": [{"text": system_instruction}]},
|
||||
"contents": [{"parts": [{"text": user_prompt}]}],
|
||||
"generationConfig": {"temperature": 0.7, "maxOutputTokens": 400}
|
||||
}
|
||||
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
|
||||
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
|
||||
|
||||
elif self.ai_provider == 'openai':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||
payload = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_instruction},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 400
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
|
||||
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
|
||||
elif self.ai_provider == 'claude':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
|
||||
if not api_key: raise UserError("Claude API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.anthropic.com/v1/messages"
|
||||
headers = {
|
||||
'x-api-key': api_key,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
payload = {
|
||||
"model": "claude-3-5-sonnet-20240620",
|
||||
"system": system_instruction,
|
||||
"messages": [{"role": "user", "content": user_prompt}],
|
||||
"max_tokens": 400,
|
||||
"temperature": 0.7
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
|
||||
generated_text = response.json().get('content', [{}])[0].get('text', '')
|
||||
|
||||
elif self.ai_provider == 'grok':
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
|
||||
if not api_key: raise UserError("Grok API Anahtarı eksik!")
|
||||
|
||||
url = "https://api.x.ai/v1/chat/completions"
|
||||
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||
payload = {
|
||||
"model": "grok-2",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_instruction},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 400
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
|
||||
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
generated_text = self.env['mymach.social.ai.utils'].generate_text(
|
||||
prompt=user_prompt,
|
||||
system_prompt=system_instruction,
|
||||
response_format='text',
|
||||
provider=self.ai_provider
|
||||
)
|
||||
|
||||
if not generated_text:
|
||||
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
|
||||
generated_text = generated_text.strip()
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"AI API Error: {str(e)}")
|
||||
raise UserError(f"AI ile iletişim kurulamadı: {str(e)}")
|
||||
|
||||
# --- 2. Generate Image (Free Stable Diffusion via Pollinations) ---
|
||||
media_record = False
|
||||
if self.generate_image:
|
||||
import urllib.parse
|
||||
import base64
|
||||
import requests
|
||||
import random
|
||||
|
||||
img_prompt = self.image_prompt if self.image_prompt else self.prompt
|
||||
encoded_prompt = urllib.parse.quote(img_prompt)
|
||||
|
||||
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
|
||||
|
||||
images_data = []
|
||||
try:
|
||||
for i in range(3):
|
||||
seed = random.randint(1, 999999)
|
||||
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true&seed={seed}"
|
||||
img_data = requests.get(pollinations_url, timeout=30).content
|
||||
encoded_image = base64.b64encode(img_data)
|
||||
images_data.append(base64.b64encode(img_data))
|
||||
|
||||
media_record = self.env['ir.attachment'].create({
|
||||
'name': f"AI_Generated_Image.png",
|
||||
'datas': encoded_image,
|
||||
'res_model': 'mymach.social.post',
|
||||
'res_id': self.post_id.id,
|
||||
'type': 'binary',
|
||||
self.write({
|
||||
'image_1': images_data[0],
|
||||
'image_2': images_data[1],
|
||||
'image_3': images_data[2],
|
||||
'generated_text_temp': generated_text,
|
||||
'state': 'select_image'
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Görsel Seçimi',
|
||||
'res_model': 'mymach.social.ai.wizard',
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
except Exception as img_e:
|
||||
_logger.error(f"Stable Diffusion Image Error: {str(img_e)}")
|
||||
raise UserError(f"Ücretsiz görsel üretilirken hata oluştu: {str(img_e)}")
|
||||
|
||||
# --- 3. Update the post ---
|
||||
# --- 3. Update the post directly if no image needed ---
|
||||
update_vals = {}
|
||||
if generated_text:
|
||||
update_vals['content'] = generated_text
|
||||
if media_record:
|
||||
|
||||
if update_vals:
|
||||
self.post_id.write(update_vals)
|
||||
|
||||
def action_confirm_image_selection(self):
|
||||
self.ensure_one()
|
||||
if not self.selected_image:
|
||||
raise UserError("Lütfen kullanmak istediğiniz görseli seçin!")
|
||||
|
||||
update_vals = {}
|
||||
if self.generated_text_temp:
|
||||
update_vals['content'] = self.generated_text_temp
|
||||
|
||||
chosen_image_data = False
|
||||
if self.selected_image == '1':
|
||||
chosen_image_data = self.image_1
|
||||
elif self.selected_image == '2':
|
||||
chosen_image_data = self.image_2
|
||||
elif self.selected_image == '3':
|
||||
chosen_image_data = self.image_3
|
||||
|
||||
if chosen_image_data:
|
||||
media_record = self.env['ir.attachment'].create({
|
||||
'name': "AI_Generated_Image.png",
|
||||
'datas': chosen_image_data,
|
||||
'res_model': 'mymach.social.post',
|
||||
'res_id': self.post_id.id,
|
||||
'type': 'binary',
|
||||
})
|
||||
update_vals['media_ids'] = [(4, media_record.id)]
|
||||
|
||||
if update_vals:
|
||||
|
||||
@@ -5,22 +5,46 @@
|
||||
<field name="model">mymach.social.ai.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Yapay Zeka İçerik Üreticisi">
|
||||
<group>
|
||||
<field name="state" invisible="1"/>
|
||||
|
||||
<!-- STAGE 1: DRAFT (İçerik Üret) -->
|
||||
<group invisible="state != 'draft'">
|
||||
<field name="post_id" invisible="1"/>
|
||||
<field name="ai_provider"/>
|
||||
<field name="prompt" invisible="ai_provider == 'none'"/>
|
||||
<field name="tone" invisible="ai_provider == 'none'"/>
|
||||
<field name="include_hashtags" invisible="ai_provider == 'none'"/>
|
||||
</group>
|
||||
<group string="Görsel Üretimi (Ücretsiz Yapay Zeka)">
|
||||
<group string="Görsel Üretimi (Ücretsiz Yapay Zeka)" invisible="state != 'draft'">
|
||||
<field name="generate_image" widget="boolean_toggle"/>
|
||||
<field name="image_prompt" invisible="generate_image == False"/>
|
||||
<div class="alert alert-info" role="alert" invisible="not generate_image">
|
||||
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez!
|
||||
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez! Sizin için 3 farklı varyasyon üretilecek ve seçmeniz istenecektir.
|
||||
</div>
|
||||
</group>
|
||||
|
||||
<!-- STAGE 2: SELECT IMAGE -->
|
||||
<group string="Lütfen Kullanılacak Görseli Seçin" invisible="state != 'select_image'">
|
||||
<group col="3">
|
||||
<div class="o_social_image_container text-center">
|
||||
<field name="image_1" widget="image" options="{'size': [200, 200]}"/>
|
||||
<p><b>Görsel 1</b></p>
|
||||
</div>
|
||||
<div class="o_social_image_container text-center">
|
||||
<field name="image_2" widget="image" options="{'size': [200, 200]}"/>
|
||||
<p><b>Görsel 2</b></p>
|
||||
</div>
|
||||
<div class="o_social_image_container text-center">
|
||||
<field name="image_3" widget="image" options="{'size': [200, 200]}"/>
|
||||
<p><b>Görsel 3</b></p>
|
||||
</div>
|
||||
</group>
|
||||
<field name="selected_image" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
|
||||
<footer>
|
||||
<button name="action_generate_content" string="Üret (Generate)" type="object" class="btn-primary"/>
|
||||
<button name="action_generate_content" string="Üret (Generate)" type="object" class="btn-primary" invisible="state != 'draft'"/>
|
||||
<button name="action_confirm_image_selection" string="Görseli Onayla ve Ekle" type="object" class="btn-success" invisible="state != 'select_image'"/>
|
||||
<button string="İptal" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user