GPS dan Business Category
This commit is contained in:
Vendored
+1
-1
@@ -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,2 +1,2 @@
|
||||
from . import models
|
||||
|
||||
from . import controllers
|
||||
|
||||
@@ -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,
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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
@@ -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,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',
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user