From d8629653240e5f4287b329a89c599989b7ef596e Mon Sep 17 00:00:00 2001 From: Florian Gilbert Date: Wed, 16 Oct 2019 10:11:43 +0000 Subject: [PATCH] [IMP] account: Add tracking for account move line in account move chatter. Purpose: In V13, you can reset to draft a posted move if this move isn't in a hashed journal. But it's little bit dangerous and we want to track changes for some fields. It works for editing in editable list view and form view. Task ID: 2061399 --- addons/account/models/account_move.py | 95 +++++++++++++++++++++++++-- addons/mail/models/mail_thread.py | 27 ++++++-- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 748fb5b1586..d9cc34b9ff3 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -97,7 +97,7 @@ class AccountMove(models.Model): date = fields.Date(string='Date', required=True, index=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today) - ref = fields.Char(string='Reference', copy=False) + ref = fields.Char(string='Reference', copy=False, tracking=True) narration = fields.Text(string='Internal Note') state = fields.Selection(selection=[ ('draft', 'Draft'), @@ -2396,11 +2396,12 @@ class AccountMoveLine(models.Model): country_id = fields.Many2one(comodel_name='res.country', related='move_id.company_id.country_id') account_id = fields.Many2one('account.account', string='Account', index=True, ondelete="cascade", - domain=[('deprecated', '=', False)]) + domain=[('deprecated', '=', False)], + tracking=True) account_internal_type = fields.Selection(related='account_id.user_type_id.type', string="Internal Type", store=True, readonly=True) account_root_id = fields.Many2one(related='account_id.root_id', string="Account Root", store=True, readonly=True) sequence = fields.Integer(default=10) - name = fields.Char(string='Label') + name = fields.Char(string='Label', tracking=True) quantity = fields.Float(string='Quantity', default=1.0, digits='Product Unit of Measure', help="The optional quantity expressed by this line, eg: number of product sold. " @@ -2422,7 +2423,7 @@ class AccountMoveLine(models.Model): reconciled = fields.Boolean(compute='_amount_residual', store=True) blocked = fields.Boolean(string='No Follow-up', default=False, help="You can check this box to mark this journal item as a litigation with the associated partner") - date_maturity = fields.Date(string='Due Date', index=True, + date_maturity = fields.Date(string='Due Date', index=True, tracking=True, help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.") currency_id = fields.Many2one('res.currency', string='Currency') partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict') @@ -2456,7 +2457,7 @@ class AccountMoveLine(models.Model): string="Originator Tax Repartition Line", ondelete='restrict', readonly=True, help="Tax repartition line that caused the creation of this move line, if any") tag_ids = fields.Many2many(string="Tags", comodel_name='account.account.tag', ondelete='restrict', - help="Tags assigned to this line by the tax creating it, if any. It determines its impact on financial reports.") + help="Tags assigned to this line by the tax creating it, if any. It determines its impact on financial reports.", tracking=True) tax_audit = fields.Char(string="Tax Audit String", compute="_compute_tax_audit", store=True, help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.") @@ -3214,8 +3215,59 @@ class AccountMoveLine(models.Model): or (account_type != 'payable' and account_to_write.user_type_id.type == 'payable'): raise UserError(_("You can only set an account having the payable type on payment terms lines for vendor bill.")) + # Get all tracked fields (without related fields because these fields must be manage on their own model) + tracking_fields = [] + for value in vals: + field = self._fields[value] + if hasattr(field, 'related') and field.related: + continue # We don't want to track related field. + if hasattr(field, 'tracking') and field.tracking: + tracking_fields.append(value) + ref_fields = self.env['account.move.line'].fields_get(tracking_fields) + + # Get initial values for each line + move_initial_values = {} + for line in self.filtered(lambda l: l.move_id.name != '/'): # Only lines with posted once move. + for field in tracking_fields: + # Group initial values by move_id + if line.move_id.id not in move_initial_values: + move_initial_values[line.move_id.id] = {} + move_initial_values[line.move_id.id].update({field: line[field]}) + result = super(AccountMoveLine, self).write(vals) + # Create the dict for the message post + tracking_values = {} # Tracking values to write in the message post + for move_id, modified_lines in move_initial_values.items(): + tmp_move = {move_id: []} + for line in self.filtered(lambda l: l.move_id.id == move_id): + tracked_field = self.env['mail.thread'].static_message_track(line, ref_fields, modified_lines) # Return a tuple like (changed field, ORM command) + tmp = {'line_id': line.id} + if len(tracked_field[1]) > 0: + selected_field = tracked_field[1][0][2] # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)]) + tmp.update({ + **{'field_name': selected_field.get('field_desc')}, + **self._get_formated_values(selected_field) + }) + elif len(tracked_field[0]): + field_name = line._fields[tracked_field[0].pop()].string # Get the field name + tmp.update({ + 'error': True, + 'field_error': field_name + }) + else: + continue + tmp_move[move_id].append(tmp) + if len(tmp_move[move_id]) > 0: + tracking_values.update(tmp_move) + + # Write in the chatter. + for move in self.mapped('move_id'): + fields = tracking_values.get(move.id, []) + if len(fields) > 0: + msg = self._get_tracking_field_string(tracking_values.get(move.id)) + move.message_post(body=msg) # Write for each concerned move the message in the chatter + for line in self: if not line.move_id.is_invoice(include_receipts=True): continue @@ -3323,6 +3375,39 @@ class AccountMoveLine(models.Model): result.append((line.id, name)) return result + # ------------------------------------------------------------------------- + # TRACKING METHODS + # ------------------------------------------------------------------------- + + def _get_formated_values(self, tracked_field): + if tracked_field.get('field_type') in ('date', 'datetime'): + return { + 'old_value': format_date(self.env, fields.Datetime.from_string(tracked_field.get('old_value_datetime'))), + 'new_value': format_date(self.env, fields.Datetime.from_string(tracked_field.get('new_value_datetime'))), + } + elif tracked_field.get('field_type') in ('one2many', 'many2many', 'many2one'): + return { + 'old_value': tracked_field.get('old_value_char', ''), + 'new_value': tracked_field.get('new_value_char', '') + } + else: + return { + 'old_value': [val for key, val in tracked_field.items() if 'old_value' in key][0], # Get the first element because we create a list like ['Elem'] + 'new_value': [val for key, val in tracked_field.items() if 'new_value' in key][0], # Get the first element because we create a list like ['Elem'] + } + + def _get_tracking_field_string(self, fields): + ARROW_RIGHT = '' + msg = '' + return _(msg) + # ------------------------------------------------------------------------- # RECONCILIATION # ------------------------------------------------------------------------- diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 54a1a004a76..ac7442fa13e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -596,33 +596,46 @@ class MailThread(models.AbstractModel): self.message_post_with_template(template.id, **post_kwargs) return True - def _message_track(self, tracked_fields, initial): + @api.model + def static_message_track(self, record, tracked_fields, initial): """ For a given record, fields to check (tuple column name, column info) and initial values, return a structure that is a tuple containing : - a set of updated column names - - a list of ORM (0, 0, values) commands to create 'mail.tracking.value' """ - self.ensure_one() + - a list of ORM (0, 0, values) commands to create 'mail.tracking.value' + + This static method is usefull when you don't want to inherit from mail.thread but + you want to use '_message_track()' method. + """ + record.ensure_one() changes = set() # contains onchange tracked fields that changed tracking_value_ids = [] # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}} for col_name, col_info in tracked_fields.items(): initial_value = initial[col_name] - new_value = self[col_name] + new_value = record[col_name] if new_value != initial_value and (new_value or initial_value): # because browse null != False - tracking_sequence = getattr(self._fields[col_name], 'tracking', - getattr(self._fields[col_name], 'track_sequence', 100)) # backward compatibility with old parameter name + tracking_sequence = getattr(record._fields[col_name], 'tracking', + getattr(record._fields[col_name], 'track_sequence', 100)) # backward compatibility with old parameter name if tracking_sequence is True: tracking_sequence = 100 - tracking = self.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info, tracking_sequence, self._name) + tracking = record.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info, tracking_sequence, record._name) if tracking: tracking_value_ids.append([0, 0, tracking]) changes.add(col_name) return changes, tracking_value_ids + def _message_track(self, tracked_fields, initial): + """ For a given record, fields to check (tuple column name, column info) + and initial values, return a structure that is a tuple containing : + + - a set of updated column names + - a list of ORM (0, 0, values) commands to create 'mail.tracking.value' """ + return self.static_message_track(self, tracked_fields, initial) + def message_track(self, tracked_fields, initial_values): """ Track updated values. Comparing the initial and current values of the fields given in tracked_fields, it generates a message containing