# Copyright 2015-2017 Camptocamp SA # Copyright 2020 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models class RecordChangeset(models.Model): _name = "record.changeset" _description = "Record Changeset" _order = "date desc" _rec_name = "date" model = fields.Char(index=True, required=True, readonly=True) res_id = fields.Many2oneReference( string="Record ID", index=True, required=True, readonly=True, model_field="model", ) change_ids = fields.One2many( comodel_name="record.changeset.change", inverse_name="changeset_id", string="Changes", readonly=True, ) date = fields.Datetime( string="Modified on", default=fields.Datetime.now(), index=True, readonly=True ) modified_by_id = fields.Many2one( "res.users", default=lambda self: self.env.user, readonly=True ) state = fields.Selection( compute="_compute_state", selection=[("draft", "Pending"), ("done", "Done")], store=True, ) note = fields.Text() source = fields.Reference( string="Source of the change", selection="_reference_models", readonly=True ) company_id = fields.Many2one("res.company") record_id = fields.Reference( selection="_reference_models", compute="_compute_resource_record", readonly=True ) @api.depends("model", "res_id") def _compute_resource_record(self): for changeset in self: changeset.record_id = "{},{}".format(changeset.model, changeset.res_id or 0) @api.model def _reference_models(self): models = self.env["ir.model"].sudo().search([]) return [(model.model, model.name) for model in models] @api.depends("change_ids", "change_ids.state") def _compute_state(self): for rec in self: changes = rec.mapped("change_ids") if all(change.state in ("done", "cancel") for change in changes): rec.state = "done" else: rec.state = "draft" def name_get(self): result = [] for changeset in self: name = "{} ({})".format(changeset.date, changeset.record_id.display_name) result.append((changeset.id, name)) return result def apply(self): self.with_context(skip_pending_status_check=True).mapped("change_ids").apply() def cancel(self): self.with_context(skip_pending_status_check=True).mapped("change_ids").cancel() @api.model def add_changeset(self, record, values, create=False): """Add a changeset on a record By default, when a record is modified by a user or by the system, the the changeset will follow the rules configured for the global rules. A caller should pass the following keys in the context: * ``__changeset_rules_source_model``: name of the model which asks for the change * ``__changeset_rules_source_id``: id of the record which asks for the change When the source model and id are not defined, the current user is considered as the origin of the change. Should be called before the execution of ``write`` on the record so we can keep track of the existing value and also because the returned values should be used for ``write`` as some of the values may have been removed. :param values: the values being written on the record :type values: dict :param create: in create mode, no check is made to see if the field value consitutes a change. :type creatie: boolean :returns: dict of values that should be wrote on the record (fields with a 'Validate' or 'Never' rule are excluded) """ record.ensure_one() source_model = self.env.context.get("__changeset_rules_source_model") source_id = self.env.context.get("__changeset_rules_source_id") if not source_model: # if the changes source is not defined, log the user who # made the change source_model = "res.users" if not source_id: source_id = self.env.uid if source_model and source_id: source = "{},{}".format(source_model, source_id) else: source = False change_model = self.env["record.changeset.change"] write_values = values.copy() changes = [] rules = self.env["changeset.field.rule"].get_rules( source_model_name=source_model, record_model_name=record._name ) for field in values: rule = rules.get(field) if ( not rule or not rule._evaluate_expression(record) or (create and not values[field]) ): continue if field in values: if not create and not change_model._has_field_changed( record, field, values[field] ): continue change, pop_value = change_model._prepare_changeset_change( record, rule, field, values[field], create=create, ) if pop_value: write_values.pop(field) if create: # overwrite with null value for new records write_values[field] = ( # but create some default for required text fields record._fields[field].required and record._fields[field].type in ("char", "text") and "/" or record._fields[field].null(record) ) changes.append(change) if changes: changeset_vals = self._prepare_changeset_vals(changes, record, source) self.env["record.changeset"].create(changeset_vals) return write_values @api.model def _prepare_changeset_vals(self, changes, record, source): has_company = "company_id" in self.env[record._name]._fields has_company = has_company and record.company_id company = record.company_id if has_company else self.env.company return { # newly created records are passed as newid records with the id in ref "res_id": record.id or record.id.ref, "model": record._name, "company_id": company.id, "change_ids": [(0, 0, vals) for vals in changes], "date": fields.Datetime.now(), "source": source, }