GPS dan Business Category

This commit is contained in:
2026-03-05 20:49:20 +07:00
parent d0ee49ed5b
commit 24bdbf7d60
40 changed files with 20029 additions and 32 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"program": "C:/odoo14c/server/odoo-bin",
"args": [
"--config=C:\\addon14\\odoo.conf",
// "--database=manukanjabung",
"--database=kanjabung_MRP",
"--without-demo=all"
],
"cwd": "${workspaceFolder}",
+1 -1
View File
@@ -1,2 +1,2 @@
from . import models
from . import controllers
+6 -1
View File
@@ -9,14 +9,19 @@ This enables different staging flow per business category through team pipelines
"author": "PT Gagak Rimang Teknologi",
"website": "https://rimang.id",
"category": "Sales/CRM",
"version": "14.0.1.5.0",
"version": "14.0.2.10.1",
"depends": ["crm"],
"data": [
"security/ir.model.access.csv",
"security/ir.rule.csv",
"data/crm_business_category_data.xml",
"views/assets.xml",
"views/crm_activity_history_views.xml",
"views/crm_business_category_views.xml",
"views/crm_lead_views.xml",
"views/crm_team_views.xml",
"views/crm_team_business_category_views.xml",
"views/mail_activity_views.xml",
"views/res_users_business_category_views.xml",
],
"installable": True,
@@ -0,0 +1 @@
from . import main
@@ -0,0 +1,20 @@
from odoo import fields, http
from odoo.http import request
class GrtCrmBusinessCategoryController(http.Controller):
@http.route("/grt_crm_business_category/gps/ping", type="json", auth="user")
def gps_ping(self, latitude=None, longitude=None, client_time=None, client_tz=None):
if latitude is None or longitude is None:
return {"ok": False}
user = request.env.user.sudo()
user.write(
{
"last_gps_latitude": latitude,
"last_gps_longitude": longitude,
"last_gps_client_time": client_time or False,
"last_gps_client_tz": client_tz or False,
"last_gps_recorded_at": fields.Datetime.now(),
}
)
return {"ok": True}
@@ -3,3 +3,5 @@ from . import business_category_mixin
from . import crm_team
from . import crm_lead
from . import res_users
from . import mail_activity
from . import crm_activity_history
@@ -0,0 +1,61 @@
from odoo import fields, models
class CrmActivityHistory(models.Model):
_name = "crm.activity.history"
_description = "CRM Activity History"
_order = "scheduled_at desc, id desc"
name = fields.Char(string="Activity", required=True)
activity_id = fields.Many2one("mail.activity", string="Activity Record", ondelete="set null")
lead_id = fields.Many2one("crm.lead", string="Lead/Opportunity", required=True, ondelete="cascade", index=True)
business_category_id = fields.Many2one(
"crm.business.category",
string="Business Category",
related="lead_id.business_category_id",
store=True,
readonly=True,
)
team_id = fields.Many2one(
"crm.team",
string="Sales Team",
related="lead_id.team_id",
store=True,
readonly=True,
)
company_id = fields.Many2one(
"res.company",
string="Company",
related="lead_id.company_id",
store=True,
readonly=True,
)
activity_type_id = fields.Many2one("mail.activity.type", string="Activity Type", readonly=True)
assigned_user_id = fields.Many2one("res.users", string="Assigned To", readonly=True)
created_by_id = fields.Many2one("res.users", string="Created By", readonly=True)
summary = fields.Char(string="Summary", readonly=True)
note = fields.Html(string="Scheduled Note", readonly=True)
date_deadline = fields.Date(string="Deadline", readonly=True)
scheduled_at = fields.Datetime(string="Scheduled At", readonly=True)
state = fields.Selection(
[("scheduled", "Scheduled"), ("done", "Done")],
string="Status",
default="scheduled",
required=True,
index=True,
readonly=True,
)
done_at = fields.Datetime(string="Done At", readonly=True)
feedback = fields.Text(string="Done Feedback", readonly=True)
schedule_gps_latitude = fields.Float(string="Schedule GPS Latitude", digits=(16, 6), readonly=True)
schedule_gps_longitude = fields.Float(string="Schedule GPS Longitude", digits=(16, 6), readonly=True)
schedule_gps_url = fields.Char(string="Schedule OpenStreetMap", readonly=True)
schedule_client_time = fields.Char(string="Schedule Local Time", readonly=True)
schedule_client_tz = fields.Char(string="Schedule Local TZ", readonly=True)
done_gps_latitude = fields.Float(string="Done GPS Latitude", digits=(16, 6), readonly=True)
done_gps_longitude = fields.Float(string="Done GPS Longitude", digits=(16, 6), readonly=True)
done_gps_url = fields.Char(string="Done OpenStreetMap", readonly=True)
done_client_time = fields.Char(string="Done Local Time", readonly=True)
done_client_tz = fields.Char(string="Done Local TZ", readonly=True)
@@ -54,6 +54,25 @@ class CrmLead(models.Model):
if lead.team_id and lead.team_id.business_category_id:
lead.business_category_id = lead.team_id.business_category_id
@api.model_create_multi
def create(self, vals_list):
team_model = self.env["crm.team"]
for vals in vals_list:
team_id = vals.get("team_id")
if team_id and not vals.get("business_category_id"):
team = team_model.browse(team_id)
if team.business_category_id:
vals["business_category_id"] = team.business_category_id.id
return super().create(vals_list)
def write(self, vals):
team_model = self.env["crm.team"]
if vals.get("team_id") and not vals.get("business_category_id"):
team = team_model.browse(vals["team_id"])
if team.business_category_id:
vals["business_category_id"] = team.business_category_id.id
return super().write(vals)
@api.constrains("business_category_id", "team_id")
def _check_business_category_team(self):
for lead in self:
@@ -75,5 +94,11 @@ class CrmLead(models.Model):
@api.constrains("type", "business_category_id")
def _check_business_category_required(self):
for lead in self:
if lead.type not in ("lead", "opportunity"):
continue
if lead.business_category_id:
continue
if lead.team_id and lead.team_id.business_category_id:
continue
if lead.type in ("lead", "opportunity") and not lead.business_category_id:
raise ValidationError(_("Business Category is required for both Leads and Opportunities."))
@@ -0,0 +1,222 @@
from odoo import _, api, fields, models
from datetime import timedelta
class MailActivity(models.Model):
_inherit = "mail.activity"
gps_captured = fields.Boolean(string="GPS Captured")
gps_latitude = fields.Float(string="GPS Latitude", digits=(9, 6))
gps_longitude = fields.Float(string="GPS Longitude", digits=(9, 6))
gps_client_time = fields.Char(string="GPS Client Time")
gps_client_tz = fields.Char(string="GPS Client TZ")
gps_openstreetmap_url = fields.Char(string="OpenStreetMap URL")
@staticmethod
def _has_valid_gps(lat, lon):
if lat is None or lon is None:
return False
try:
lat_f = float(lat)
lon_f = float(lon)
except (TypeError, ValueError):
return False
if abs(lat_f) < 1e-9 and abs(lon_f) < 1e-9:
return False
return -90.0 <= lat_f <= 90.0 and -180.0 <= lon_f <= 180.0
@staticmethod
def _build_osm_url(lat, lon):
return "https://www.openstreetmap.org/?mlat=%s&mlon=%s#map=18/%s/%s" % (lat, lon, lat, lon)
@api.onchange("gps_latitude", "gps_longitude", "gps_captured")
def _onchange_gps_openstreetmap_url(self):
for activity in self:
if activity._has_valid_gps(activity.gps_latitude, activity.gps_longitude):
activity.gps_openstreetmap_url = activity._build_osm_url(activity.gps_latitude, activity.gps_longitude)
activity.gps_captured = True
else:
activity.gps_openstreetmap_url = False
@api.model_create_multi
def create(self, vals_list):
ctx = self.env.context
ctx_gps_captured = bool(ctx.get("gps_captured"))
ctx_lat = ctx.get("gps_latitude")
ctx_lon = ctx.get("gps_longitude")
ctx_client_time = ctx.get("gps_client_time")
ctx_client_tz = ctx.get("gps_client_tz")
for vals in vals_list:
if vals.get("res_model") == "crm.lead" and not vals.get("user_id"):
vals["user_id"] = self.env.user.id
# Fallback: if popup payload keeps default zeros, use GPS from context.
if ctx_gps_captured:
current_lat = vals.get("gps_latitude")
current_lon = vals.get("gps_longitude")
if (not current_lat and current_lat != 0.0) or (not current_lon and current_lon != 0.0):
vals["gps_captured"] = True
vals["gps_latitude"] = ctx_lat
vals["gps_longitude"] = ctx_lon
elif (current_lat == 0.0 and current_lon == 0.0) and (ctx_lat is not None and ctx_lon is not None):
vals["gps_captured"] = True
vals["gps_latitude"] = ctx_lat
vals["gps_longitude"] = ctx_lon
if ctx_client_time:
vals["gps_client_time"] = ctx_client_time
if ctx_client_tz:
vals["gps_client_tz"] = ctx_client_tz
if self._has_valid_gps(vals.get("gps_latitude"), vals.get("gps_longitude")):
vals["gps_captured"] = True
vals["gps_openstreetmap_url"] = self._build_osm_url(vals.get("gps_latitude"), vals.get("gps_longitude"))
# Final fallback from user's latest browser GPS ping.
has_gps = self._has_valid_gps(vals.get("gps_latitude"), vals.get("gps_longitude"))
if vals.get("res_model") == "crm.lead" and not has_gps:
try:
user = self.env.user.sudo()
# Check if GPS fields exist in res.users (module might not be upgraded in all databases)
if hasattr(user, 'last_gps_recorded_at') and user.last_gps_recorded_at:
max_age = fields.Datetime.now() - timedelta(minutes=30)
if user.last_gps_recorded_at >= max_age:
vals["gps_captured"] = True
vals["gps_latitude"] = user.last_gps_latitude
vals["gps_longitude"] = user.last_gps_longitude
vals["gps_client_time"] = user.last_gps_client_time or vals.get("gps_client_time")
vals["gps_client_tz"] = user.last_gps_client_tz or vals.get("gps_client_tz")
except Exception:
# Silently skip if GPS fields don't exist or any error occurs
pass
activities = super().create(vals_list)
activities._create_crm_activity_history()
return activities
def write(self, vals):
if self._has_valid_gps(vals.get("gps_latitude"), vals.get("gps_longitude")):
vals["gps_captured"] = True
vals["gps_openstreetmap_url"] = self._build_osm_url(vals.get("gps_latitude"), vals.get("gps_longitude"))
elif "gps_latitude" in vals or "gps_longitude" in vals:
vals["gps_openstreetmap_url"] = False
return super().write(vals)
def action_feedback(self, feedback=False, attachment_ids=None):
crm_activities = self.filtered(lambda a: a.res_model == "crm.lead")
if not crm_activities:
return super().action_feedback(feedback=feedback, attachment_ids=attachment_ids)
ctx = self.env.context
gps_captured = bool(ctx.get("gps_captured"))
gps_latitude = ctx.get("gps_latitude")
gps_longitude = ctx.get("gps_longitude")
gps_url = ctx.get("gps_openstreetmap_url")
gps_done_client_time = ctx.get("gps_done_client_time")
gps_done_client_tz = ctx.get("gps_done_client_tz")
extra_line = False
has_ctx_gps = self._has_valid_gps(gps_latitude, gps_longitude)
if has_ctx_gps:
gps_captured = True
if not gps_url:
gps_url = self._build_osm_url(gps_latitude, gps_longitude)
if gps_captured and gps_latitude is not None and gps_longitude is not None:
extra_line = _(
"Update at GPS location: %(lat)s, %(lon)s%(url)s",
lat=gps_latitude,
lon=gps_longitude,
url=(" - %s" % gps_url) if gps_url else "",
)
new_feedback = feedback or ""
if extra_line:
new_feedback = ("%s\n%s" % (new_feedback, extra_line)).strip()
crm_activities._mark_crm_activity_history_done(
feedback=new_feedback,
gps_captured=gps_captured,
gps_latitude=gps_latitude,
gps_longitude=gps_longitude,
gps_url=gps_url,
client_time=gps_done_client_time,
client_tz=gps_done_client_tz,
)
return super().action_feedback(feedback=new_feedback, attachment_ids=attachment_ids)
def _create_crm_activity_history(self):
history_model = self.env["crm.activity.history"].sudo()
histories = []
for activity in self:
if activity.res_model != "crm.lead":
continue
has_gps = self._has_valid_gps(activity.gps_latitude, activity.gps_longitude)
schedule_url = activity.gps_openstreetmap_url if has_gps else False
histories.append(
{
"name": activity.activity_type_id.display_name or activity.summary or _("Activity"),
"activity_id": activity.id,
"lead_id": activity.res_id,
"activity_type_id": activity.activity_type_id.id,
"assigned_user_id": activity.user_id.id,
"created_by_id": self.env.user.id,
"summary": activity.summary,
"note": activity.note,
"date_deadline": activity.date_deadline,
"scheduled_at": fields.Datetime.now(),
"state": "scheduled",
"schedule_gps_latitude": activity.gps_latitude if has_gps else False,
"schedule_gps_longitude": activity.gps_longitude if has_gps else False,
"schedule_gps_url": schedule_url,
"schedule_client_time": activity.gps_client_time or False,
"schedule_client_tz": activity.gps_client_tz or False,
}
)
if histories:
history_model.create(histories)
def _mark_crm_activity_history_done(
self,
feedback,
gps_captured=False,
gps_latitude=None,
gps_longitude=None,
gps_url=None,
client_time=None,
client_tz=None,
):
history_model = self.env["crm.activity.history"].sudo()
for activity in self:
history = history_model.search([("activity_id", "=", activity.id)], limit=1, order="id desc")
vals = {
"state": "done",
"done_at": fields.Datetime.now(),
"feedback": feedback or False,
"done_client_time": client_time or False,
"done_client_tz": client_tz or False,
}
if gps_captured and gps_latitude is not None and gps_longitude is not None:
vals.update(
{
"done_gps_latitude": gps_latitude,
"done_gps_longitude": gps_longitude,
"done_gps_url": gps_url or False,
}
)
if history:
history.write(vals)
else:
vals.update(
{
"name": activity.activity_type_id.display_name or activity.summary or _("Activity"),
"activity_id": activity.id,
"lead_id": activity.res_id,
"activity_type_id": activity.activity_type_id.id,
"assigned_user_id": activity.user_id.id,
"created_by_id": self.env.user.id,
"summary": activity.summary,
"note": activity.note,
"date_deadline": activity.date_deadline,
"scheduled_at": fields.Datetime.now(),
}
)
history_model.create(vals)
+80 -11
View File
@@ -5,6 +5,12 @@ from odoo.exceptions import ValidationError
class ResUsers(models.Model):
_inherit = "res.users"
last_gps_latitude = fields.Float(string="Last GPS Latitude", digits=(9, 6), readonly=True)
last_gps_longitude = fields.Float(string="Last GPS Longitude", digits=(9, 6), readonly=True)
last_gps_client_time = fields.Char(string="Last GPS Client Time", readonly=True)
last_gps_client_tz = fields.Char(string="Last GPS Client TZ", readonly=True)
last_gps_recorded_at = fields.Datetime(string="Last GPS Recorded At", readonly=True)
allowed_business_category_ids = fields.Many2many(
"crm.business.category",
"res_users_crm_business_category_rel",
@@ -13,31 +19,72 @@ class ResUsers(models.Model):
string="Allowed Business Categories",
help="Users can only access CRM data in these business categories.",
)
team_business_category_ids = fields.Many2many(
"crm.business.category",
string="Team Business Categories",
compute="_compute_effective_business_category_ids",
readonly=True,
help="Business categories inherited automatically from Sales Team membership.",
)
effective_business_category_ids = fields.Many2many(
"crm.business.category",
string="Effective Business Categories",
compute="_compute_effective_business_category_ids",
readonly=True,
help="Union of manual access and Sales Team-based access.",
)
default_business_category_id = fields.Many2one(
"crm.business.category",
string="Default Business Category",
domain="[('id', 'in', allowed_business_category_ids)]",
domain="[('id', 'in', effective_business_category_ids)]",
help="Default category for new records.",
)
active_business_category_id = fields.Many2one(
"crm.business.category",
string="Active Business Category",
domain="[('id', 'in', allowed_business_category_ids)]",
domain="[('id', 'in', effective_business_category_ids)]",
help="Current user context category, similar to active company.",
)
def _get_team_business_categories(self):
self.ensure_one()
team_model = self.env["crm.team"].sudo()
domain_parts = []
if "user_id" in team_model._fields:
domain_parts.append(("user_id", "=", self.id))
if "member_ids" in team_model._fields:
domain_parts.append(("member_ids", "in", self.id))
if not domain_parts:
return self.env["crm.business.category"]
if len(domain_parts) == 1:
domain = domain_parts
else:
domain = ["|"] + domain_parts
teams = team_model.search(domain)
return teams.mapped("business_category_id")
@api.depends("allowed_business_category_ids")
def _compute_effective_business_category_ids(self):
for user in self:
team_categories = user._get_team_business_categories()
user.team_business_category_ids = team_categories
user.effective_business_category_ids = user.allowed_business_category_ids | team_categories
@api.onchange("allowed_business_category_ids")
def _onchange_allowed_business_category_ids(self):
for user in self:
if user.default_business_category_id not in user.allowed_business_category_ids:
effective_categories = user.allowed_business_category_ids | user._get_team_business_categories()
if user.default_business_category_id not in effective_categories:
user.default_business_category_id = False
if user.active_business_category_id not in user.allowed_business_category_ids:
if user.active_business_category_id not in effective_categories:
user.active_business_category_id = False
if user.allowed_business_category_ids and not user.default_business_category_id:
user.default_business_category_id = user.allowed_business_category_ids[0]
if user.allowed_business_category_ids and not user.active_business_category_id:
if effective_categories and not user.default_business_category_id:
user.default_business_category_id = effective_categories[0]
if effective_categories and not user.active_business_category_id:
user.active_business_category_id = user.default_business_category_id
@api.constrains(
@@ -47,11 +94,33 @@ class ResUsers(models.Model):
)
def _check_business_category_consistency(self):
for user in self:
if user.default_business_category_id and user.default_business_category_id not in user.allowed_business_category_ids:
effective_categories = user.allowed_business_category_ids | user._get_team_business_categories()
if user.default_business_category_id and user.default_business_category_id not in effective_categories:
raise ValidationError(
_("Default Business Category must be included in Allowed Business Categories.")
_("Default Business Category must be included in Effective Business Categories.")
)
if user.active_business_category_id and user.active_business_category_id not in user.allowed_business_category_ids:
if user.active_business_category_id and user.active_business_category_id not in effective_categories:
raise ValidationError(
_("Active Business Category must be included in Allowed Business Categories.")
_("Active Business Category must be included in Effective Business Categories.")
)
def action_sync_team_business_category_access(self):
for user in self:
effective_categories = user.allowed_business_category_ids | user._get_team_business_categories()
first_category = effective_categories[:1]
vals = {}
if user.default_business_category_id not in effective_categories:
vals["default_business_category_id"] = first_category.id if first_category else False
elif not user.default_business_category_id and first_category:
vals["default_business_category_id"] = first_category.id
default_id = vals.get("default_business_category_id") or user.default_business_category_id.id
if user.active_business_category_id not in effective_categories:
vals["active_business_category_id"] = default_id or (first_category.id if first_category else False)
elif not user.active_business_category_id:
vals["active_business_category_id"] = default_id or (first_category.id if first_category else False)
if vals:
user.write(vals)
return {"type": "ir.actions.client", "tag": "reload"}
@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_crm_business_category_user,crm.business.category.user,model_crm_business_category,crm.group_use_lead,1,0,0,0
access_crm_business_category_manager,crm.business.category.manager,model_crm_business_category,sales_team.group_sale_manager,1,1,1,1
access_crm_activity_history_user,crm.activity.history.user,model_crm_activity_history,crm.group_use_lead,1,0,0,0
access_crm_activity_history_manager,crm.activity.history.manager,model_crm_activity_history,sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_crm_business_category_user crm.business.category.user model_crm_business_category crm.group_use_lead 1 0 0 0
3 access_crm_business_category_manager crm.business.category.manager model_crm_business_category sales_team.group_sale_manager 1 1 1 1
4 access_crm_activity_history_user crm.activity.history.user model_crm_activity_history crm.group_use_lead 1 0 0 0
5 access_crm_activity_history_manager crm.activity.history.manager model_crm_activity_history sales_team.group_sale_manager 1 1 1 1
@@ -1,8 +1,10 @@
id,name,model_id:id,domain_force,groups:id,perm_read,perm_write,perm_create,perm_unlink
crm_lead_business_category_rule_user_read_create,CRM Lead read/create by allowed business category,crm.model_crm_lead,"[('business_category_id','in',user.allowed_business_category_ids.ids)]",crm.group_use_lead,1,0,1,0
crm_lead_business_category_rule_user_write_own,CRM Lead write own by allowed business category,crm.model_crm_lead,"[('business_category_id','in',user.allowed_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",crm.group_use_lead,0,1,0,1
crm_team_business_category_rule_user,CRM Team read by allowed business category,sales_team.model_crm_team,"[('business_category_id','in',user.allowed_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_business_category_rule_user,Business Category by user allow list,model_crm_business_category,"[('id','in',user.allowed_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_lead_business_category_rule_user_read_create,CRM Lead read/create by effective business category,crm.model_crm_lead,"[('business_category_id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,1,0
crm_lead_business_category_rule_user_write_own,CRM Lead write own by effective business category,crm.model_crm_lead,"[('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)]",crm.group_use_lead,0,1,0,1
crm_team_business_category_rule_user,CRM Team read by effective business category,sales_team.model_crm_team,"[('business_category_id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_business_category_rule_user,Business Category by user effective list,model_crm_business_category,"[('id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_lead_business_category_rule_manager,CRM Lead full access for manager,crm.model_crm_lead,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
crm_team_business_category_rule_manager,CRM Team full access for manager,sales_team.model_crm_team,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
crm_business_category_rule_manager,Business Category full access for manager,model_crm_business_category,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
crm_activity_history_rule_user,CRM Activity History by effective business category,model_crm_activity_history,"[('business_category_id','in',user.effective_business_category_ids.ids)]",crm.group_use_lead,1,0,0,0
crm_activity_history_rule_manager,CRM Activity History full access for manager,model_crm_activity_history,"[(1,'=',1)]",sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id domain_force groups:id perm_read perm_write perm_create perm_unlink
2 crm_lead_business_category_rule_user_read_create CRM Lead read/create by allowed business category CRM Lead read/create by effective business category crm.model_crm_lead [('business_category_id','in',user.allowed_business_category_ids.ids)] [('business_category_id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 1 0
3 crm_lead_business_category_rule_user_write_own CRM Lead write own by allowed business category CRM Lead write own by effective business category crm.model_crm_lead [('business_category_id','in',user.allowed_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] [('business_category_id','in',user.effective_business_category_ids.ids),'|',('user_id','=',user.id),('create_uid','=',user.id)] crm.group_use_lead 0 1 0 1
4 crm_team_business_category_rule_user CRM Team read by allowed business category CRM Team read by effective business category sales_team.model_crm_team [('business_category_id','in',user.allowed_business_category_ids.ids)] [('business_category_id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
5 crm_business_category_rule_user Business Category by user allow list Business Category by user effective list model_crm_business_category [('id','in',user.allowed_business_category_ids.ids)] [('id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
6 crm_lead_business_category_rule_manager CRM Lead full access for manager crm.model_crm_lead [(1,'=',1)] sales_team.group_sale_manager 1 1 1 1
7 crm_team_business_category_rule_manager CRM Team full access for manager sales_team.model_crm_team [(1,'=',1)] sales_team.group_sale_manager 1 1 1 1
8 crm_business_category_rule_manager Business Category full access for manager model_crm_business_category [(1,'=',1)] sales_team.group_sale_manager 1 1 1 1
9 crm_activity_history_rule_user CRM Activity History by effective business category model_crm_activity_history [('business_category_id','in',user.effective_business_category_ids.ids)] crm.group_use_lead 1 0 0 0
10 crm_activity_history_rule_manager CRM Activity History full access for manager model_crm_activity_history [(1,'=',1)] sales_team.group_sale_manager 1 1 1 1
@@ -0,0 +1,252 @@
odoo.define("grt_crm_business_category.activity_gps_simple", function (require) {
"use strict";
var core = require("web.core");
var Dialog = require("web.Dialog");
var FormController = require("web.FormController");
var ajax = require("web.ajax");
var _t = core._t;
var pendingGpsCoords = null;
// GPS Configuration
var GPS_TIMEOUT = 8000;
var GPS_BACKGROUND_INTERVAL = 60000;
// Simple promise-based getCurrentPosition
function getCurrentPosition() {
return new Promise(function(resolve, reject) {
if (!navigator.geolocation) {
return reject("Geolocation not available");
}
navigator.geolocation.getCurrentPosition(
function (position) {
var coords = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
};
console.log("[GPS] Captured high accuracy:", coords);
pendingGpsCoords = coords;
resolve(coords);
},
function (error) {
console.warn("[GPS] High accuracy failed, trying low accuracy:", error.message);
// Try low accuracy fallback
navigator.geolocation.getCurrentPosition(
function (position) {
var coords = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
};
console.log("[GPS] Captured low accuracy:", coords);
pendingGpsCoords = coords;
resolve(coords);
},
function (err) {
console.error("[GPS] Both attempts failed:", err.message);
reject(err.message);
},
{enableHighAccuracy: false, timeout: 10000, maximumAge: 60000}
);
},
{enableHighAccuracy: true, timeout: GPS_TIMEOUT, maximumAge: 0}
);
});
}
// Background GPS capture every 60 seconds
function captureGpsInBackground() {
getCurrentPosition()
.then(function (coords) {
console.log("[GPS] Background capture successful");
// Send to server
var now = new Date();
var localTime = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
ajax.jsonRpc("/grt_crm_business_category/gps/ping", "call", {
latitude: coords.latitude,
longitude: coords.longitude,
client_time: localTime,
client_tz: Intl.DateTimeFormat().resolvedOptions().timeZone || ""
}).catch(function (err) {
console.warn("[GPS] Background ping failed:", err);
});
})
.catch(function (err) {
console.warn("[GPS] Background capture failed:", err);
});
}
// Start background capture
console.log("[GPS] Initializing background GPS capture");
setTimeout(captureGpsInBackground, 2000);
setInterval(captureGpsInBackground, GPS_BACKGROUND_INTERVAL);
// Extend FormController for activity forms
FormController.include({
_update: function () {
var result = this._super.apply(this, arguments);
var self = this;
// Attach GPS button handler when form loads
if (this.modelName === 'mail.activity') {
result.then(function() {
self._attachGpsButtonHandler();
});
}
return result;
},
_attachGpsButtonHandler: function() {
var self = this;
console.log("[GPS] Looking for button...");
// Multiple selector attempts
var $button = this.$('[name="gps_capture_btn"]');
if ($button.length === 0) {
$button = this.$('.grt_capture_gps_btn');
}
if ($button.length === 0) {
$button = this.$('.o_activity_gps_btn');
}
if ($button.length > 0) {
console.log("[GPS] Button found, attaching click handler");
$button.off('click').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
self._captureGpsAndUpdate();
return false;
});
} else {
console.warn("[GPS] Button not found in DOM");
}
},
_captureGpsAndUpdate: function() {
var self = this;
console.log("[GPS] Manual capture started");
getCurrentPosition()
.then(function(coords) {
console.log("[GPS] Manual capture successful:", coords);
// Show success notification
var msg = "GPS berhasil di-capture: " + coords.latitude.toFixed(6) + ", " + coords.longitude.toFixed(6);
Dialog.alert(self, msg, { title: 'GPS Captured' });
// Update the form fields using the correct Odoo 14 approach
self._updateActivityGpsFieldsOdoo14(coords);
})
.catch(function(err) {
console.error("[GPS] Manual capture failed:", err);
Dialog.alert(self, "Gagal: " + err, { title: 'GPS Error' });
});
},
_updateActivityGpsFieldsOdoo14: function(coords) {
console.log("[GPS] Updating activity fields (Odoo 14 proper method)");
var self = this;
var handle = this.handle;
var model = this.model;
try {
// Get the current record
var record = model.get(handle);
console.log("[GPS] Current record:", record);
if (!record) {
console.error("[GPS] Record not found");
return;
}
// Direct record data update (Odoo 14 way)
var osmUrl = "https://www.openstreetmap.org/?mlat=" + coords.latitude + "&mlon=" + coords.longitude + "#map=18/" + coords.latitude + "/" + coords.longitude;
var now = new Date();
var localTime = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
var changes = {
gps_captured: true,
gps_latitude: coords.latitude,
gps_longitude: coords.longitude,
gps_openstreetmap_url: osmUrl,
gps_client_time: localTime,
gps_client_tz: Intl.DateTimeFormat().resolvedOptions().timeZone || ""
};
// Update internal record data
_.extend(record.data, changes);
console.log("[GPS] Record data updated:", record.data);
// Method 1: Try notifyChanges with a changes object
try {
model.notifyChanges(handle, changes);
console.log("[GPS] notifyChanges called with changes object");
} catch (e) {
console.warn("[GPS] notifyChanges with changes failed:", e.message);
}
// Method 2: Try calling notifyChanges without params
try {
model.notifyChanges(handle);
console.log("[GPS] notifyChanges called without params");
} catch (e) {
console.warn("[GPS] notifyChanges simple failed:", e.message);
}
// Method 3: Force renderer update after delay
setTimeout(function() {
try {
if (self.renderer && self.renderer.update) {
var state = model.get(handle, {raw: true});
self.renderer.update(state);
console.log("[GPS] Renderer.update called");
}
} catch (e) {
console.warn("[GPS] Renderer update failed:", e.message);
}
}, 100);
// Method 4: Update DOM inputs for display only (no change trigger)
setTimeout(function() {
console.log("[GPS] Attempting direct DOM update...");
var $latInput = self.$('input[name="gps_latitude"]');
var $lonInput = self.$('input[name="gps_longitude"]');
if ($latInput.length) {
$latInput.val(coords.latitude);
console.log("[GPS] DOM lat input display updated to", coords.latitude);
}
if ($lonInput.length) {
$lonInput.val(coords.longitude);
console.log("[GPS] DOM lon input display updated to", coords.longitude);
}
var $urlInput = self.$('input[name="gps_openstreetmap_url"]');
if ($urlInput.length) {
$urlInput.val(osmUrl);
console.log("[GPS] DOM URL input display updated");
}
// Also check for the checkbox
var $capturedCheckbox = self.$('input[name="gps_captured"]');
if ($capturedCheckbox.length) {
$capturedCheckbox.prop('checked', true);
console.log("[GPS] gps_captured checkbox display checked");
}
}, 150);
} catch (err) {
console.error("[GPS] Error updating fields:", err, err.stack);
}
}
});
console.log("[GPS] Module loaded successfully");
});
@@ -0,0 +1,559 @@
odoo.define("grt_crm_business_category.activity_gps", function (require) {
"use strict";
var core = require("web.core");
var Dialog = require("web.Dialog");
var rpc = require("web.rpc");
var ajax = require("web.ajax");
var _t = core._t;
var originalQuery = rpc.query ? rpc.query.bind(rpc) : null;
var mapClickHandlerRegistered = false;
if (!originalQuery) {
console.error("GRT GPS: rpc.query not available, GPS tracking disabled");
return;
}
function showGpsWarning(message) {
Dialog.alert(
null,
message ||
_t(
"GPS belum aktif atau belum terdeteksi. Aktifkan izin lokasi di browser, lalu refresh halaman sebelum mengisi activity."
)
);
}
function getCurrentPosition() {
var deferred = $.Deferred();
console.log("GRT GPS: getCurrentPosition called");
console.log("GRT GPS: navigator.geolocation available:", !!navigator.geolocation);
console.log("GRT GPS: window.isSecureContext:", window.isSecureContext);
console.log("GRT GPS: Current protocol:", window.location.protocol);
console.log("GRT GPS: Current host:", window.location.host);
if (!navigator.geolocation) {
var msg = _t("Browser tidak mendukung Geolocation API.");
console.error("GRT GPS:", msg);
deferred.reject(msg);
return deferred.promise();
}
// Try high accuracy first; fallback to lower accuracy for laptops/desktops.
console.log("GRT GPS: Requesting position with high accuracy...");
navigator.geolocation.getCurrentPosition(
function (position) {
console.log("GRT GPS: High accuracy success:", position.coords);
deferred.resolve(position.coords);
},
function (errorHigh) {
console.warn("GRT GPS: High accuracy failed, error code:", errorHigh.code, "message:", errorHigh.message);
console.log("GRT GPS: Trying low accuracy fallback...");
navigator.geolocation.getCurrentPosition(
function (position) {
console.log("GRT GPS: Low accuracy success:", position.coords);
deferred.resolve(position.coords);
},
function (error) {
var reason = _t("Lokasi tidak dapat diambil.");
if (error && error.code === 1) {
reason = _t("Izin lokasi ditolak oleh browser.");
console.error("GRT GPS: Permission denied (code 1)");
} else if (error && error.code === 2) {
reason = _t("Posisi tidak tersedia (sinyal/Wi-Fi lemah).");
console.error("GRT GPS: Position unavailable (code 2)");
} else if (error && error.code === 3) {
reason = _t("Permintaan lokasi timeout.");
console.error("GRT GPS: Timeout (code 3)");
}
if (!window.isSecureContext) {
reason +=
" " +
_t(
"Koneksi saat ini bukan secure context. Buka Odoo via HTTPS atau http://localhost agar geolocation diizinkan browser."
);
console.error("GRT GPS: Not a secure context -", window.location.href);
}
console.error("GRT GPS: Both attempts failed. Final reason:", reason);
deferred.reject(reason);
},
{
enableHighAccuracy: false,
timeout: 25000,
maximumAge: 300000,
}
);
},
{
enableHighAccuracy: true,
timeout: 12000,
maximumAge: 0,
}
);
return deferred.promise();
}
function pushGpsToServer(coords) {
if (!coords) {
return;
}
ajax.jsonRpc("/grt_crm_business_category/gps/ping", "call", {
latitude: coords.latitude,
longitude: coords.longitude,
client_time: new Date().toLocaleString(),
client_tz: Intl.DateTimeFormat().resolvedOptions().timeZone || "",
}).fail(function (err) {
console.warn("GRT GPS: Failed to push GPS ping to server", err);
});
}
function normalizeCreateArgs(args) {
if (!args || !args.length) {
return null;
}
var firstArg = args[0];
if (Array.isArray(firstArg)) {
return {valuesList: firstArg, isList: true};
}
if (firstArg && typeof firstArg === "object") {
return {valuesList: [firstArg], isList: false};
}
return null;
}
function isCrmCreateContext(params, valuesList) {
var context = (params && params.kwargs && params.kwargs.context) || {};
if (context.default_res_model === "crm.lead" || context.active_model === "crm.lead") {
return true;
}
return valuesList.some(function (values) {
return values && values.res_model === "crm.lead";
});
}
function injectCoordinates(valuesList, coords, isCrmContext) {
console.log("GRT GPS: injectCoordinates called with coords:", coords);
console.log("GRT GPS: injectCoordinates valuesList before:", JSON.stringify(valuesList));
var result = valuesList.map(function (values) {
var updated = Object.assign({}, values);
updated.gps_captured = true;
updated.gps_latitude = coords.latitude;
updated.gps_longitude = coords.longitude;
updated.gps_client_time = new Date().toLocaleString();
updated.gps_client_tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
console.log("GRT GPS: Injected GPS fields - lat:", updated.gps_latitude, "lon:", updated.gps_longitude);
if (isCrmContext) {
var gpsUrl = buildGpsUrl(coords.latitude, coords.longitude);
var gpsNoteHtml =
"<p>Update at GPS location: " +
coords.latitude +
", " +
coords.longitude +
' - <a href="' +
gpsUrl +
'" class="grt-gps-map-link" target="_blank">Open Maps</a></p>';
updated.note = (updated.note || "") + gpsNoteHtml;
}
return updated;
});
console.log("GRT GPS: injectCoordinates result:", JSON.stringify(result));
return result;
}
function getActivityIds(params) {
if (!params || !params.args || !params.args.length) {
return [];
}
var ids = params.args[0];
return Array.isArray(ids) ? ids : [];
}
function injectGpsContext(params, coords) {
var gpsUrl = buildGpsUrl(coords.latitude, coords.longitude);
var kwargs = Object.assign({}, params.kwargs || {});
var context = Object.assign({}, kwargs.context || {});
context.gps_captured = true;
context.gps_latitude = coords.latitude;
context.gps_longitude = coords.longitude;
context.gps_openstreetmap_url = gpsUrl;
context.gps_done_client_time = new Date().toLocaleString();
context.gps_done_client_tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
kwargs.context = context;
return Object.assign({}, params, {kwargs: kwargs});
}
function injectCreateGpsContext(params, coords) {
var kwargs = Object.assign({}, params.kwargs || {});
var context = Object.assign({}, kwargs.context || {});
context.gps_captured = true;
context.gps_latitude = coords.latitude;
context.gps_longitude = coords.longitude;
context.gps_client_time = new Date().toLocaleString();
context.gps_client_tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
kwargs.context = context;
return Object.assign({}, params, {kwargs: kwargs});
}
function buildGpsUrl(latitude, longitude) {
return (
"https://www.openstreetmap.org/?mlat=" +
latitude +
"&mlon=" +
longitude +
"#map=18/" +
latitude +
"/" +
longitude
);
}
function registerMapModalHandler() {
if (mapClickHandlerRegistered) {
return;
}
mapClickHandlerRegistered = true;
$(document).on("click", "a.grt-gps-map-link", function (ev) {
var url = $(this).attr("href");
if (!url || url.indexOf("openstreetmap.org") === -1) {
return;
}
ev.preventDefault();
openMapModal(url);
});
}
function openMapModal(url) {
var embedUrl = buildOsmEmbedUrl(url);
var $content = $(
'<div style="height:70vh;">' +
'<iframe src="' +
_.escape(embedUrl) +
'" style="width:100%;height:100%;border:0;" allowfullscreen="allowfullscreen"></iframe>' +
"</div>"
);
var dialog = new Dialog(null, {
title: _t("GPS Location"),
$content: $content,
size: "large",
buttons: [
{
text: _t("Open in New Tab"),
classes: "btn-primary",
close: false,
click: function () {
window.open(url, "_blank");
},
},
{
text: _t("Close"),
close: true,
},
],
});
dialog.open();
}
function buildOsmEmbedUrl(url) {
try {
var parsed = new URL(url, window.location.origin);
var lat = parseFloat(parsed.searchParams.get("mlat"));
var lon = parseFloat(parsed.searchParams.get("mlon"));
if ((!isFinite(lat) || !isFinite(lon)) && parsed.hash && parsed.hash.indexOf("#map=") === 0) {
var mapParts = parsed.hash.replace("#map=", "").split("/");
if (mapParts.length >= 3) {
lat = parseFloat(mapParts[1]);
lon = parseFloat(mapParts[2]);
}
}
if (!isFinite(lat) || !isFinite(lon)) {
return "about:blank";
}
var delta = 0.01;
var left = lon - delta;
var right = lon + delta;
var top = lat + delta;
var bottom = lat - delta;
return (
"https://www.openstreetmap.org/export/embed.html?bbox=" +
left +
"%2C" +
bottom +
"%2C" +
right +
"%2C" +
top +
"&layer=mapnik&marker=" +
lat +
"%2C" +
lon
);
} catch (err) {
return "about:blank";
}
}
registerMapModalHandler();
// Add GPS status indicator to UI
function addGpsStatusIndicator() {
if ($('#grt_gps_status_indicator').length) {
return; // Already added
}
var $indicator = $('<div id="grt_gps_status_indicator" style="' +
'position: fixed; ' +
'bottom: 10px; ' +
'right: 10px; ' +
'background: rgba(0,0,0,0.7); ' +
'color: white; ' +
'padding: 8px 12px; ' +
'border-radius: 4px; ' +
'font-size: 12px; ' +
'z-index: 9999; ' +
'cursor: pointer; ' +
'box-shadow: 0 2px 8px rgba(0,0,0,0.3);' +
'">' +
'<span class="fa fa-map-marker"></span> GPS: <span id="grt_gps_status_text">Initializing...</span>' +
'</div>');
$indicator.on('click', function() {
var msg = "GPS Status: " + gpsStatus + "\n" +
"Has coordinates: " + (!!pendingGpsCoords) + "\n" +
"Secure context: " + window.isSecureContext + "\n" +
"Protocol: " + window.location.protocol + "\n" +
"Host: " + window.location.host;
if (pendingGpsCoords) {
msg += "\n\nLatitude: " + pendingGpsCoords.latitude.toFixed(6) +
"\nLongitude: " + pendingGpsCoords.longitude.toFixed(6) +
"\nAccuracy: " + (pendingGpsCoords.accuracy || 'N/A') + "m";
}
alert(msg);
});
$('body').append($indicator);
updateGpsStatusIndicator();
}
function updateGpsStatusIndicator() {
var $text = $('#grt_gps_status_text');
if (!$text.length) return;
if (gpsStatus === "success" && pendingGpsCoords) {
$text.text('Active ✓').css('color', '#28a745');
$('#grt_gps_status_indicator').css('background', 'rgba(40, 167, 69, 0.9)');
} else if (gpsStatus === "capturing") {
$text.text('Capturing...').css('color', '#ffc107');
$('#grt_gps_status_indicator').css('background', 'rgba(255, 193, 7, 0.9)');
} else if (gpsStatus === "failed") {
$text.text('Failed ✗').css('color', '#dc3545');
$('#grt_gps_status_indicator').css('background', 'rgba(220, 53, 69, 0.9)');
} else {
$text.text('Starting...').css('color', '#17a2b8');
$('#grt_gps_status_indicator').css('background', 'rgba(23, 162, 184, 0.9)');
}
}
// Add indicator when DOM ready
$(document).ready(function() {
setTimeout(addGpsStatusIndicator, 1000);
});
// Store GPS coordinates in memory for immediate use
var pendingGpsCoords = null;
var gpsCapturePending = false;
var gpsStatus = "not_started"; // not_started, capturing, success, failed
function captureGpsInBackground() {
if (gpsCapturePending) {
console.log("GRT GPS: Capture already in progress, skipping");
return;
}
console.log("GRT GPS: Starting background GPS capture...");
gpsCapturePending = true;
gpsStatus = "capturing";
updateGpsStatusIndicator();
getCurrentPosition()
.done(function (coords) {
pendingGpsCoords = coords;
gpsCapturePending = false;
gpsStatus = "success";
console.log("GRT GPS: Successfully captured coordinates:", coords.latitude, coords.longitude);
pushGpsToServer(coords);
updateGpsStatusIndicator();
})
.fail(function (reason) {
gpsCapturePending = false;
gpsStatus = "failed";
console.error("GRT GPS: Failed to capture GPS:", reason);
updateGpsStatusIndicator();
});
}
// Pre-capture GPS on page load
console.log("GRT GPS: Initializing GPS tracking module");
setTimeout(function() {
captureGpsInBackground();
}, 2000); // Wait 2 seconds after page load
// Re-capture every 60 seconds
setInterval(captureGpsInBackground, 60000);
function processMailActivityRpc(params, options, callOriginal) {
try {
if (!params || params.model !== "mail.activity") {
return callOriginal(params, options);
}
if (params.method === "create") {
var normalized = normalizeCreateArgs(params.args);
if (!normalized) {
return callOriginal(params, options);
}
console.log("GRT GPS: Creating CRM activity, GPS status:", gpsStatus, "Has coords:", !!pendingGpsCoords);
var crmContext = isCrmCreateContext(params, normalized.valuesList);
// Use cached GPS if available
if (pendingGpsCoords) {
console.log("GRT GPS: Using cached coordinates:", pendingGpsCoords.latitude, pendingGpsCoords.longitude);
var updatedArgs = params.args.slice(0);
var updatedValuesList = injectCoordinates(normalized.valuesList, pendingGpsCoords, crmContext);
updatedArgs[0] = normalized.isList ? updatedValuesList : updatedValuesList[0];
console.log("GRT GPS: Original params.args:", JSON.stringify(params.args));
console.log("GRT GPS: Updated args being sent:", JSON.stringify(updatedArgs));
var updatedParams = Object.assign({}, params, {args: updatedArgs});
updatedParams = injectCreateGpsContext(updatedParams, pendingGpsCoords);
console.log("GRT GPS: Final params being sent to RPC:", JSON.stringify(updatedParams));
// Re-capture for next time (async, don't wait)
setTimeout(captureGpsInBackground, 100);
return callOriginal(updatedParams, options);
}
// No GPS available - try to capture now with user feedback
console.warn("GRT GPS: No cached GPS. Attempting immediate capture...");
// Show a brief notification
var $notification = $('<div class="o_notification_manager">' +
'<div class="o_notification_content">' +
'<div class="o_notification bg-info">' +
'<div class="o_close" style="cursor:pointer;">×</div>' +
'<div>Mencoba menangkap lokasi GPS...</div>' +
'</div></div></div>');
$('body').append($notification);
var notifTimeout = setTimeout(function() {
$notification.fadeOut(function() { $(this).remove(); });
}, 3000);
// Try immediate capture
var deferred = $.Deferred();
getCurrentPosition()
.done(function (coords) {
clearTimeout(notifTimeout);
$notification.find('.o_notification').removeClass('bg-info').addClass('bg-success')
.find('div:last').text('GPS berhasil: ' + coords.latitude.toFixed(6) + ', ' + coords.longitude.toFixed(6));
setTimeout(function() {
$notification.fadeOut(function() { $(this).remove(); });
}, 2000);
pendingGpsCoords = coords;
pushGpsToServer(coords);
console.log("GRT GPS: Immediate capture successful:", coords.latitude, coords.longitude);
var updatedArgs = params.args.slice(0);
var updatedValuesList = injectCoordinates(normalized.valuesList, coords, crmContext);
updatedArgs[0] = normalized.isList ? updatedValuesList : updatedValuesList[0];
var updatedParams = Object.assign({}, params, {args: updatedArgs});
updatedParams = injectCreateGpsContext(updatedParams, coords);
callOriginal(updatedParams, options).then(function(result) {
deferred.resolve(result);
}).guardedCatch(function(error) {
deferred.reject(error);
});
})
.fail(function (reason) {
clearTimeout(notifTimeout);
$notification.find('.o_notification').removeClass('bg-info').addClass('bg-warning')
.find('div:last').text('GPS gagal: ' + reason);
setTimeout(function() {
$notification.fadeOut(function() { $(this).remove(); });
}, 4000);
console.error("GRT GPS: Immediate capture failed:", reason);
showGpsWarning(_t("GPS tidak dapat diambil: ") + reason + _t(". Activity dibuat tanpa koordinat GPS."));
callOriginal(params, options).then(function(result) {
deferred.resolve(result);
}).guardedCatch(function(error) {
deferred.reject(error);
});
});
return deferred;
}
if (params.method === "action_feedback") {
var ids = getActivityIds(params);
if (!ids.length) {
return callOriginal(params, options);
}
console.log("GRT GPS: Marking activity done, GPS status:", gpsStatus, "Has coords:", !!pendingGpsCoords);
// For action_feedback, inject GPS if available
if (pendingGpsCoords) {
console.log("GRT GPS: Injecting GPS context for feedback:", pendingGpsCoords.latitude, pendingGpsCoords.longitude);
var updatedParams = injectGpsContext(params, pendingGpsCoords);
// Re-capture for next time (async, don't wait)
setTimeout(captureGpsInBackground, 100);
return callOriginal(updatedParams, options);
}
// No GPS, proceed without it
console.warn("GRT GPS: No GPS available for feedback");
setTimeout(captureGpsInBackground, 100); // Try to capture for next time
return callOriginal(params, options);
}
return callOriginal(params, options);
} catch (e) {
console.error("GRT GPS: Process RPC error", e);
return callOriginal(params, options);
}
}
rpc.query = function (params, options) {
try {
return processMailActivityRpc(
params,
options,
function (p, o) {
return originalQuery(p, o);
}
);
} catch (e) {
console.error("GRT GPS: RPC query error", e);
return originalQuery(params, options);
}
};
window.__grtGpsHookLoaded = true;
});
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template
id="grt_crm_business_category_assets_backend"
name="GRT CRM Business Category Assets"
inherit_id="web.assets_backend"
>
<xpath expr="//script[last()]" position="after">
<script
type="text/javascript"
src="/grt_crm_business_category/static/src/js/activity_gps.js"
/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_crm_activity_history_search" model="ir.ui.view">
<field name="name">crm.activity.history.search</field>
<field name="model">crm.activity.history</field>
<field name="arch" type="xml">
<search string="CRM Activity History">
<field name="lead_id"/>
<field name="activity_type_id"/>
<field name="assigned_user_id"/>
<field name="business_category_id"/>
<field name="team_id"/>
<field name="state"/>
<filter string="Scheduled" name="scheduled" domain="[('state', '=', 'scheduled')]"/>
<filter string="Done" name="done" domain="[('state', '=', 'done')]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
<filter string="Assigned To" name="group_assigned" context="{'group_by': 'assigned_user_id'}"/>
<filter string="Category" name="group_category" context="{'group_by': 'business_category_id'}"/>
<filter string="Team" name="group_team" context="{'group_by': 'team_id'}"/>
<filter string="Type" name="group_type" context="{'group_by': 'activity_type_id'}"/>
<filter string="Deadline" name="group_deadline" context="{'group_by': 'date_deadline'}"/>
</group>
</search>
</field>
</record>
<record id="view_crm_activity_history_tree" model="ir.ui.view">
<field name="name">crm.activity.history.tree</field>
<field name="model">crm.activity.history</field>
<field name="arch" type="xml">
<tree string="CRM Activity History" create="false" edit="false" delete="false">
<field name="scheduled_at"/>
<field name="done_at"/>
<field name="state"/>
<field name="lead_id"/>
<field name="activity_type_id"/>
<field name="assigned_user_id"/>
<field name="business_category_id"/>
<field name="team_id"/>
<field name="date_deadline"/>
<field name="schedule_gps_url" widget="url" string="Schedule GPS"/>
<field name="done_gps_url" widget="url" string="Done GPS"/>
<field name="schedule_client_time"/>
<field name="done_client_time"/>
</tree>
</field>
</record>
<record id="view_crm_activity_history_form" model="ir.ui.view">
<field name="name">crm.activity.history.form</field>
<field name="model">crm.activity.history</field>
<field name="arch" type="xml">
<form string="CRM Activity History" create="false" edit="false" delete="false">
<sheet>
<group>
<group>
<field name="name"/>
<field name="state"/>
<field name="lead_id"/>
<field name="activity_type_id"/>
<field name="assigned_user_id"/>
<field name="created_by_id"/>
</group>
<group>
<field name="business_category_id"/>
<field name="team_id"/>
<field name="date_deadline"/>
<field name="scheduled_at"/>
<field name="done_at"/>
</group>
</group>
<notebook>
<page string="Schedule">
<group>
<field name="summary"/>
<field name="note"/>
</group>
<group>
<field name="schedule_gps_latitude"/>
<field name="schedule_gps_longitude"/>
<field name="schedule_gps_url" widget="url"/>
<field name="schedule_client_time"/>
<field name="schedule_client_tz"/>
</group>
</page>
<page string="Done">
<group>
<field name="feedback"/>
</group>
<group>
<field name="done_gps_latitude"/>
<field name="done_gps_longitude"/>
<field name="done_gps_url" widget="url"/>
<field name="done_client_time"/>
<field name="done_client_tz"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_crm_activity_history_pivot" model="ir.ui.view">
<field name="name">crm.activity.history.pivot</field>
<field name="model">crm.activity.history</field>
<field name="arch" type="xml">
<pivot string="CRM Activity History">
<field name="state" type="row"/>
<field name="assigned_user_id" type="row"/>
<field name="business_category_id" type="row"/>
<field name="activity_type_id" type="col"/>
<field name="id" type="measure" string="Activities"/>
</pivot>
</field>
</record>
<record id="view_crm_activity_history_graph" model="ir.ui.view">
<field name="name">crm.activity.history.graph</field>
<field name="model">crm.activity.history</field>
<field name="arch" type="xml">
<graph string="CRM Activity History" type="bar">
<field name="activity_type_id"/>
<field name="state"/>
</graph>
</field>
</record>
<record id="action_crm_activity_history_report" model="ir.actions.act_window">
<field name="name">Activity History Report</field>
<field name="res_model">crm.activity.history</field>
<field name="view_mode">tree,form,pivot,graph</field>
<field name="search_view_id" ref="view_crm_activity_history_search"/>
</record>
<menuitem
id="menu_crm_activity_history_report"
name="Activity History"
parent="crm.crm_menu_report"
action="action_crm_activity_history_report"
sequence="35"
groups="sales_team.group_sale_manager"
/>
</odoo>
@@ -6,6 +6,7 @@
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="user_id"/>
<field name="business_category_id"/>
<field name="active"/>
</tree>
@@ -20,6 +21,9 @@
<sheet>
<group>
<field name="name"/>
<field name="company_id" invisible="1"/>
<field name="user_id"/>
<field name="member_ids" widget="many2many_tags"/>
<field name="business_category_id"/>
<field name="active"/>
</group>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail_activity_view_form_popup_gps" model="ir.ui.view">
<field name="name">mail.activity.form.popup.gps</field>
<field name="model">mail.activity</field>
<field name="inherit_id" ref="mail.mail_activity_view_form_popup" />
<field name="arch" type="xml">
<xpath expr="//field[@name='date_deadline']" position="after">
<group string="GPS Location" name="gps_group" col="2" colspan="2">
<field name="gps_captured" invisible="1" />
<field name="gps_latitude" colspan="2" nolabel="1" style="width: 98% !important; min-width: 300px !important;" />
<field name="gps_longitude" colspan="2" nolabel="1" style="width: 98% !important; min-width: 300px !important;" />
<button name="gps_capture_btn" string="🗺️ Capture GPS" class="btn btn-primary btn-lg grt_capture_gps_btn o_activity_gps_btn" colspan="2" style="width: 98% !important; min-width: 300px !important; margin: 5px 0; padding: 8px;" />
<field name="gps_openstreetmap_url" widget="url" colspan="2" nolabel="1" style="width: 98% !important; min-width: 300px !important;" />
</group>
</xpath>
</field>
</record>
<record id="mail_activity_view_tree_gps" model="ir.ui.view">
<field name="name">mail.activity.tree.gps</field>
<field name="model">mail.activity</field>
<field name="inherit_id" ref="mail.mail_activity_view_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='date_deadline']" position="after">
<field name="gps_latitude" readonly="1" />
<field name="gps_longitude" readonly="1" />
<field name="gps_openstreetmap_url" widget="url" string="OpenStreetMap" readonly="1" />
</xpath>
</field>
</record>
</odoo>
@@ -5,9 +5,13 @@
<field name="model">res.users</field>
<field name="arch" type="xml">
<tree>
<button name="action_sync_team_business_category_access" type="object" string="Sync" class="btn-link"/>
<field name="name"/>
<field name="login"/>
<field name="active"/>
<field name="team_business_category_ids" widget="many2many_tags"/>
<field name="allowed_business_category_ids" widget="many2many_tags"/>
<field name="effective_business_category_ids" widget="many2many_tags"/>
<field name="default_business_category_id"/>
<field name="active_business_category_id"/>
</tree>
@@ -19,6 +23,14 @@
<field name="model">res.users</field>
<field name="arch" type="xml">
<form string="User Business Category Access">
<header>
<button
name="action_sync_team_business_category_access"
type="object"
string="Sync Team Access"
class="oe_highlight"
/>
</header>
<sheet>
<group>
<field name="name"/>
@@ -26,7 +38,9 @@
<field name="active"/>
</group>
<group string="Business Category Access">
<field name="team_business_category_ids" widget="many2many_tags" readonly="1"/>
<field name="allowed_business_category_ids" widget="many2many_tags"/>
<field name="effective_business_category_ids" widget="many2many_tags" readonly="1"/>
<field name="default_business_category_id"/>
<field name="active_business_category_id"/>
</group>
@@ -42,6 +56,15 @@
<field name="domain">[('share', '=', False)]</field>
</record>
<record id="action_sync_team_business_category_access_server" model="ir.actions.server">
<field name="name">Sync Team Category Access</field>
<field name="model_id" ref="base.model_res_users" />
<field name="binding_model_id" ref="base.model_res_users" />
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">records.action_sync_team_business_category_access()</field>
</record>
<menuitem id="menu_res_users_business_category_access"
name="User Category Access"
parent="crm.crm_menu_config"
+12 -4
View File
@@ -600,12 +600,17 @@ Content-Type: application/json
"method": "call",
"params": {
"limit": 10,
"offset": 0
"offset": 0,
"states": ["confirmed"]
}
}
```
**Response**: Same structure as `mo-detail` but returns array of MOs with states 'confirmed', 'progress', 'to_close'
`states` is optional:
- If omitted, default is `["confirmed"]` (recommended for middleware pickup queue).
- Can be string CSV: `"confirmed,progress"` or array: `["confirmed","progress"]`.
**Response**: Same structure as `mo-detail` but returns array of MOs filtered by `states`.
```json
{
@@ -687,7 +692,9 @@ Content-Type: application/json
- Each MO in the list includes full `equipment` object (MO-level equipment), `bom_components` array, and `components_consumption` array
- Each component in `components_consumption` includes full `equipment` object if linked to SCADA equipment via component move; null otherwise
- Equipment fields include: id, code, name, equipment_type, manufacturer, model_number, serial_number, ip_address, port, protocol, is_active, connection_status, sync_status, last_connected
- `to_consume`, `reserved`, and `consumed` quantities are based on raw material stock moves
- `to_consume` is based on BoM quantity for BoM-based components.
- `reserved` and `consumed` are aggregated from raw moves, then capped to BoM quantity for BoM-based components (to avoid over-reporting from split/extra moves).
- Non-BoM-only components (if any) use raw move quantities.
**JSON-RPC Example**:
```bash
@@ -699,7 +706,8 @@ curl -X POST http://localhost:8069/api/scada/mo-list-detailed \
"method": "call",
"params": {
"limit": 10,
"offset": 0
"offset": 0,
"states": "confirmed,progress"
}
}'
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
'name': 'SCADA for Odoo - Manufacturing Integration',
'version': '7.0.79',
'version': '7.0.80',
'category': 'manufacturing',
'license': 'LGPL-3',
'author': 'PT. Gagak Rimang Teknologi',
@@ -48,7 +48,19 @@ class ScadaMODetailedController(http.Controller):
except (ValueError, TypeError):
offset = 0
domain = [('state', 'in', ['confirmed', 'progress', 'to_close'])]
# Default behavior: only return new MO queue for middleware pickup.
# Optional override:
# - states: ['confirmed', 'progress'] or "confirmed,progress"
states = params.get('states')
if states:
if isinstance(states, str):
states = [s.strip() for s in states.split(',') if s.strip()]
elif not isinstance(states, (list, tuple)):
states = ['confirmed']
else:
states = ['confirmed']
domain = [('state', 'in', states)]
mos = request.env['mrp.production'].search(
domain,
limit=limit,
+10 -5
View File
@@ -12,11 +12,11 @@ echo.
if "%1"=="" (
echo CARA PAKAI:
echo install_module.bat nama_modul
echo install_module.bat nama_modul [nama_database]
echo.
echo CONTOH:
echo install_module.bat account_dynamic_reports
echo install_module.bat project_task_timer
echo install_module.bat account_dynamic_reports kanjabung_MRP
echo install_module.bat project_task_timer manukanjabung
echo.
echo Module yang tersedia di C:\addon14:
echo - account_dynamic_reports
@@ -31,7 +31,12 @@ set MODULE_NAME=%1
set ODOO_BIN=C:\odoo14c\server\odoo-bin
set PYTHON=c:\odoo14c\python\python.exe
set CONFIG=C:\addon14\odoo.conf
set DATABASE=manu14
if "%2"=="" (
set DATABASE=kanjabung_MRP
) else (
set DATABASE=%2
)
echo Installing module: %MODULE_NAME%
echo Database: %DATABASE%
@@ -48,7 +53,7 @@ echo.
echo Installing...
echo.
%PYTHON% %ODOO_BIN% --config=%CONFIG% -d %DATABASE% -i %MODULE_NAME% --stop-after-init
%PYTHON% %ODOO_BIN% --config=%CONFIG% -d %DATABASE% -u %MODULE_NAME% --stop-after-init
echo.
echo ============================================================================
+18502
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -5,7 +5,7 @@ echo ================================================================
echo.
echo Langkah yang akan dilakukan:
echo 1. Kill semua proses Odoo yang berjalan
echo 2. Start Odoo dengan upgrade module grt_scada
echo 2. Start Odoo dengan upgrade module grt_crm_business_category
echo.
pause
@@ -19,11 +19,11 @@ echo.
echo [2/2] Starting Odoo with module upgrade...
echo.
cd C:\odoo14c\server
start "Odoo Server" c:\odoo14c\python\python.exe odoo-bin -c C:\addon14\odoo.conf -d manukanjabung -u grt_scada --without-demo=all
start "Odoo Server" c:\odoo14c\python\python.exe odoo-bin -c C:\addon14\odoo.conf -d kanjabung_MRP -u grt_crm_business_category --without-demo=all
echo.
echo ================================================================
echo Odoo sedang starting dengan upgrade module grt_scada...
echo Odoo sedang starting dengan upgrade module grt_crm_business_category...
echo Tunggu sekitar 30-60 detik hingga Odoo fully loaded
echo ================================================================
echo.
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
import xmlrpc.client
url = 'http://localhost:8070'
db = 'kanjabung_MRP'
username = 'admin'
password = 'admin'
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
print(f"Authenticated as uid: {uid}")
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
# Search for module
module_ids = models.execute_kw(db, uid, password,
'ir.module.module', 'search',
[[['name', '=', 'grt_crm_business_category']]])
if module_ids:
print(f"Found module ID: {module_ids[0]}")
# Upgrade module
result = models.execute_kw(db, uid, password,
'ir.module.module', 'button_immediate_upgrade',
[module_ids])
print("Module upgrade initiated successfully")
print(f"Result: {result}")
else:
print("Module not found")