File: account_lock_exception.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (326 lines) | stat: -rw-r--r-- 13,499 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
from odoo import _, api, fields, models, Command
from odoo.osv import expression
from odoo.tools import create_index
from odoo.tools.misc import format_datetime
from odoo.exceptions import UserError, ValidationError

from odoo.addons.account.models.company import SOFT_LOCK_DATE_FIELDS

from datetime import date


class AccountLockException(models.Model):
    _name = "account.lock_exception"
    _description = "Account Lock Exception"

    active = fields.Boolean(
        string='Active',
        default=True,
    )
    state = fields.Selection(
        selection=[
            ('active', 'Active'),
            ('revoked', 'Revoked'),
            ('expired', 'Expired'),
        ],
        string="State",
        compute='_compute_state',
        search='_search_state'
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        required=True,
        readonly=True,
        default=lambda self: self.env.company,
    )
    # An exception w/o user_id is an exception for everyone
    user_id = fields.Many2one(
        'res.users',
        string='User',
        default=lambda self: self.env.user,
    )
    reason = fields.Char(
        string='Reason',
    )
    # An exception without `end_datetime` is valid forever
    end_datetime = fields.Datetime(
        string='End Date',
    )

    # The changed lock date
    lock_date_field = fields.Selection(
        selection=[
            ('fiscalyear_lock_date', 'Global Lock Date'),
            ('tax_lock_date', 'Tax Return Lock Date'),
            ('sale_lock_date', 'Sales Lock Date'),
            ('purchase_lock_date', 'Purchase Lock Date'),
        ],
        string="Lock Date Field",
        required=True,
        help="Technical field identifying the changed lock date",
    )
    lock_date = fields.Date(
        string="Changed Lock Date",
        help="Technical field giving the date the lock date was changed to.",
    )
    company_lock_date = fields.Date(
        string="Original Lock Date",
        copy=False,
        help="Technical field giving the date the company lock date at the time the exception was created.",
    )

    # (Non-stored) computed lock date fields; c.f. res.company
    fiscalyear_lock_date = fields.Date(
        string="Global Lock Date",
        compute="_compute_lock_dates",
        search="_search_fiscalyear_lock_date",
        help="The date the Global Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
    )
    tax_lock_date = fields.Date(
        string="Tax Return Lock Date",
        compute="_compute_lock_dates",
        search="_search_tax_lock_date",
        help="The date the Tax Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
    )
    sale_lock_date = fields.Date(
        string='Sales Lock Date',
        compute="_compute_lock_dates",
        search="_search_sale_lock_date",
        help="The date the Sale Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
    )
    purchase_lock_date = fields.Date(
        string='Purchase Lock Date',
        compute="_compute_lock_dates",
        search="_search_purchase_lock_date",
        help="The date the Purchase Lock Date is set to by this exception. If the lock date is not changed it is set to the maximal date.",
    )

    def init(self):
        super().init()
        create_index(
            self.env.cr,
            indexname='account_lock_exception_company_id_end_datetime_idx',
            tablename=self._table,
            expressions=['company_id', 'user_id', 'end_datetime'],
            where="active = TRUE"
        )

    def _compute_display_name(self):
        for record in self:
            record.display_name = _("Lock Date Exception %s", record.id)

    @api.depends('active', 'end_datetime')
    def _compute_state(self):
        for record in self:
            if not record.active:
                record.state = 'revoked'
            elif record.end_datetime and record.end_datetime < self.env.cr.now():
                record.state = 'expired'
            else:
                record.state = 'active'

    @api.depends('lock_date_field', 'lock_date')
    def _compute_lock_dates(self):
        for exception in self:
            for field in SOFT_LOCK_DATE_FIELDS:
                if field == exception.lock_date_field:
                    exception[field] = exception.lock_date
                else:
                    exception[field] = date.max

    def _search_state(self, operator, value):
        if operator not in ['=', '!='] or value not in ['revoked', 'expired', 'active']:
            raise UserError(_('Operation not supported'))

        normal_domain_for_equals = []
        if value == 'revoked':
            normal_domain_for_equals = [
                ('active', '=', False),
            ]
        elif value == 'expired':
            normal_domain_for_equals = [
                '&',
                    ('active', '=', True),
                    ('end_datetime', '<', self.env.cr.now()),
            ]
        elif value == 'active':
            normal_domain_for_equals = [
                '&',
                    ('active', '=', True),
                    '|',
                        ('end_datetime', '=', None),
                        ('end_datetime', '>=', self.env.cr.now()),
            ]
        if operator == '=':
            return normal_domain_for_equals
        else:
            return ['!'] + normal_domain_for_equals

    def _search_lock_date(self, field, operator, value):
        if operator not in ['<', '<='] or not value:
            raise UserError(_('Operation not supported'))
        return ['&',
                  ('lock_date_field', '=', field),
                  '|',
                      ('lock_date', '=', False),
                      ('lock_date', operator, value),
               ]

    def _search_fiscalyear_lock_date(self, operator, value):
        return self._search_lock_date('fiscalyear_lock_date', operator, value)

    def _search_tax_lock_date(self, operator, value):
        return self._search_lock_date('tax_lock_date', operator, value)

    def _search_sale_lock_date(self, operator, value):
        return self._search_lock_date('sale_lock_date', operator, value)

    def _search_purchase_lock_date(self, operator, value):
        return self._search_lock_date('purchase_lock_date', operator, value)

    def _invalidate_affected_user_lock_dates(self):
        affected_lock_date_fields = {exception.lock_date_field for exception in self}
        self.env['res.company'].invalidate_model(
            fnames=[f'user_{field}' for field in list(affected_lock_date_fields)],
        )

    @api.model_create_multi
    def create(self, vals_list):
        # Preprocess arguments:
        # 1. Parse lock date arguments
        #   E.g. to create an exception for 'fiscalyear_lock_date' to '2024-01-01' put
        #   {'fiscalyear_lock_date': '2024-01-01'} in the create vals.
        #   The same thing works for all other fields in SOFT_LOCK_DATE_FIELDS.
        # 2. Fetch company lock date
        for vals in vals_list:
            if 'lock_date' not in vals or 'lock_date_field' not in vals:
                # Use vals[field] (for field in SOFT_LOCK_DATE_FIELDS) to init the data
                changed_fields = [field for field in SOFT_LOCK_DATE_FIELDS if field in vals]
                if len(changed_fields) != 1:
                    raise ValidationError(_("A single exception must change exactly one lock date field."))
                field = changed_fields[0]
                vals['lock_date_field'] = field
                vals['lock_date'] = vals.pop(field)
            company = self.env['res.company'].browse(vals.get('company_id', self.env.company.id))
            if 'company_lock_date' not in vals:
                vals['company_lock_date'] = company[vals['lock_date_field']]

        exceptions = super().create(vals_list)

        # Log the creation of the exception and the changed field on the company chatter
        for exception in exceptions:
            company = exception.company_id

            # Create tracking values to display the lock date change in the chatter
            field = exception.lock_date_field
            value = exception.lock_date
            field_info = exception.fields_get([field])[field]
            tracking_values = self.env['mail.tracking.value']._create_tracking_values(
                company[field], value, field, field_info, exception,
            )
            tracking_value_ids = [Command.create(tracking_values)]

            # In case there is no explicit end datetime "forever" is implied by not mentioning an end datetime
            end_datetime_string = _(" valid until %s", format_datetime(self.env, exception.end_datetime)) if exception.end_datetime else ""
            reason_string = _(" for '%s'", exception.reason) if exception.reason else ""
            company_chatter_message = _(
                "%(exception)s for %(user)s%(end_datetime_string)s%(reason)s.",
                exception=exception._get_html_link(title=_("Exception")),
                user=exception.user_id.display_name if exception.user_id else _("everyone"),
                end_datetime_string=end_datetime_string,
                reason=reason_string,
            )
            company.sudo().message_post(
                body=company_chatter_message,
                tracking_value_ids=tracking_value_ids,
            )

        exceptions._invalidate_affected_user_lock_dates()
        return exceptions

    def copy(self, default=None):
        raise UserError(_('You cannot duplicate a Lock Date Exception.'))

    def _recreate(self):
        """
        1. Copy all exceptions in self but update the company lock date.
        2. Revoke all exceptions in self.
        3. Return the new records from step 1.
        """
        if not self:
            return self.env['account.lock_exception']
        vals_list = self.with_context(active_test=False).copy_data()
        new_records = self.create(vals_list)
        self.sudo().action_revoke()
        return new_records

    def action_revoke(self):
        """Revokes an active exception."""
        if not self.env.user.has_group('account.group_account_manager'):
            raise UserError(_("You cannot revoke Lock Date Exceptions. Ask someone with the 'Adviser' role."))
        for record in self:
            if record.state == 'active':
                record_sudo = record.sudo()
                record_sudo.active = False
                record_sudo.end_datetime = fields.Datetime.now()
                record._invalidate_affected_user_lock_dates()

    @api.model
    def _get_active_exceptions_domain(self, company, soft_lock_date_fields):
        return [
            *expression.OR([(field, '<', company[field])] for field in soft_lock_date_fields if company[field]),
            ('company_id', '=', company.id),
            ('state', '=', 'active'),  # checks the datetime
        ]

    def _get_audit_trail_during_exception_domain(self):
        self.ensure_one()

        common_message_domain = [
            ('date', '>=', self.create_date),
        ]
        if self.user_id:
            common_message_domain.append(('create_uid', '=', self.user_id.id))
        if self.end_datetime:
            common_message_domain.append(('date', '<=', self.end_datetime))

        # Add restrictions on the accounting date to avoid unnecessary entries
        min_date = self.lock_date
        max_date = self.company_lock_date
        move_date_domain = []
        tracking_old_datetime_domain = []
        tracking_new_datetime_domain = []
        if min_date:
            move_date_domain.append([('date', '>=', min_date)])
            tracking_old_datetime_domain.append([('tracking_value_ids.old_value_datetime', '>=', min_date)])
            tracking_new_datetime_domain.append([('tracking_value_ids.new_value_datetime', '>=', min_date)])
        if max_date:
            move_date_domain.append([('date', '<=', max_date)])
            tracking_old_datetime_domain.append([('tracking_value_ids.old_value_datetime', '<=', max_date)])
            tracking_new_datetime_domain.append([('tracking_value_ids.new_value_datetime', '<=', max_date)])

        return [
            ('company_id', 'child_of', self.company_id.id),
            ('audit_trail_message_ids', 'any', common_message_domain),
            '|',
                # The date was changed from or to a value inside the excepted period
                ('audit_trail_message_ids', 'any', [
                    ('tracking_value_ids.field_id', '=', self.env['ir.model.fields']._get('account.move', 'date').id),
                    '|',
                        *expression.AND(tracking_old_datetime_domain),
                        *expression.AND(tracking_new_datetime_domain),
                ]),
                # The date of the move is inside the excepted period and sth. was changed on the move
                *expression.AND(move_date_domain),
        ]

    def action_show_audit_trail_during_exception(self):
        self.ensure_one()
        return {
            'name': _("Journal Items"),
            'type': 'ir.actions.act_window',
            'res_model': 'account.move.line',
            'view_mode': 'list,form',
            'domain': [('move_id', 'any', self._get_audit_trail_during_exception_domain())],
       }