diff --git a/shopfloor/__manifest__.py b/shopfloor/__manifest__.py
index f9e2462919..2d0d521699 100644
--- a/shopfloor/__manifest__.py
+++ b/shopfloor/__manifest__.py
@@ -37,12 +37,15 @@
"delivery",
],
"data": [
+ "data/ir_config_parameter_data.xml",
+ "data/ir_cron_data.xml",
"security/ir.model.access.csv",
"views/shopfloor_menu.xml",
"views/stock_picking_type.xml",
"views/stock_location.xml",
"views/stock_move_line.xml",
"views/shopfloor_profile_views.xml",
+ "views/shopfloor_log_views.xml",
"views/menus.xml",
],
"demo": [
diff --git a/shopfloor/data/ir_config_parameter_data.xml b/shopfloor/data/ir_config_parameter_data.xml
new file mode 100644
index 0000000000..2127ea5cbe
--- /dev/null
+++ b/shopfloor/data/ir_config_parameter_data.xml
@@ -0,0 +1,7 @@
+
+
+
+ shopfloor.log.retention.days
+ 30
+
+
diff --git a/shopfloor/data/ir_cron_data.xml b/shopfloor/data/ir_cron_data.xml
new file mode 100644
index 0000000000..7c5809584b
--- /dev/null
+++ b/shopfloor/data/ir_cron_data.xml
@@ -0,0 +1,15 @@
+
+
+
+ Auto-vacuum Shopfloor Logs
+
+
+
+ 1
+ days
+ -1
+
+ code
+ model.autovacuum()
+
+
diff --git a/shopfloor/models/__init__.py b/shopfloor/models/__init__.py
index 7f9b986965..1bfd3ece74 100644
--- a/shopfloor/models/__init__.py
+++ b/shopfloor/models/__init__.py
@@ -1,4 +1,5 @@
from . import shopfloor_menu
+from . import shopfloor_log
from . import stock_picking_type
from . import shopfloor_profile
from . import stock_inventory
diff --git a/shopfloor/models/shopfloor_log.py b/shopfloor/models/shopfloor_log.py
new file mode 100644
index 0000000000..a17e9a1432
--- /dev/null
+++ b/shopfloor/models/shopfloor_log.py
@@ -0,0 +1,58 @@
+import logging
+from datetime import datetime, timedelta
+
+from odoo import fields, models
+
+_logger = logging.getLogger(__name__)
+
+
+class ShopfloorLog(models.Model):
+ _name = "shopfloor.log"
+ _description = "Shopfloor Logging"
+ _order = "id desc"
+
+ DEFAULT_RETENTION = 30 # days
+
+ request_url = fields.Char(readonly=True, string="Request URL")
+ request_method = fields.Char(readonly=True)
+ params = fields.Text(readonly=True)
+ headers = fields.Text(readonly=True)
+ result = fields.Text(readonly=True)
+ error = fields.Text(readonly=True)
+ state = fields.Selection(
+ selection=[("success", "Success"), ("failed", "Failed")], readonly=True,
+ )
+
+ def _logs_retention_days(self):
+ retention = self.DEFAULT_RETENTION
+ param = (
+ self.env["ir.config_parameter"]
+ .sudo()
+ .get_param("shopfloor.log.retention.days")
+ )
+ if param:
+ try:
+ retention = int(param)
+ except ValueError:
+ _logger.exception(
+ "Could not convert System Parameter"
+ " 'shopfloor.log.retention.days' to integer,"
+ " reverting to the"
+ " default configuration."
+ )
+ return retention
+
+ def logging_active(self):
+ retention = self._logs_retention_days()
+ return retention > 0
+
+ def autovacuum(self):
+ """Delete logs which have exceeded their retention duration
+
+ Called from a cron.
+ """
+ deadline = datetime.now() - timedelta(days=self._logs_retention_days())
+ logs = self.search([("create_date", "<=", deadline)])
+ if logs:
+ logs.unlink()
+ return True
diff --git a/shopfloor/readme/CONFIGURE.rst b/shopfloor/readme/CONFIGURE.rst
index 8442c179fd..fe0cc8cdee 100644
--- a/shopfloor/readme/CONFIGURE.rst
+++ b/shopfloor/readme/CONFIGURE.rst
@@ -1 +1,17 @@
writeme
+
+Logs retention
+--------------
+
+Logs are kept in database for every REST requests made by a client application.
+They can be used for debugging and monitoring of the activity.
+
+The Logs menu is shown only with Developer tools (``?debug=1``) activated.
+
+By default, Shopfloor logs are kept 30 days.
+You can change the duration of the retention by changing the System Parameter
+``shopfloor.log.retention.days``.
+
+If the value is set to 0, the logs are not stored at all.
+
+Logged data is: request URL and method, parameters, headers, result or error.
diff --git a/shopfloor/security/ir.model.access.csv b/shopfloor/security/ir.model.access.csv
index 6aca8c733c..bdf728b8e5 100644
--- a/shopfloor/security/ir.model.access.csv
+++ b/shopfloor/security/ir.model.access.csv
@@ -1,5 +1,6 @@
"id","name","model_id/id","group_id/id","perm_read","perm_write","perm_create","perm_unlink"
-"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu",,1,0,0,0
+"access_shopfloor_menu_users","shopfloor menu","model_shopfloor_menu","stock.group_stock_user",1,0,0,0
"access_shopfloor_menu_stock_manager","shopfloor menu inventory manager","model_shopfloor_menu","stock.group_stock_manager",1,1,1,1
-"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile",,1,0,0,0
+"access_shopfloor_profile_users","shopfloor profile","model_shopfloor_profile","stock.group_stock_user",1,0,0,0
"access_shopfloor_profile_stock_manager","shopfloor profile inventory manager","model_shopfloor_profile","stock.group_stock_manager",1,1,1,1
+"access_shopfloor_log","access_shopfloor_log","model_shopfloor_log","stock.group_stock_manager",1,0,0,0
diff --git a/shopfloor/services/service.py b/shopfloor/services/service.py
index 3841079f56..51465f1f16 100644
--- a/shopfloor/services/service.py
+++ b/shopfloor/services/service.py
@@ -1,5 +1,6 @@
-from odoo import _, exceptions
+from odoo import _, exceptions, registry
from odoo.exceptions import MissingError
+from odoo.http import request
from odoo.osv import expression
from odoo.addons.base_rest.controllers.main import _PseudoCollection
@@ -22,6 +23,63 @@ class BaseShopfloorService(AbstractComponent):
_collection = "shopfloor.service"
_actions_collection_name = "shopfloor.action"
_expose_model = None
+ # can be overridden to disable logging of requests to DB
+ _log_calls_in_db = True
+
+ def dispatch(self, method_name, _id=None, params=None):
+ if not self._db_logging_active():
+ return super().dispatch(method_name, _id=_id, params=params)
+ return self._dispatch_with_db_logging(method_name, _id=_id, params=params)
+
+ def _db_logging_active(self):
+ return (
+ request
+ and self._log_calls_in_db
+ and self.env["shopfloor.log"].logging_active()
+ )
+
+ # TODO logging to DB should be an extra module for base_rest
+ def _dispatch_with_db_logging(self, method_name, _id=None, params=None):
+ try:
+ result = super().dispatch(method_name, _id=_id, params=params)
+ except Exception as err:
+ self.env.cr.rollback()
+ with registry(self.env.cr.dbname).cursor() as cr:
+ env = self.env(cr=cr)
+ self._log_call_in_db(env, request, _id, params, error=err)
+ raise
+ self._log_call_in_db(self.env, request, _id, params, result=result)
+ return result
+
+ @property
+ def _log_call_header_strip(self):
+ return ("Cookie", "Api-Key")
+
+ def _log_call_in_db_values(self, _request, _id, params, result=None, error=None):
+ httprequest = _request.httprequest
+ headers = dict(httprequest.headers)
+ for header_key in self._log_call_header_strip:
+ if header_key in headers:
+ headers[header_key] = ""
+ if _id:
+ params = dict(params, _id=_id)
+ return {
+ "request_url": httprequest.url,
+ "request_method": httprequest.method,
+ "params": params,
+ "headers": headers,
+ "result": result,
+ "error": error,
+ "state": "success" if result else "failed",
+ }
+
+ def _log_call_in_db(self, env, _request, _id, params, result=None, error=None):
+ values = self._log_call_in_db_values(
+ _request, _id, params, result=result, error=error
+ )
+ if not values:
+ return
+ env["shopfloor.log"].sudo().create(values)
def _get(self, _id):
domain = expression.normalize_domain(self._get_base_search_domain())
diff --git a/shopfloor/views/menus.xml b/shopfloor/views/menus.xml
index 29876a3a77..f1a619ed6b 100644
--- a/shopfloor/views/menus.xml
+++ b/shopfloor/views/menus.xml
@@ -18,4 +18,11 @@
parent="menu_shopfloor_settings"
sequence="20"
/>
+
diff --git a/shopfloor/views/shopfloor_log_views.xml b/shopfloor/views/shopfloor_log_views.xml
new file mode 100644
index 0000000000..6ebb576f7c
--- /dev/null
+++ b/shopfloor/views/shopfloor_log_views.xml
@@ -0,0 +1,125 @@
+
+
+
+ shopfloor.log tree
+ shopfloor.log
+
+
+
+
+
+
+
+
+
+
+
+ shopfloor.log form
+ shopfloor.log
+
+
+
+
+
+ shopfloor.log search
+ shopfloor.log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shopfloor Logs
+ shopfloor.log
+ ir.actions.act_window
+ tree,form
+
+