From 5aeff66acd215e3f2d874a7112712e917c6ffe7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 6 Apr 2025 00:14:17 +0200 Subject: [PATCH] Use ModelStorage hooks instead of extending create/write/delete --- modules/account/account.py | 26 +- modules/account/company.py | 27 +- modules/account/fiscalyear.py | 32 +-- modules/account/journal.py | 82 +++---- modules/account/message.xml | 3 - modules/account/move.py | 231 +++++++----------- modules/account/period.py | 72 +++--- modules/account/tax.py | 35 +-- modules/account_asset/asset.py | 16 +- modules/account_consolidation/account.py | 2 +- modules/account_es_sii/account.py | 43 ++-- modules/account_export/account.py | 20 +- modules/account_fr_chorus/account.py | 27 +- modules/account_invoice/account.py | 21 +- modules/account_invoice/invoice.py | 158 ++++-------- modules/account_invoice_defer/account.py | 18 +- modules/account_invoice_stock/account.py | 16 +- modules/account_payment/payment.py | 108 ++++---- modules/account_payment_braintree/account.py | 29 ++- modules/account_payment_braintree/party.py | 29 ++- modules/account_payment_clearing/account.py | 13 +- modules/account_payment_clearing/payment.py | 3 +- modules/account_payment_sepa/payment.py | 56 ++--- modules/account_payment_stripe/party.py | 29 ++- modules/account_payment_stripe/payment.py | 69 +++--- modules/account_statement/statement.py | 60 ++--- modules/account_stock_anglo_saxon/stock.py | 19 +- modules/account_stock_eu/account_stock_eu.py | 30 +-- modules/account_stock_eu/stock.py | 26 +- modules/account_stock_landed_cost/account.py | 40 +-- .../account_stock_shipment_cost/account.py | 40 +-- modules/account_tax_cash/account.py | 14 +- modules/analytic_account/account.py | 33 +-- modules/analytic_account/line.py | 36 ++- modules/attendance/attendance.py | 8 +- modules/bank/bank.py | 18 +- modules/carrier/carrier.py | 36 +-- modules/carrier_carriage/stock.py | 24 +- modules/company/company.py | 32 +-- modules/company/res.py | 47 +--- modules/country/country.py | 47 +--- modules/currency_rs/currency.py | 25 +- modules/customs/product.py | 44 ++-- modules/dashboard/res.py | 13 +- modules/document_incoming_ocr/document.py | 15 +- .../document_incoming_ocr_typless/document.py | 23 +- modules/incoterm/incoterm.py | 19 +- modules/ldap_authentication/res.py | 27 +- .../marketing_automation.py | 8 +- modules/marketing_campaign/marketing.py | 22 +- modules/marketing_email/marketing.py | 26 +- modules/party/address.py | 55 ++--- modules/party/configuration.py | 15 +- modules/party/contact_mechanism.py | 23 +- modules/party/party.py | 17 +- modules/party_relationship/party.py | 45 ++-- modules/product/category.py | 13 +- modules/product/product.py | 55 ++--- modules/product/uom.py | 49 ++-- modules/product_cost_fifo/move.py | 16 +- modules/product_image/product.py | 26 +- modules/product_price_list_cache/product.py | 14 +- modules/production/bom.py | 38 ++- modules/production/production.py | 21 +- modules/production/stock.py | 17 +- modules/production_work/work.py | 33 ++- modules/production_work_timesheet/work.py | 25 +- modules/project/work.py | 38 +-- modules/project_invoice/invoice.py | 18 +- modules/project_invoice/project.py | 23 +- modules/project_invoice/timesheet.py | 25 +- modules/project_plan/allocation.py | 42 ++-- modules/project_plan/work.py | 108 ++++---- modules/purchase/invoice.py | 11 +- modules/purchase/product.py | 12 +- modules/purchase/purchase.py | 54 ++-- modules/purchase/stock.py | 9 +- modules/purchase_request/purchase.py | 39 ++- modules/purchase_request/purchase_request.py | 26 +- .../purchase_request_quotation/purchase.py | 14 +- modules/purchase_requisition/purchase.py | 43 ++-- modules/quality/quality.py | 61 ++--- modules/sale/invoice.py | 11 +- modules/sale/sale.py | 54 ++-- modules/sale/stock.py | 10 +- modules/sale_complaint/complaint.py | 55 +++-- modules/sale_opportunity/opportunity.py | 44 ++-- modules/sale_opportunity/sale.py | 37 ++- modules/sale_payment/account.py | 22 +- modules/sale_point/sale.py | 143 ++++++----- modules/sale_promotion_coupon/sale.py | 12 +- modules/sale_subscription/subscription.py | 24 +- modules/sale_supply/purchase.py | 5 +- modules/sale_supply_drop_shipment/message.xml | 3 - modules/sale_supply_drop_shipment/stock.py | 45 +--- modules/sale_supply_production/production.py | 7 +- modules/stock/inventory.py | 75 +++--- modules/stock/location.py | 49 ++-- modules/stock/message.xml | 3 - modules/stock/move.py | 92 +++---- modules/stock/product.py | 189 +++++++------- modules/stock/shipment.py | 170 ++++--------- modules/stock/tests/test_module.py | 30 ++- modules/stock_forecast/forecast.py | 18 +- modules/stock_location_move/stock.py | 3 +- modules/stock_lot/stock.py | 82 +++---- modules/stock_lot_sled/stock.py | 15 +- modules/stock_package/stock.py | 22 +- modules/stock_package_shipping_dpd/carrier.py | 24 +- .../stock_package_shipping_mygls/carrier.py | 26 +- .../carrier.py | 26 +- modules/stock_package_shipping_ups/carrier.py | 26 +- modules/timesheet_cost/company.py | 20 +- modules/timesheet_cost/timesheet.py | 11 +- modules/user_role/res.py | 29 +-- modules/web_shop/web.py | 25 +- modules/web_shop_shopify/common.py | 24 +- modules/web_shop_shopify/product.py | 83 ++----- modules/web_shop_shopify/web.py | 27 +- modules/web_user/user.py | 31 +-- trytond/trytond/ir/action.py | 173 ++++--------- trytond/trytond/ir/avatar.py | 18 +- trytond/trytond/ir/configuration.py | 15 +- trytond/trytond/ir/email_.py | 16 +- trytond/trytond/ir/export.py | 13 +- trytond/trytond/ir/lang.py | 56 ++--- trytond/trytond/ir/message.py | 34 +-- trytond/trytond/ir/model.py | 125 ++-------- trytond/trytond/ir/module.py | 22 +- trytond/trytond/ir/resource.py | 8 +- trytond/trytond/ir/rule.py | 40 +-- trytond/trytond/ir/session.py | 18 +- trytond/trytond/ir/translation.py | 26 +- trytond/trytond/ir/trigger.py | 18 +- trytond/trytond/ir/ui/icon.py | 15 +- trytond/trytond/ir/ui/view.py | 50 +--- trytond/trytond/model/avatar.py | 14 +- trytond/trytond/model/dictschema.py | 15 +- trytond/trytond/model/digits.py | 7 +- trytond/trytond/model/fields/fmany2one.py | 22 +- trytond/trytond/res/group.py | 8 +- trytond/trytond/res/user.py | 126 ++++------ 142 files changed, 2104 insertions(+), 3189 deletions(-) diff --git a/modules/account/account.py b/modules/account/account.py index 27c2310222..6964ef77f5 100644 --- a/modules/account/account.py +++ b/modules/account/account.py @@ -214,8 +214,8 @@ class Type( ModelSQL, ModelView): __name__ = 'account.account.type' - parent = fields.Many2One('account.account.type', 'Parent', - ondelete="RESTRICT", + parent = fields.Many2One( + 'account.account.type', 'Parent', ondelete='CASCADE', states={ 'readonly': (Bool(Eval('template', -1)) & ~Eval('template_override', False)), @@ -370,13 +370,6 @@ class Type( default.setdefault('template', None) return super().copy(types, default=default) - @classmethod - def delete(cls, types): - types = cls.search([ - ('parent', 'child_of', [t.id for t in types]), - ]) - super().delete(types) - def update_type(self, template2type=None): ''' Update recursively types based on template. @@ -1280,21 +1273,6 @@ class Account( cls._rebuild_tree('parent', None, 0) return new_accounts - @classmethod - def delete(cls, accounts): - MoveLine = Pool().get('account.move.line') - childs = cls.search([ - ('parent', 'child_of', [a.id for a in accounts]), - ]) - lines = MoveLine.search([ - ('account', 'in', [a.id for a in childs]), - ]) - if lines: - raise AccessError( - gettext('account.msg_delete_account_with_move_lines', - account=lines[0].account.rec_name)) - super().delete(accounts) - def update_account(self, template2account=None, template2type=None): ''' Update recursively accounts based on template. diff --git a/modules/account/company.py b/modules/account/company.py index b4a2dc2f87..88ac83dfe0 100644 --- a/modules/account/company.py +++ b/modules/account/company.py @@ -133,20 +133,17 @@ class Company(metaclass=PoolMeta): return [('id', 'in', query)] @classmethod - def write(cls, *args): + def check_modification(cls, mode, companies, values=None, external=False): pool = Pool() Move = pool.get('account.move') - transaction = Transaction() - if transaction.user and transaction.check_access: - actions = iter(args) - for companies, values in zip(actions, actions): - if 'currency' in values: - moves = Move.search([ - ('company', 'in', [c.id for c in companies]), - ], - limit=1, order=[]) - if moves: - raise AccessError(gettext( - 'account.msg_company_change_currency')) - - super().write(*args) + super().check_modification( + mode, companies, values=values, external=external) + if mode == 'write': + if 'currency' in values: + moves = Move.search([ + ('company', 'in', [c.id for c in companies]), + ], + limit=1, order=[]) + if moves: + raise AccessError(gettext( + 'account.msg_company_change_currency')) diff --git a/modules/account/fiscalyear.py b/modules/account/fiscalyear.py index 9edb656d7b..438b842800 100644 --- a/modules/account/fiscalyear.py +++ b/modules/account/fiscalyear.py @@ -204,38 +204,28 @@ class FiscalYear(Workflow, ModelSQL, ModelView): Period.check_fiscalyear_dates(periods, field_names={'fiscalyear'}) @classmethod - def create(cls, vlist): - fiscalyears = super().create(vlist) - cls._find_cache.clear() - return fiscalyears - - @classmethod - def write(cls, *args): + def check_modification( + cls, mode, fiscalyears, values=None, external=False): pool = Pool() Move = pool.get('account.move') - actions = iter(args) - for fiscalyears, values in zip(actions, actions): - if values.get('move_sequence'): - for fiscalyear in fiscalyears: - if (fiscalyear.move_sequence - and fiscalyear.move_sequence.id - != values['move_sequence']): + super().check_modification( + mode, fiscalyears, values=values, external=external) + if mode == 'write' and 'move_sequence' in values: + for fiscalyear in fiscalyears: + if sequence := fiscalyear.move_sequence: + if sequence.id != values['move_sequence']: if Move.search([ ('period.fiscalyear', '=', fiscalyear.id), ('state', '=', 'posted'), - ]): + ], limit=1): raise AccessError( gettext('account.' 'msg_change_fiscalyear_move_sequence', fiscalyear=fiscalyear.rec_name)) - super().write(*args) - cls._find_cache.clear() @classmethod - def delete(cls, fiscalyears): - Period = Pool().get('account.period') - Period.delete([p for f in fiscalyears for p in f.periods]) - super().delete(fiscalyears) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._find_cache.clear() @classmethod diff --git a/modules/account/journal.py b/modules/account/journal.py index ce099950cb..133f93c367 100644 --- a/modules/account/journal.py +++ b/modules/account/journal.py @@ -147,23 +147,22 @@ class Journal( return journal @classmethod - def write(cls, *args): + def check_modification(cls, mode, journals, values=None, external=False): pool = Pool() Move = pool.get('account.move') - actions = iter(args) - for journals, values in zip(actions, actions): - if 'type' in values: - for sub_journals in grouped_slice(journals): - moves = Move.search([ - ('journal', 'in', [j.id for j in sub_journals]), - ('state', '=', 'posted') - ], order=[], limit=1) - if moves: - move, = moves - raise AccessError(gettext( - 'account.msg_journal_account_moves', - journal=move.journal.rec_name)) - super().write(*args) + super().check_modification( + mode, journals, values=values, external=external) + if mode == 'write' and 'type' in values: + for sub_journals in grouped_slice(journals): + moves = Move.search([ + ('journal', 'in', [j.id for j in sub_journals]), + ('state', '=', 'posted') + ], order=[], limit=1) + if moves: + move, = moves + raise AccessError(gettext( + 'account.msg_journal_account_moves', + journal=move.journal.rec_name)) class JournalCashContext(ModelView): @@ -275,39 +274,26 @@ class JournalPeriod(Workflow, ModelSQL, ModelView): journal_period=period.rec_name)) @classmethod - def create(cls, vlist): - Period = Pool().get('account.period') - for vals in vlist: - if vals.get('period'): - period = Period(vals['period']) - if period.state != 'open': - raise AccessError( - gettext('account' - '.msg_create_journal_period_closed_period', - period=period.rec_name)) - return super().create(vlist) - - @classmethod - def write(cls, *args): - actions = iter(args) - for journal_periods, values in zip(actions, actions): - if (values != {'state': 'closed'} - and values != {'state': 'open'}): - cls._check(journal_periods) - if values.get('state') == 'open': - for journal_period in journal_periods: - if journal_period.period.state != 'open': - raise AccessError( - gettext('account' - '.msg_open_journal_period_closed_period', - journal_period=journal_period.rec_name, - period=journal_period.period.rec_name)) - super().write(*args) - - @classmethod - def delete(cls, periods): - cls._check(periods) - super().delete(periods) + def check_modification(cls, mode, records, values=None, external=False): + super().check_modification( + mode, records, values=values, external=external) + if mode == 'create': + for record in records: + if record.period.state != 'open': + raise AccessError(gettext( + 'account.msg_create_journal_period_closed_period', + period=record.period.rec_name)) + elif mode in {'write', 'delete'}: + if values != {'state': 'closed'} and values != {'state': 'open'}: + cls._check(records) + if mode == 'write' and values.get('state') == 'open': + for record in records: + if record.period.state != 'open': + raise AccessError(gettext( + 'account.' + 'msg_open_journal_period_closed_period', + journal_period=record.rec_name, + period=record.period.rec_name)) @classmethod @ModelView.button diff --git a/modules/account/message.xml b/modules/account/message.xml index fa4c43fef3..e2ebcb0ad4 100644 --- a/modules/account/message.xml +++ b/modules/account/message.xml @@ -95,9 +95,6 @@ this repository contains the full copyright notices and license terms. --> Account can have only one of the debit or credit type set. - - You cannot delete account "%(account)s" because it has move lines. - To set a second currency for account "%(account)s", it must not have a type "payable", "revenue", "receivable" nor "expense". diff --git a/modules/account/move.py b/modules/account/move.py index d70cbb8b5e..8e1114cdc4 100644 --- a/modules/account/move.py +++ b/modules/account/move.py @@ -132,7 +132,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView): super().__setup__() t = cls.__table__() cls.create_date.select = True - cls._check_modify_exclude = ['lines'] + cls._check_modify_exclude = {'lines'} cls._order.insert(0, ('date', 'DESC')) cls._order.insert(1, ('number', 'DESC')) cls._buttons.update({ @@ -269,40 +269,17 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView): return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod - def check_modify(cls, moves): - 'Check posted moves for modifications.' - for move in moves: - if move.state == 'posted': - raise AccessError( - gettext('account.msg_modify_posted_moved', - move=move.rec_name)) - - def get_rec_name(self, name): - if self.number: - return self.number - else: - return f'({self.id})' - - @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for moves, values in zip(actions, actions): - keys = list(values.keys()) - for key in cls._check_modify_exclude: - if key in keys: - keys.remove(key) - if len(keys): - cls.check_modify(moves) - args.extend((moves, values)) - super().write(*args) - - @classmethod - def delete(cls, moves): - MoveLine = Pool().get('account.move.line') - cls.check_modify(moves) - MoveLine.delete([l for m in moves for l in m.lines]) - super().delete(moves) + def check_modification(cls, mode, moves, values=None, external=False): + super().check_modification( + mode, moves, values=values, external=external) + if (mode == 'delete' + or (mode == 'write' + and values.keys() - cls._check_modify_exclude)): + for move in moves: + if move.state == 'posted': + raise AccessError(gettext( + 'account.msg_modify_posted_moved', + move=move.rec_name)) @classmethod def copy(cls, moves, default=None): @@ -591,20 +568,49 @@ class Reconciliation(ModelSQL, ModelView): return Transaction().context.get('company') @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('account.configuration') - configuration = Configuration(1) + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'reconciliation_sequence', company=company_id): + values['number'] = sequence.get() + return values - vlist = [x.copy() for x in vlist] - default_company = cls.default_company() - for vals in vlist: - if 'number' not in vals: - vals['number'] = configuration.get_multivalue( - 'reconciliation_sequence', - company=vals.get('company', default_company)).get() + @classmethod + def check_modification( + cls, mode, reconciliations, values=None, external=False): + pool = Pool() + Warning = pool.get('res.user.warning') - return super().create(vlist) + super().check_modification( + mode, reconciliations, values=values, external=external) + + if mode == 'delete': + for reconciliation in reconciliations: + if reconciliation.delegate_to: + key = Warning.format('delete.delegated', [reconciliation]) + if Warning.check(key): + raise ReconciliationDeleteWarning(key, gettext( + 'account.msg_reconciliation_delete_delegated', + reconciliation=reconciliation.rec_name, + line=reconciliation.delegate_to.rec_name, + move=reconciliation.delegate_to.move.rec_name)) + for line in reconciliation.lines: + if line.move.journal.type == 'write-off': + key = Warning.format( + 'delete.write-off', [reconciliation]) + if Warning.check(key): + raise ReconciliationDeleteWarning(key, gettext( + 'account.' + 'msg_reconciliation_delete_write_off', + reconciliation=reconciliation.rec_name, + line=line.rec_name, + move=line.move.rec_name)) @classmethod def write(cls, moves, values, *args): @@ -615,31 +621,6 @@ class Reconciliation(ModelSQL, ModelView): super().validate(reconciliations) cls.check_lines(reconciliations) - @classmethod - def delete(cls, reconciliations): - pool = Pool() - Warning = pool.get('res.user.warning') - for reconciliation in reconciliations: - if reconciliation.delegate_to: - key = Warning.format('delete.delegated', [reconciliation]) - if Warning.check(key): - raise ReconciliationDeleteWarning(key, - gettext('account.msg_reconciliation_delete_delegated', - reconciliation=reconciliation.rec_name, - line=reconciliation.delegate_to.rec_name, - move=reconciliation.delegate_to.move.rec_name)) - for line in reconciliation.lines: - if line.move.journal.type == 'write-off': - key = Warning.format('delete.write-off', [reconciliation]) - if Warning.check(key): - raise ReconciliationDeleteWarning(key, - gettext( - 'account.msg_reconciliation_delete_write_off', - reconciliation=reconciliation.rec_name, - line=line.rec_name, - move=line.move.rec_name)) - super().delete(reconciliations) - @classmethod def check_lines(cls, reconciliations): Lang = Pool().get('ir.lang') @@ -1441,36 +1422,50 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): }]) @classmethod - def check_modify(cls, lines, modified_fields=None): - ''' - Check if the lines can be modified - ''' - if (modified_fields is not None - and modified_fields <= cls._check_modify_exclude): - return - journal_period_done = [] - for line in lines: - if line.move.state == 'posted': - raise AccessError( - gettext('account.msg_modify_line_posted_move', - line=line.rec_name, - move=line.move.rec_name)) - journal_period = (line.journal.id, line.period.id) - if journal_period not in journal_period_done: - cls.check_journal_period_modify(line.period, - line.journal) - journal_period_done.append(journal_period) + def check_modification(cls, mode, lines, values=None, external=False): + pool = Pool() + Move = pool.get('account.move') + + super().check_modification( + mode, lines, values=values, external=external) + + if (mode in {'create', 'delete'} + or (mode == 'write' + and values.keys() - cls._check_modify_exclude)): + journal_period_done = set() + for line in lines: + if line.move.state == 'posted': + raise AccessError(gettext( + 'account.msg_modify_line_posted_move', + line=line.rec_name, + move=line.move.rec_name)) + journal_period = (line.journal.id, line.period.id) + if journal_period not in journal_period_done: + cls.check_journal_period_modify( + line.period, line.journal) + journal_period_done.add(journal_period) + + if (mode == 'delete' + or (mode == 'write' + and values.keys() & cls._reconciliation_modify_disallow)): + for line in lines: + if line.reconciliation: + raise AccessError(gettext( + 'account.msg_modify_line_reconciled', + line=line.rec_name)) + + if mode in {'create', 'write'}: + moves = Move.browse({l.move for l in lines}) + Move.validate_move(moves) @classmethod - def check_reconciliation(cls, lines, modified_fields=None): - if (modified_fields is not None - and not modified_fields & cls._reconciliation_modify_disallow): - return - for line in lines: - if line.reconciliation: - raise AccessError( - gettext('account.msg_modify_line_reconciled', - line=line.rec_name)) + def on_delete(cls, lines): + pool = Pool() + Move = pool.get('account.move') + callback = super().on_delete(lines) + moves = Move.browse({l.move for l in lines}) + callback.append(lambda: Move.validate_move(moves)) + return callback @classmethod def view_attributes(cls): @@ -1489,35 +1484,6 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): return {ModelData.get_id( 'account', 'move_line_view_list_payable_receivable')} - @classmethod - def delete(cls, lines): - Move = Pool().get('account.move') - cls.check_modify(lines) - cls.check_reconciliation(lines) - moves = [x.move for x in lines] - super().delete(lines) - Move.validate_move(moves) - - @classmethod - def write(cls, *args): - Move = Pool().get('account.move') - - actions = iter(args) - args = [] - moves = [] - all_lines = [] - for lines, values in zip(actions, actions): - cls.check_modify(lines, set(values.keys())) - cls.check_reconciliation(lines, set(values.keys())) - moves.extend((x.move for x in lines)) - all_lines.extend(lines) - args.extend((lines, values)) - - super().write(*args) - - Transaction().timestamp = {} - Move.validate_move(list(set(l.move for l in all_lines) | set(moves))) - @classmethod def create(cls, vlist): pool = Pool() @@ -1550,16 +1516,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): # prevent default value for field with set_move_field for fname in move_fields(move_name=False): vals.setdefault(fname, None) - lines = super().create(vlist) - period_and_journals = set((line.period, line.journal) - for line in lines) - for period, journal in period_and_journals: - cls.check_journal_period_modify(period, journal) - # Re-browse for cache alignment - moves = Move.browse(list(set(line.move for line in lines))) - Move.check_modify(moves) - Move.validate_move(moves) - return lines + return super().create(vlist) @classmethod def copy(cls, lines, default=None): diff --git a/modules/account/period.py b/modules/account/period.py index a3b463ca70..f38d4fe6cc 100644 --- a/modules/account/period.py +++ b/modules/account/period.py @@ -30,7 +30,8 @@ class Period(Workflow, ModelSQL, ModelView): end_date = fields.Date('Ending Date', required=True, states=_STATES, domain=[('end_date', '>=', Eval('start_date', None))]) fiscalyear = fields.Many2One( - 'account.fiscalyear', "Fiscal Year", required=True, states=_STATES) + 'account.fiscalyear', "Fiscal Year", required=True, ondelete='CASCADE', + states=_STATES) state = fields.Selection([ ('open', 'Open'), ('closed', 'Closed'), @@ -320,26 +321,29 @@ class Period(Workflow, ModelSQL, ModelView): order=order, count=count, query=query) @classmethod - def create(cls, vlist): - FiscalYear = Pool().get('account.fiscalyear') - vlist = [x.copy() for x in vlist] - for vals in vlist: - if vals.get('fiscalyear'): - fiscalyear = FiscalYear(vals['fiscalyear']) - if fiscalyear.state != 'open': - raise AccessError( - gettext('account.msg_create_period_closed_fiscalyear', - fiscalyear=fiscalyear.rec_name)) - periods = super().create(vlist) - cls._find_cache.clear() - return periods + def preprocess_values(cls, mode, values): + pool = Pool() + FiscalYear = pool.get('account.fiscalyear') + values = super().preprocess_values(mode, values) + if mode == 'create' and values.get('fiscalyear') is not None: + fiscalyear = FiscalYear(values['fiscalyear']) + if values.get('move_sequence') is None: + values['move_sequence'] = fiscalyear.move_sequence.id + return values @classmethod - def write(cls, *args): - Move = Pool().get('account.move') - actions = iter(args) - args = [] - for periods, values in zip(actions, actions): + def check_modification(cls, mode, periods, values=None, external=False): + pool = Pool() + Move = pool.get('account.move') + super().check_modification( + mode, periods, values=values, external=external) + if mode == 'create': + for period in periods: + if period.fiscalyear.state != 'open': + raise AccessError( + gettext('account.msg_create_period_closed_fiscalyear', + fiscalyear=period.fiscalyear.rec_name)) + elif mode == 'write': if values.get('state') == 'open': for period in periods: if period.fiscalyear.state != 'open': @@ -348,26 +352,22 @@ class Period(Workflow, ModelSQL, ModelView): 'account.msg_open_period_closed_fiscalyear', period=period.rec_name, fiscalyear=period.fiscalyear.rec_name)) - if values.get('move_sequence'): + if 'move_sequence' in values: for period in periods: - if (period.move_sequence - and period.move_sequence.id - != values['move_sequence']): - if Move.search([ - ('period', '=', period.id), - ('state', '=', 'posted'), - ]): - raise AccessError( - gettext('account' - '.msg_change_period_move_sequence', - period=period.rec_name)) - args.extend((periods, values)) - super().write(*args) - cls._find_cache.clear() + if sequence := period.move_sequence: + if sequence.id != values['move_sequence']: + if Move.search([ + ('period', '=', period.id), + ('state', '=', 'posted'), + ], limit=1, order=[]): + raise AccessError(gettext( + 'account.' + 'msg_change_period_move_sequence', + period=period.rec_name)) @classmethod - def delete(cls, periods): - super().delete(periods) + def on_modification(cls, mode, periods, field_names=None): + super().on_modification(mode, periods, field_names=field_names) cls._find_cache.clear() @classmethod diff --git a/modules/account/tax.py b/modules/account/tax.py index db4aa7a04f..0dce46ad8e 100644 --- a/modules/account/tax.py +++ b/modules/account/tax.py @@ -132,7 +132,8 @@ class TaxCode( code = fields.Char('Code', states=_states) company = fields.Many2One('company.company', 'Company', required=True) parent = fields.Many2One( - 'account.tax.code', "Parent", states=_states, + 'account.tax.code', "Parent", ondelete='CASCADE', + states=_states, domain=[ ('company', '=', Eval('company', -1)), ]) @@ -243,13 +244,6 @@ class TaxCode( default.setdefault('template', None) return super().copy(codes, default=default) - @classmethod - def delete(cls, codes): - codes = cls.search([ - ('parent', 'child_of', [c.id for c in codes]), - ]) - super().delete(codes) - @classmethod def update_tax_code(cls, company_id, template2tax_code=None): ''' @@ -1407,34 +1401,19 @@ class TaxLine(ModelSQL, ModelView): def search_rec_name(cls, name, clause): return [('tax',) + tuple(clause[1:])] - @classmethod - def create(cls, vlist): - lines = super().create(vlist) - cls.check_modify(lines) - return lines - - @classmethod - def write(cls, *args): - lines = sum(args[0:None:2], []) - cls.check_modify(lines) - super().write(*args) - - @classmethod - def delete(cls, lines): - cls.check_modify(lines) - super().delete(lines) - @property def period_checked(self): return self.move_line.period @classmethod - def check_modify(cls, lines): + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) for line in lines: period = line.period_checked if period and period.state != 'open': - raise AccessError( - gettext('account.msg_modify_tax_line_closed_period', + raise AccessError(gettext( + 'account.msg_modify_tax_line_closed_period', period=period.rec_name)) diff --git a/modules/account_asset/asset.py b/modules/account_asset/asset.py index d067898a3f..39db924d3a 100644 --- a/modules/account_asset/asset.py +++ b/modules/account_asset/asset.py @@ -712,13 +712,15 @@ class Asset(Workflow, ModelSQL, ModelView): return super().copy(assets, default=default) @classmethod - def delete(cls, assets): - for asset in assets: - if asset.state != 'draft': - raise AccessError( - gettext('account_asset.msg_asset_delete_draft', - asset=asset.rec_name)) - return super().delete(assets) + def check_modification(cls, mode, assets, values=None, external=False): + super().check_modification( + mode, assets, values=values, external=external) + if mode == 'delete': + for asset in assets: + if asset.state != 'draft': + raise AccessError(gettext( + 'account_asset.msg_asset_delete_draft', + asset=asset.rec_name)) class AssetLine(ModelSQL, ModelView): diff --git a/modules/account_consolidation/account.py b/modules/account_consolidation/account.py index 282f501ad7..32ad2df1f1 100644 --- a/modules/account_consolidation/account.py +++ b/modules/account_consolidation/account.py @@ -57,7 +57,7 @@ class Move(metaclass=PoolMeta): @classmethod def __setup__(cls): super().__setup__() - cls._check_modify_exclude.append('consolidation_company') + cls._check_modify_exclude.add('consolidation_company') class MoveLine(metaclass=PoolMeta): diff --git a/modules/account_es_sii/account.py b/modules/account_es_sii/account.py index 269dd47a32..f072a388f6 100644 --- a/modules/account_es_sii/account.py +++ b/modules/account_es_sii/account.py @@ -229,17 +229,16 @@ class Period(metaclass=PoolMeta): help="Check to create SII records for the invoices in the period.") @classmethod - def write(cls, *args): - actions = iter(args) - to_check = [] - for periods, values in zip(actions, actions): - if 'es_sii_send_invoices' in values: - for period in periods: - if (period.es_sii_send_invoices - != values['es_sii_send_invoices']): - to_check.append(period) - cls.check_es_sii_posted_invoices(to_check) - super().write(*args) + def check_modification(cls, mode, periods, values=None, external=False): + super().check_modification( + mode, periods, values=values, external=external) + if mode == 'write' and 'es_sii_send_invoices' in values: + to_check = [] + for period in periods: + if (period.es_sii_send_invoices + != values['es_sii_send_invoices']): + to_check.append(period) + cls.check_es_sii_posted_invoices(to_check) @classmethod def check_es_sii_posted_invoices(cls, periods): @@ -248,11 +247,11 @@ class Period(metaclass=PoolMeta): for sub_ids in grouped_slice(list(map(int, periods))): invoices = Invoice.search([ ('move.period', 'in', sub_ids), - ], limit=1) + ], limit=1, order=[]) if invoices: invoice, = invoices - raise ESSIIPostedInvoicesError( - gettext('account_es_sii.msg_es_sii_posted_invoices', + raise ESSIIPostedInvoicesError(gettext( + 'account_es_sii.msg_es_sii_posted_invoices', period=invoice.move.period.rec_name)) @@ -352,13 +351,15 @@ class InvoiceSII(ModelSQL, ModelView): return [('invoice.rec_name',) + tuple(clause[1:])] @classmethod - def delete(cls, records): - for record in records: - if record.csv: - raise AccessError( - gettext('account_es_sii.msg_es_sii_invoice_delete_sent', - invoice=record.invoice.rec_name)) - super().delete(records) + def check_modification(cls, mode, invoices, values=None, external=False): + super().check_modification( + mode, invoices, values=values, external=external) + if mode == 'delete': + for invoice in invoices: + if invoice.csv: + raise AccessError(gettext( + 'account_es_sii.msg_es_sii_invoice_delete_sent', + invoice=invoice.rec_name)) @property def endpoint(self): diff --git a/modules/account_export/account.py b/modules/account_export/account.py index 29943e57b3..21d9ef6e6a 100644 --- a/modules/account_export/account.py +++ b/modules/account_export/account.py @@ -60,7 +60,7 @@ class Move(metaclass=PoolMeta): @classmethod def __setup__(cls): super().__setup__() - cls._check_modify_exclude.append('export') + cls._check_modify_exclude.add('export') @classmethod def copy(cls, moves, default=None): @@ -209,11 +209,13 @@ class MoveExport(Workflow, ModelSQL, ModelView): return super().copy(exports, default=default) @classmethod - def delete(cls, exports): - for export in exports: - if export.state != 'draft': - raise AccessError( - gettext( - 'account_export.msg_account_move_export_delete_draft', - export=export.rec_name)) - super().delete(exports) + def check_modification(cls, mode, exports, values=None, external=False): + super().check_modification( + mode, exports, values=values, external=external) + if mode == 'delete': + for export in exports: + if export.state != 'draft': + raise AccessError(gettext( + 'account_export.' + 'msg_account_move_export_delete_draft', + export=export.rec_name)) diff --git a/modules/account_fr_chorus/account.py b/modules/account_fr_chorus/account.py index e2e73dc96a..b18374720d 100644 --- a/modules/account_fr_chorus/account.py +++ b/modules/account_fr_chorus/account.py @@ -163,11 +163,12 @@ class CredentialChorus(ModelSQL, CompanyValueMixin): return resp.json() @classmethod - def write(cls, *args): + def check_modification(cls, mode, records, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for records, values in zip(actions, actions): + super().check_modification( + mode, records, values=values, external=external) + if mode == 'write' and external: for record in records: for field in [ 'chorus_piste_client_id', 'chorus_piste_client_secret', @@ -182,7 +183,6 @@ class CredentialChorus(ModelSQL, CompanyValueMixin): warning_name, gettext('account_fr_chorus' '.msg_chorus_credential_modified')) - return super().write(*args) class Invoice(metaclass=PoolMeta): @@ -300,6 +300,9 @@ class InvoiceChorus( table, _ = tables[None] return [CharLength(table.number), table.number] + def get_rec_name(self, name): + return self.invoice.rec_name + @classmethod def validate(cls, records): super().validate(records) @@ -316,13 +319,15 @@ class InvoiceChorus( address=address.rec_name)) @classmethod - def delete(cls, records): - for record in records: - if record.number: - raise AccessError( - gettext('account_fr_chorus.msg_invoice_delete_sent', - invoice=record.invoice.rec_name)) - super().delete(records) + def check_modification(cls, mode, invoices, values=None, external=False): + super().check_modification( + mode, invoices, values=values, external=external) + if mode == 'delete': + for invoice in invoices: + if invoice.number: + raise AccessError(gettext( + 'account_fr_chorus.msg_invoice_delete_sent', + invoice=invoice.rec_name)) def _send_context(self): return { diff --git a/modules/account_invoice/account.py b/modules/account_invoice/account.py index da1b61889b..56a76e13d7 100644 --- a/modules/account_invoice/account.py +++ b/modules/account_invoice/account.py @@ -253,27 +253,18 @@ class Reconciliation(metaclass=PoolMeta): __name__ = 'account.move.reconciliation' @classmethod - def create(cls, vlist): - Invoice = Pool().get('account.invoice') + def on_modification(cls, mode, reconciliations, field_names=None): + pool = Pool() + Invoice = pool.get('account.invoice') transaction = Transaction() context = transaction.context - reconciliations = super().create(vlist) + + super().on_modification(mode, reconciliations, field_names=field_names) + with transaction.set_context( queue_batch=context.get('queue_batch', True)): Invoice.__queue__.process( list(_invoices_to_process(reconciliations))) - return reconciliations - - @classmethod - def delete(cls, reconciliations): - Invoice = Pool().get('account.invoice') - transaction = Transaction() - context = transaction.context - invoices_to_process = _invoices_to_process(reconciliations) - super().delete(reconciliations) - with transaction.set_context( - queue_batch=context.get('queue_batch', True)): - Invoice.__queue__.process(list(invoices_to_process)) class RenewFiscalYear(metaclass=PoolMeta): diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py index 79237b9c22..c882f148f7 100644 --- a/modules/account_invoice/invoice.py +++ b/modules/account_invoice/invoice.py @@ -1404,17 +1404,6 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin): or (self.state == 'cancelled' and (self.move or self.cancel_move or self.number))) - @classmethod - def check_modify(cls, invoices): - ''' - Check if the invoices can be modified - ''' - for invoice in invoices: - if not invoice.is_modifiable: - raise AccessError( - gettext('account_invoice.msg_invoice_modify', - invoice=invoice.rec_name)) - def get_rec_name(self, name): items = [] if self.number: @@ -1456,39 +1445,34 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin): ] @classmethod - def delete(cls, invoices): - cls.check_modify(invoices) - # Cancel before delete - cls.cancel(invoices) - for invoice in invoices: - if invoice.state != 'cancelled': - raise AccessError( - gettext('account_invoice.msg_invoice_delete_cancel', - invoice=invoice.rec_name)) - if invoice.number: - raise AccessError( - gettext('account_invoice.msg_invoice_delete_numbered', - invoice=invoice.rec_name)) - super().delete(invoices) + def check_modification(cls, mode, invoices, values=None, external=False): + super().check_modification( + mode, invoices, values=values, external=external) + if (mode == 'delete' + or (mode == 'write' + and values.keys() - cls._check_modify_exclude)): + for invoice in invoices: + if not invoice.is_modifiable: + raise AccessError(gettext( + 'account_invoice.msg_invoice_modify', + invoice=invoice.rec_name)) + if mode == 'delete': + for invoice in invoices: + if invoice.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'account_invoice.msg_invoice_delete_cancel', + invoice=invoice.rec_name)) + if invoice.number: + raise AccessError(gettext( + 'account_invoice.msg_invoice_delete_numbered', + invoice=invoice.rec_name)) @classmethod - def create(cls, vlist): - invoices = super().create(vlist) - cls.update_taxes(invoices) - return invoices - - @classmethod - def write(cls, *args): - actions = iter(args) - all_invoices = [] - for invoices, values in zip(actions, actions): - if set(values) - cls._check_modify_exclude: - cls.check_modify(invoices) - all_invoices += invoices - update_tax = [i for i in all_invoices if i.state == 'draft'] - super().write(*args) - if update_tax: - cls.update_taxes(update_tax) + def on_modification(cls, mode, invoices, field_names=None): + super().on_modification(mode, invoices, field_names=field_names) + if mode != 'delete': + if draft_invoices := [i for i in invoices if i.state == 'draft']: + cls.update_taxes(draft_invoices) @classmethod def copy(cls, invoices, default=None): @@ -2804,15 +2788,22 @@ class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): ] @classmethod - def check_modify(cls, lines, fields=None): - ''' - Check if the lines can be modified - ''' - if fields is None or fields - cls._check_modify_exclude: + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'create': + for line in lines: + if line.invoice.state != 'draft': + raise AccessError(gettext( + 'account_invoice.msg_invoice_line_create_draft', + invoice=line.invoice.rec_name)) + elif (mode == 'delete' + or (mode == 'write' + and values.keys() - cls._check_modify_exclude)): for line in lines: if line.invoice and not line.invoice.is_modifiable: - raise AccessError( - gettext('account_invoice.msg_invoice_line_modify', + raise AccessError(gettext( + 'account_invoice.msg_invoice_line_modify', line=line.rec_name, invoice=line.invoice.rec_name)) @@ -2822,30 +2813,6 @@ class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): ('/form//field[@name="note"]|/form//field[@name="description"]', 'spell', Eval('party_lang'))] - @classmethod - def delete(cls, lines): - cls.check_modify(lines) - super().delete(lines) - - @classmethod - def write(cls, *args): - actions = iter(args) - for lines, values in zip(actions, actions): - cls.check_modify(lines, set(values)) - super().write(*args) - - @classmethod - def create(cls, vlist): - pool = Pool() - Invoice = pool.get('account.invoice') - invoice_ids = filter(None, {v.get('invoice') for v in vlist}) - for invoice in Invoice.browse(list(invoice_ids)): - if invoice.state != 'draft': - raise AccessError( - gettext('account_invoice.msg_invoice_line_create_draft', - invoice=invoice.rec_name)) - return super().create(vlist) - @classmethod def copy(cls, lines, default=None): if default is None: @@ -3134,15 +3101,22 @@ class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): return (account_id, tax_id, (getattr(self, 'base', 0) or 0) >= 0) @classmethod - def check_modify(cls, taxes, fields=None): - ''' - Check if the taxes can be modified - ''' - if fields is None or fields - cls._check_modify_exclude: + def check_modification(cls, mode, taxes, values=None, external=False): + super().check_modification( + mode, taxes, values=values, external=external) + if mode == 'create': + for tax in taxes: + if tax.invoice.state != 'draft': + raise AccessError(gettext( + 'account_invoice.msg_invoice_line_create_draft', + invoice=tax.invoice.rec_name)) + elif (mode == 'delete' + or (mode == 'write' + and values.keys() - cls._check_modify_exclude)): for tax in taxes: if not tax.invoice.is_modifiable: - raise AccessError( - gettext('account_invoice.msg_invoice_tax_modify', + raise AccessError(gettext( + 'account_invoice.msg_invoice_tax_modify', tax=tax.rec_name, invoice=tax.invoice.rec_name)) @@ -3154,30 +3128,6 @@ class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): i += 1 return 0 - @classmethod - def delete(cls, taxes): - cls.check_modify(taxes) - super().delete(taxes) - - @classmethod - def write(cls, *args): - actions = iter(args) - for taxes, values in zip(actions, actions): - cls.check_modify(taxes, set(values)) - super().write(*args) - - @classmethod - def create(cls, vlist): - pool = Pool() - Invoice = pool.get('account.invoice') - invoice_ids = filter(None, {v.get('invoice') for v in vlist}) - for invoice in Invoice.browse(list(invoice_ids)): - if invoice.state != 'draft': - raise AccessError( - gettext('account_invoice.msg_invoice_tax_create_draft', - invoice=invoice.rec_name)) - return super().create(vlist) - def get_move_lines(self): ''' Return a list of move lines instances for invoice tax diff --git a/modules/account_invoice_defer/account.py b/modules/account_invoice_defer/account.py index f37b48c22a..decef1ffb0 100644 --- a/modules/account_invoice_defer/account.py +++ b/modules/account_invoice_defer/account.py @@ -240,14 +240,16 @@ class InvoiceDeferred(Workflow, ModelSQL, ModelView): and deferral.moves[-1].period.end_date >= deferral.end_date) @classmethod - def delete(cls, deferrals): - for deferral in deferrals: - if deferral.state != 'draft': - raise AccessError( - gettext('account_invoice_defer' - '.msg_invoice_deferred_delete_draft', - deferral=deferral.rec_name)) - return super().delete(deferrals) + def check_modification(cls, mode, deferrals, values=None, external=False): + super().check_modification( + mode, deferrals, values=values, external=external) + if mode == 'delete': + for deferral in deferrals: + if deferral.state != 'draft': + raise AccessError(gettext( + 'account_invoice_defer' + '.msg_invoice_deferred_delete_draft', + deferral=deferral.rec_name)) @classmethod def defer_amount(cls, deferrals): diff --git a/modules/account_invoice_stock/account.py b/modules/account_invoice_stock/account.py index 64fc63d7d6..e9e32c220d 100644 --- a/modules/account_invoice_stock/account.py +++ b/modules/account_invoice_stock/account.py @@ -115,18 +115,18 @@ class InvoiceLine(metaclass=PoolMeta): return ', '.join(sorted(shipments)) @classmethod - def write(cls, *args): + def on_modification(cls, mode, lines, field_names=None): pool = Pool() Move = pool.get('stock.move') transaction = Transaction() context = transaction.context - super().write(*args) - lines = sum(args[0:None:2], []) - moves = sum((l.stock_moves for l in lines), ()) - if moves: - with transaction.set_context( - queue_batch=context.get('queue_batch', True)): - Move.__queue__.update_unit_price(moves) + super().on_modification(mode, lines, field_names=field_names) + if mode == 'write': + moves = sum((l.stock_moves for l in lines), ()) + if moves: + with transaction.set_context( + queue_batch=context.get('queue_batch', True)): + Move.__queue__.update_unit_price(moves) @classmethod def copy(cls, lines, default=None): diff --git a/modules/account_payment/payment.py b/modules/account_payment/payment.py index 029850d871..b95484219c 100644 --- a/modules/account_payment/payment.py +++ b/modules/account_payment/payment.py @@ -141,19 +141,18 @@ class Group(ModelSQL, ModelView): pass @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Config = pool.get('account.configuration') - - vlist = [v.copy() for v in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'payment_group_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + Configuration = pool.get('account.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + configuration = Configuration(1) + company_id = values.get('company', cls.default_company()) + if company_id is not None: + if sequence := configuration.get_multivalue( + 'payment_group_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, groups, default=None): @@ -610,53 +609,58 @@ class Payment(Workflow, ModelSQL, ModelView): return super().copy(payments, default=default) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Line = pool.get('account.move.line') - Config = pool.get('account.configuration') - - vlist = [v.copy() for v in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'payment_sequence', - company=values.get('company', default_company)).get() - payments = super().create(vlist) - lines = {p.line for p in payments if p.line} - if lines: - Line.set_payment_amount(list(lines)) - return payments + Configuration = pool.get('account.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + configuration = Configuration(1) + company_id = values.get('company', cls.default_company()) + if company_id is not None: + if sequence := configuration.get_multivalue( + 'payment_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod - def write(cls, *args): - pool = Pool() - Line = pool.get('account.move.line') - actions = iter(args) - lines = set() - for payments, values in zip(actions, actions): - if values.keys() & {'line', 'amount', 'state'}: - lines.update(p.line for p in payments if p.line) - if line := values.get('line'): - lines.add(Line(line)) - super().write(*args) - if lines: - Line.set_payment_amount(list(lines)) + def check_modification(cls, mode, payments, values=None, external=False): + super().check_modification( + mode, payments, values=values, external=external) + if mode == 'delete': + for payment in payments: + if payment.state != 'draft': + raise AccessError(gettext( + 'account_payment.msg_payment_delete_draft', + payment=payment.rec_name)) @classmethod - def delete(cls, payments): + def on_modification(cls, mode, payments, field_names=None): pool = Pool() Line = pool.get('account.move.line') - for payment in payments: - if payment.state != 'draft': - raise AccessError( - gettext('account_payment.msg_payment_delete_draft', - payment=payment.rec_name)) - lines = {p.line for p in payments if p.line} - super().delete(payments) - if lines: - Line.set_payment_amount(list(lines)) + super().on_modification(mode, payments, field_names=field_names) + if mode in {'create', 'write'}: + if not field_names or 'line' in field_names: + if lines := Line.browse({p.line for p in payments if p.line}): + Line.set_payment_amount(lines) + + @classmethod + def on_write(cls, payments, values): + pool = Pool() + Line = pool.get('account.move.line') + callback = super().on_write(payments, values) + if values.keys() & {'line', 'amount', 'state'}: + if lines := Line.browse({p.line for p in payments if p.line}): + callback.append(lambda: Line.set_payment_amount(lines)) + return callback + + @classmethod + def on_delete(cls, payments): + pool = Pool() + Line = pool.get('account.move.line') + callback = super().on_delete(payments) + if lines := Line.browse({p.line for p in payments if p.line}): + callback.append(lambda: Line.set_payment_amount(lines)) + return callback @classmethod @ModelView.button diff --git a/modules/account_payment_braintree/account.py b/modules/account_payment_braintree/account.py index 532c5f670f..65908e139c 100644 --- a/modules/account_payment_braintree/account.py +++ b/modules/account_payment_braintree/account.py @@ -909,20 +909,23 @@ class PaymentBraintreeAccount(ModelSQL, ModelView): return bool(payments) @classmethod - def write(cls, *args): + def check_modification(cls, mode, accounts, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for accounts, values in zip(actions, actions): - if ({'environment', 'merchant_id', 'public_key', 'private_key'} - & values.keys()): - warning_name = Warning.format('braintree_key', accounts) - if Warning.check(warning_name): - raise BraintreeAccountWarning( - warning_name, - gettext('account_payment_braintree' - '.msg_braintree_key_modified')) - return super().write(*args) + + super().check_modification( + mode, accounts, values=values, external=external) + + if (mode == 'write' + and external + and values.keys() + & {'environment', 'merchant_id', 'public_key', 'private_key'}): + warning_name = Warning.format('braintree_key', accounts) + if Warning.check(warning_name): + raise BraintreeAccountWarning( + warning_name, + gettext('account_payment_braintree' + '.msg_braintree_key_modified')) class PaymentBraintreeCustomer( @@ -992,9 +995,11 @@ class PaymentBraintreeCustomer( @classmethod def delete(cls, customers): + ids, on_delete = cls._before_delete(customers) cls.write(customers, { 'active': False, }) + cls._after_delete(ids, on_delete) @classmethod def copy(cls, customers, default=None): diff --git a/modules/account_payment_braintree/party.py b/modules/account_payment_braintree/party.py index 6c3f6ed4dc..7baf55ddf5 100644 --- a/modules/account_payment_braintree/party.py +++ b/modules/account_payment_braintree/party.py @@ -21,26 +21,29 @@ class Party(metaclass=PoolMeta): return parties @classmethod - def write(cls, *args): + def on_write(cls, parties, values): pool = Pool() Customer = pool.get('account.payment.braintree.customer') transaction = Transaction() context = transaction.context - parties = sum(args[0:None:2], []) + callback = super().on_write(parties, values) + customers = sum((p.braintree_customers for p in parties), ()) - customer2params = {c: c._customer_parameters() for c in customers} + if customers: + customer2params = {c: c._customer_parameters() for c in customers} - super().write(*args) - - to_update = [] - for customer, params in customer2params.items(): - if customer._customer_parameters() != params: - to_update.append(customer) - if to_update: - with Transaction().set_context( - queue_batch=context.get('queue_batch', True)): - Customer.__queue__.braintree_update(to_update) + def update(): + to_update = [] + for customer, params in customer2params.items(): + if customer._customer_parameters() != params: + to_update.append(customer) + if to_update: + with transaction.set_context( + queue_batch=context.get('queue_batch', True)): + Customer.__queue__.braintree_update(to_update) + callback.append(update) + return callback class PartyReceptionDirectDebit( diff --git a/modules/account_payment_clearing/account.py b/modules/account_payment_clearing/account.py index 9787dd80f5..65d8b23230 100644 --- a/modules/account_payment_clearing/account.py +++ b/modules/account_payment_clearing/account.py @@ -40,18 +40,9 @@ class MoveReconciliation(metaclass=PoolMeta): __name__ = 'account.move.reconciliation' @classmethod - def create(cls, vlist): + def on_modification(cls, mode, reconciliations, field_names=None): pool = Pool() Payment = pool.get('account.payment') - reconciliations = super().create(vlist) + super().on_modification(mode, reconciliations, field_names=field_names) Payment.__queue__.update_reconciled( list(_payments_to_update(reconciliations))) - return reconciliations - - @classmethod - def delete(cls, reconciliations): - pool = Pool() - Payment = pool.get('account.payment') - payments = _payments_to_update(reconciliations) - super().delete(reconciliations) - Payment.__queue__.update_reconciled(list(payments)) diff --git a/modules/account_payment_clearing/payment.py b/modules/account_payment_clearing/payment.py index fa041724a9..f694628a55 100644 --- a/modules/account_payment_clearing/payment.py +++ b/modules/account_payment_clearing/payment.py @@ -76,7 +76,7 @@ def cancel_clearing_move(func): Line = pool.get('account.move.line') Reconciliation = pool.get('account.move.reconciliation') - func(cls, payments, *args, **kwargs) + result = func(cls, payments, *args, **kwargs) to_delete = [] to_reconcile = defaultdict(lambda: defaultdict(list)) @@ -110,6 +110,7 @@ def cancel_clearing_move(func): for lines in to_reconcile[party].values(): Line.reconcile(lines) cls.update_reconciled(payments) + return result return wrapper diff --git a/modules/account_payment_sepa/payment.py b/modules/account_payment_sepa/payment.py index 8b382acc03..4e538d2881 100644 --- a/modules/account_payment_sepa/payment.py +++ b/modules/account_payment_sepa/payment.py @@ -639,36 +639,32 @@ class Mandate(Workflow, ModelSQL, ModelView): ] @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('account.configuration') - - config = Configuration(1) - vlist = [v.copy() for v in vlist] - default_company = cls.default_company() - for values in vlist: - if not values.get('identification'): - sequence = config.get_multivalue( - 'sepa_mandate_sequence', - company=values.get('company', default_company)) - if sequence: + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('identification'): + configuration = Configuration(1) + company_id = values.get('company', cls.default_company()) + if company_id is not None: + if sequence := configuration.get_multivalue( + 'sepa_mandate_sequence', company=company_id): values['identification'] = sequence.get() - # Prevent raising false unique constraint - if values.get('identification') == '': - values['identification'] = None - return super().create(vlist) + if values.get('identification') == '': + values['identification'] = None + return values @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for mandates, values in zip(actions, actions): - # Prevent raising false unique constraint - if values.get('identification') == '': - values = values.copy() - values['identification'] = None - args.extend((mandates, values)) - super().write(*args) + def check_modification(cls, mode, mandates, values=None, external=False): + super().check_modification( + mode, mandates, values=values, external=external) + if mode == 'delete': + for mandate in mandates: + if mandate.state not in {'draft', 'cancelled'}: + raise AccessError(gettext( + 'account_payment_sepa' + '.msg_mandate_delete_draft_cancelled', + mandate=mandate.rec_name)) @classmethod def copy(cls, mandates, default=None): @@ -767,16 +763,6 @@ class Mandate(Workflow, ModelSQL, ModelView): # TODO must be automaticaly cancelled 13 months after last collection pass - @classmethod - def delete(cls, mandates): - for mandate in mandates: - if mandate.state not in ('draft', 'cancelled'): - raise AccessError( - gettext('account_payment_sepa' - '.msg_mandate_delete_draft_cancelled', - mandate=mandate.rec_name)) - super().delete(mandates) - class MandateReport(CompanyReport): __name__ = 'account.payment.sepa.mandate' diff --git a/modules/account_payment_stripe/party.py b/modules/account_payment_stripe/party.py index 9a34f40c30..ee4d1f1f7e 100644 --- a/modules/account_payment_stripe/party.py +++ b/modules/account_payment_stripe/party.py @@ -21,26 +21,29 @@ class Party(metaclass=PoolMeta): return parties @classmethod - def write(cls, *args): + def on_write(cls, parties, values): pool = Pool() Customer = pool.get('account.payment.stripe.customer') transaction = Transaction() context = transaction.context - parties = sum(args[0:None:2], []) + callback = super().on_write(parties, values) + customers = sum((p.stripe_customers for p in parties), ()) - customer2params = {c: c._customer_parameters() for c in customers} + if customers: + customer2params = {c: c._customer_parameters() for c in customers} - super().write(*args) - - to_update = [] - for customer, params in customer2params.items(): - if customer._customer_parameters() != params: - to_update.append(customer) - if to_update: - with transaction.set_context( - queue_batch=context.get('queue_batch', True)): - Customer.__queue__.stripe_update(to_update) + def update(): + to_update = [] + for customer, params in customer2params.items(): + if customer._customer_parameters() != params: + to_update.append(customer) + if to_update: + with transaction.set_context( + queue_batch=context.get('queue_batch', True)): + Customer.__queue__.stripe_update(to_update) + callback.append(update) + return callback class PartyReceptionDirectDebit( diff --git a/modules/account_payment_stripe/payment.py b/modules/account_payment_stripe/payment.py index f782e5eb01..22dc10da35 100644 --- a/modules/account_payment_stripe/payment.py +++ b/modules/account_payment_stripe/payment.py @@ -324,14 +324,15 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta): payment=payment.rec_name)) @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - # Ensure to get a different key for each record - # default methods are called only once - values.setdefault('stripe_idempotency_key', - cls.default_stripe_idempotency_key()) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': + if 'stripe_idempotency_key' not in values: + # Ensure to get a different key for each record + # default methods are called only once + values['stripe_idempotency_key'] = ( + cls.default_stripe_idempotency_key()) + return values @classmethod def copy(cls, payments, default=None): @@ -839,13 +840,15 @@ class Refund(Workflow, ModelSQL, ModelView): return 'draft' @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - values.setdefault( - 'stripe_idempotency_key', - cls.default_stripe_idempotency_key()) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': + if 'stripe_idempotency_key' not in values: + # Ensure to get a different key for each record + # default methods are called only once + values['stripe_idempotency_key'] = ( + cls.default_stripe_idempotency_key()) + return values @classmethod def copy(cls, refunds, default=None): @@ -1394,19 +1397,22 @@ class Account(ModelSQL, ModelView): cls.save(accounts) @classmethod - def write(cls, *args): + def check_modification(cls, mode, accounts, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for accounts, values in zip(actions, actions): - if {'secret_key', 'publishable_key'} & values.keys(): - warning_name = Warning.format('stripe_key', accounts) - if Warning.check(warning_name): - raise StripeAccountWarning( - warning_name, - gettext('account_payment_stripe' - '.msg_stripe_key_modified')) - return super().write(*args) + + super().check_modification( + mode, accounts, values=values, external=external) + + if (mode == 'write' + and external + and values.keys() & {'secret_key', 'publishable_key'}): + warning_name = Warning.format('stripe_key', accounts) + if Warning.check(warning_name): + raise StripeAccountWarning( + warning_name, + gettext('account_payment_stripe' + '.msg_stripe_key_modified')) class Customer(CheckoutMixin, DeactivableMixin, ModelSQL, ModelView): @@ -1505,16 +1511,19 @@ class Customer(CheckoutMixin, DeactivableMixin, ModelSQL, ModelView): return self.stripe_customer_id if self.stripe_customer_id else name @classmethod - def write(cls, *args, **kwargs): - super().write(*args, **kwargs) - cls._sources_cache.clear() - cls._payment_methods_cache.clear() + def on_modification(cls, mode, customers, field_names=None): + super().on_modification(mode, customers, field_names=field_names) + if mode == 'write': + cls._sources_cache.clear() + cls._payment_methods_cache.clear() @classmethod def delete(cls, customers): + ids, on_delete = cls._before_delete(customers) cls.write(customers, { 'active': False, }) + cls._after_delete(ids, on_delete) @classmethod def copy(cls, customers, default=None): diff --git a/modules/account_statement/statement.py b/modules/account_statement/statement.py index 12f26e8c60..a47a5e4f70 100644 --- a/modules/account_statement/statement.py +++ b/modules/account_statement/statement.py @@ -368,15 +368,15 @@ class Statement(Workflow, ModelSQL, ModelView): ] @classmethod - def delete(cls, statements): - # Cancel before delete - cls.cancel(statements) - for statement in statements: - if statement.state != 'cancelled': - raise AccessError( - gettext('account_statement.msg_statement_delete_cancel', - statement=statement.rec_name)) - super().delete(statements) + def check_modification(cls, mode, statements, values=None, external=False): + super().check_modification( + mode, statements, values=values, external=external) + if mode == 'delete': + for statement in statements: + if statement.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'account_statement.msg_statement_delete_cancel', + statement=statement.rec_name)) @classmethod @ModelView.button @@ -963,16 +963,17 @@ class Line(origin_mixin(_states), sequence_ordered(), ModelSQL, ModelView): return [('statement.rec_name',) + tuple(clause[1:])] @classmethod - def delete(cls, lines): - for line in lines: - if line.statement_state not in {'cancelled', 'draft'}: - raise AccessError( - gettext( - 'account_statement.' - 'msg_statement_line_delete_cancel_draft', - line=line.rec_name, - sale=line.statement.rec_name)) - super().delete(lines) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'delete': + for line in lines: + if line.statement_state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'account_statement.' + 'msg_statement_line_delete_cancel_draft', + line=line.rec_name, + sale=line.statement.rec_name)) @classmethod def copy(cls, lines, default=None): @@ -1214,16 +1215,17 @@ class Origin(origin_mixin(_states), ModelSQL, ModelView): return [('id', 'in', query)] @classmethod - def delete(cls, origins): - for origin in origins: - if origin.statement_state not in {'cancelled', 'draft'}: - raise AccessError( - gettext( - 'account_statement.' - 'msg_statement_origin_delete_cancel_draft', - origin=origin.rec_name, - sale=origin.statement.rec_name)) - super().delete(origins) + def check_modification(cls, mode, origins, values=None, external=False): + super().check_modification( + mode, origins, values=values, external=external) + if mode == 'delete': + for origin in origins: + if origin.statement_state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'account_statement.' + 'msg_statement_origin_delete_cancel_draft', + origin=origin.rec_name, + sale=origin.statement.rec_name)) @classmethod def copy(cls, origins, default=None): diff --git a/modules/account_stock_anglo_saxon/stock.py b/modules/account_stock_anglo_saxon/stock.py index dd2029a6ce..b012c4fd4d 100644 --- a/modules/account_stock_anglo_saxon/stock.py +++ b/modules/account_stock_anglo_saxon/stock.py @@ -201,11 +201,14 @@ class Move(metaclass=PoolMeta): super().cancel(moves) @classmethod - def delete(cls, moves): - for move in moves: - if move.in_anglo_saxon_quantity or move.out_anglo_saxon_quantity: - raise AccessError( - gettext('account_stock_anglo_saxon' - '.msg_move_delete_anglo_saxon', - move=move.rec_name)) - super().delete(moves) + def check_modification(cls, mode, moves, values=None, external=False): + super().check_modification( + mode, moves, values=values, external=external) + if mode == 'delete': + for move in moves: + if (move.in_anglo_saxon_quantity + or move.out_anglo_saxon_quantity): + raise AccessError(gettext( + 'account_stock_anglo_saxon' + '.msg_move_delete_anglo_saxon', + move=move.rec_name)) diff --git a/modules/account_stock_eu/account_stock_eu.py b/modules/account_stock_eu/account_stock_eu.py index 81a68398e8..3011d70bc4 100644 --- a/modules/account_stock_eu/account_stock_eu.py +++ b/modules/account_stock_eu/account_stock_eu.py @@ -60,19 +60,8 @@ class IntrastatTransaction(DeactivableMixin, ModelSQL, ModelView): return transaction @classmethod - def create(cls, vlist): - transactions = super().create(vlist) - cls._code_cache.clear() - return transactions - - @classmethod - def write(cls, *args): - super().write(*args) - cls._code_cache.clear() - - @classmethod - def delete(cls, transactions): - super().delete(transactions) + def on_modification(cls, mode, transactions, field_names=None): + super().on_modification(mode, transactions, field_names=field_names) cls._code_cache.clear() @@ -212,19 +201,8 @@ class IntrastatDeclaration(Workflow, ModelSQL, ModelView): cls.close(declarations) @classmethod - def create(cls, vlist): - declarations = super().create(vlist) - cls._get_cache.clear() - return declarations - - @classmethod - def write(cls, *args): - super().write(*args) - cls._get_cache.clear() - - @classmethod - def delete(cls, declarations): - super().delete(declarations) + def on_modification(cls, mode, declarations, field_names=None): + super().on_modification(mode, declarations, field_names=field_names) cls._get_cache.clear() diff --git a/modules/account_stock_eu/stock.py b/modules/account_stock_eu/stock.py index 78f9e7d6c1..869263243d 100644 --- a/modules/account_stock_eu/stock.py +++ b/modules/account_stock_eu/stock.py @@ -10,7 +10,7 @@ from trytond.model import Index, ModelView, Workflow, fields from trytond.modules.account.exceptions import FiscalYearNotFoundError from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval -from trytond.transaction import Transaction, without_check_access +from trytond.transaction import Transaction from .exceptions import CounterPartyNotFound, CountryNotFound @@ -163,19 +163,13 @@ class Move(metaclass=PoolMeta): return self.intrastat_from_country @classmethod - @without_check_access - def _reopen_intrastat(cls, *args): + def _reopen_intrastat(cls, moves): pool = Pool() IntrastatDeclaration = pool.get( 'account.stock.eu.intrastat.declaration') - declarations = set() - actions = iter(args) - for moves, values in zip(actions, actions): - moves = cls.browse(moves) - if any(k.startswith('intrastat_') for k in values.keys()): - declarations.update( - m.intrastat_declaration for m in moves - if m.intrastat_declaration) + declarations = { + m.intrastat_declaration for m in moves + if m.intrastat_declaration} if declarations: IntrastatDeclaration.open( IntrastatDeclaration.browse(declarations)) @@ -205,10 +199,12 @@ class Move(metaclass=PoolMeta): return values @classmethod - def write(cls, *args): - cls._reopen_intrastat(*args) - super().write(*args) - cls._reopen_intrastat(*args) + def on_write(cls, moves, values): + callback = super().on_write(moves, values) + callback.append(lambda: cls._reopen_intrastat(moves)) + if any(f.startswith('intrastat_') for f in values): + cls._reopen_intrastat(moves) + return callback @classmethod def copy(cls, moves, default=None): diff --git a/modules/account_stock_landed_cost/account.py b/modules/account_stock_landed_cost/account.py index 7ca73a9654..518f19b8b1 100644 --- a/modules/account_stock_landed_cost/account.py +++ b/modules/account_stock_landed_cost/account.py @@ -410,29 +410,31 @@ class LandedCost(Workflow, ModelSQL, ModelView, MatchMixin): cls.save(landed_costs) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Config = pool.get('account.configuration') - - vlist = [v.copy() for v in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'landed_cost_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + values = super().preprocess_values(mode, values) + if values.get('number') is None: + config = Config(1) + company_id = values.get('company', cls.default_company()) + if company_id is not None: + if sequence := config.get_multivalue( + 'landed_cost_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod - def delete(cls, landed_costs): - for landed_cost in landed_costs: - if landed_cost.state not in {'cancelled', 'draft'}: - raise AccessError( - gettext('account_stock_landed_cost' - '.msg_landed_cost_delete_cancel', - landed_cost=landed_cost.rec_name)) - super().delete(landed_costs) + def check_modification( + cls, mode, landed_costs, values=None, external=False): + super().check_modification( + mode, landed_costs, values=values, external=external) + if mode == 'delete': + for landed_cost in landed_costs: + if landed_cost.state not in {'cancelled', 'draft'}: + raise AccessError( + gettext('account_stock_landed_cost' + '.msg_landed_cost_delete_cancel', + landed_cost=landed_cost.rec_name)) @classmethod def copy(cls, landed_costs, default=None): diff --git a/modules/account_stock_shipment_cost/account.py b/modules/account_stock_shipment_cost/account.py index 24365afe22..0a7eb120e4 100644 --- a/modules/account_stock_shipment_cost/account.py +++ b/modules/account_stock_shipment_cost/account.py @@ -321,29 +321,31 @@ class ShipmentCost(Workflow, ModelSQL, ModelView): }) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Config = pool.get('account.configuration') - - vlist = [v.copy() for v in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'shipment_cost_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + values = super().preprocess_values(mode, values) + if values.get('number') is None: + config = Config(1) + company_id = values.get('company', cls.default_company()) + if company_id is not None: + if sequence := config.get_multivalue( + 'shipment_cost_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod - def delete(cls, shipment_costs): - for shipment_cost in shipment_costs: - if shipment_cost.state not in {'cancelled', 'draft'}: - raise AccessError( - gettext('account_stock_shipment_cost' - '.msg_shipment_cost_delete_cancel', - shipment_cost=shipment_cost.rec_name)) - super().delete(shipment_costs) + def check_modification( + cls, mode, shipment_costs, values=None, external=False): + super().check_modification( + mode, shipment_costs, values=values, external=external) + if mode == 'delete': + for shipment_cost in shipment_costs: + if shipment_cost.state not in {'cancelled', 'draft'}: + raise AccessError( + gettext('account_stock_shipment_cost' + '.msg_shipment_cost_delete_cancel', + shipment_cost=shipment_cost.rec_name)) class ShipmentCost_Shipment(ModelSQL): diff --git a/modules/account_tax_cash/account.py b/modules/account_tax_cash/account.py index 4b382ed276..3a0613d0ac 100644 --- a/modules/account_tax_cash/account.py +++ b/modules/account_tax_cash/account.py @@ -283,16 +283,10 @@ class Invoice(metaclass=PoolMeta): return ratio @classmethod - def write(cls, *args): - super().write(*args) - - invoices = [] - actions = iter(args) - for records, values in zip(actions, actions): - if 'payment_lines' in values: - invoices.extend(records) - invoices = cls.browse(invoices) - cls._update_tax_cash_basis(invoices) + def on_modification(cls, mode, invoices, field_names=None): + super().on_modification(mode, invoices, field_names=field_names) + if mode == 'write' and 'payment_lines' in field_names: + cls._update_tax_cash_basis(invoices) @classmethod def process(cls, invoices): diff --git a/modules/analytic_account/account.py b/modules/analytic_account/account.py index 8197b1c679..29b7cbb8c8 100644 --- a/modules/analytic_account/account.py +++ b/modules/analytic_account/account.py @@ -305,24 +305,25 @@ class Account( return result @classmethod - def write(cls, *args): + def check_modification(cls, mode, accounts, values=None, external=False): pool = Pool() Entry = pool.get('analytic.account.entry') - actions = iter(args) - for records, values in zip(actions, actions): - if 'root' in values: - for sub_records in grouped_slice(records): - entries = Entry.search([ - ('account', 'in', list(map(int, sub_records))), - ], - limit=1, order=[]) - if entries: - entry, = entries - raise AccessError(gettext( - 'analytic_account' - '.msg_analytic_account_root_change', - account=entry.account.rec_name)) - super().write(*args) + + super().check_modification( + mode, accounts, values=values, external=external) + + if mode == 'write' and 'root' in values: + for sub_records in grouped_slice(accounts): + entries = Entry.search([ + ('account', 'in', list(map(int, sub_records))), + ], + limit=1, order=[]) + if entries: + entry, = entries + raise AccessError(gettext( + 'analytic_account' + '.msg_analytic_account_root_change', + account=entry.account.rec_name)) class AccountContext(ModelView): diff --git a/modules/analytic_account/line.py b/modules/analytic_account/line.py index d0e2533a99..942ccf880f 100644 --- a/modules/analytic_account/line.py +++ b/modules/analytic_account/line.py @@ -105,33 +105,27 @@ class Line(ModelSQL, ModelView): return clause @classmethod - def create(cls, vlist): + def on_modification(cls, mode, lines, field_names=None): pool = Pool() MoveLine = pool.get('account.move.line') - lines = super().create(vlist) - move_lines = [l.move_line for l in lines] - MoveLine.set_analytic_state(move_lines) - MoveLine.save(move_lines) - return lines + super().on_modification(mode, lines, field_names=field_names) + if mode in {'create', 'write'}: + move_lines = MoveLine.browse({l.move_line for l in lines}) + MoveLine.set_analytic_state(move_lines) + MoveLine.save(move_lines) @classmethod - def write(cls, *args): + def on_delete(cls, lines): pool = Pool() MoveLine = pool.get('account.move.line') - super().write(*args) - lines = sum(args[0:None:2], []) - move_lines = [l.move_line for l in lines] - MoveLine.set_analytic_state(move_lines) - MoveLine.save(move_lines) - - @classmethod - def delete(cls, lines): - pool = Pool() - MoveLine = pool.get('account.move.line') - move_lines = [l.move_line for l in lines] - super().delete(lines) - MoveLine.set_analytic_state(move_lines) - MoveLine.save(move_lines) + callback = super().on_delete(lines) + move_lines = MoveLine.browse({l.move_line for l in lines}) + if move_lines: + def set_state(): + MoveLine.set_analytic_state(move_lines) + MoveLine.save(move_lines) + callback.append(set_state) + return callback class Move(metaclass=PoolMeta): diff --git a/modules/attendance/attendance.py b/modules/attendance/attendance.py index ec7f88ed81..5752a54cc3 100644 --- a/modules/attendance/attendance.py +++ b/modules/attendance/attendance.py @@ -98,9 +98,11 @@ class Line(ModelSQL, ModelView): return values @classmethod - def delete(cls, records): - cls.check_closed_period(records, msg='delete') - super().delete(records) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'delete': + cls.check_closed_period(lines, msg='delete') @classmethod def validate_fields(cls, records, field_names): diff --git a/modules/bank/bank.py b/modules/bank/bank.py index 794c5bae12..0d57eef79b 100644 --- a/modules/bank/bank.py +++ b/modules/bank/bank.py @@ -156,15 +156,15 @@ class Account(DeactivableMixin, ModelSQL, ModelView): bic=iban.bic)) @classmethod - def create(cls, vlist): - accounts = super().create(vlist) - for account in accounts: - if not account.bank: - bank = account.guess_bank() - if bank: - account.bank = bank - cls.save(accounts) - return accounts + def on_modification(cls, mode, accounts, field_names=None): + super().on_modification(mode, accounts, field_names=field_names) + if mode == 'create': + for account in accounts: + if not account.bank: + bank = account.guess_bank() + if bank: + account.bank = bank + cls.save(accounts) def guess_bank(self): pool = Pool() diff --git a/modules/carrier/carrier.py b/modules/carrier/carrier.py index 2078ea1ec3..e6dca07fa7 100644 --- a/modules/carrier/carrier.py +++ b/modules/carrier/carrier.py @@ -66,19 +66,12 @@ class Carrier(DeactivableMixin, ModelSQL, ModelView): return None, None @classmethod - def create(cls, *args, **kwargs): + def on_modification(cls, mode, carriers, field_names=None): pool = Pool() CarrierSelection = pool.get('carrier.selection') - carriers = super().create(*args, **kwargs) - CarrierSelection._get_carriers_cache.clear() - return carriers - - @classmethod - def delete(cls, *args, **kwargs): - pool = Pool() - CarrierSelection = pool.get('carrier.selection') - super().delete(*args, **kwargs) - CarrierSelection._get_carriers_cache.clear() + super().on_modification(mode, carriers, field_names=field_names) + if mode in {'create', 'delete'}: + CarrierSelection._get_carriers_cache.clear() class Selection(sequence_ordered(), MatchMixin, ModelSQL, ModelView): @@ -97,22 +90,6 @@ class Selection(sequence_ordered(), MatchMixin, ModelSQL, ModelView): carrier = fields.Many2One('carrier', 'Carrier', required=True, ondelete='CASCADE', help="The selected carrier.") - @classmethod - def create(cls, *args, **kwargs): - selections = super().create(*args, **kwargs) - cls._get_carriers_cache.clear() - return selections - - @classmethod - def write(cls, *args, **kwargs): - super().write(*args, **kwargs) - cls._get_carriers_cache.clear() - - @classmethod - def delete(cls, *args, **kwargs): - super().delete(*args, **kwargs) - cls._get_carriers_cache.clear() - @classmethod def get_carriers(cls, pattern): pool = Pool() @@ -136,3 +113,8 @@ class Selection(sequence_ordered(), MatchMixin, ModelSQL, ModelView): cls._get_carriers_cache.set(key, list(map(int, carriers))) return carriers + + @classmethod + def on_modification(cls, mode, selections, field_names=None): + super().on_modification(mode, selections, field_names=field_names) + cls._get_carriers_cache.clear() diff --git a/modules/carrier_carriage/stock.py b/modules/carrier_carriage/stock.py index d665217745..3e7d78f9bc 100644 --- a/modules/carrier_carriage/stock.py +++ b/modules/carrier_carriage/stock.py @@ -83,13 +83,15 @@ class Carriage(sequence_ordered(), ShipmentCostMixin, ModelSQL, ModelView): return f'{self.carrier.rec_name} @ {self.shipment.rec_name}' @classmethod - def delete(cls, carriages): - for carriage in carriages: - if carriage.shipment_cost_readonly: - raise AccessError( - gettext('carrier_carriages.msg_shipment_carriage_delete', - carriage=carriage.rec_name)) - super().delete(carriages) + def check_modification(cls, mode, carriages, values=None, external=False): + super().check_modification( + mode, carriages, values=values, external=external) + if mode == 'delete': + for carriage in carriages: + if carriage.shipment_cost_readonly: + raise AccessError(gettext( + 'carrier_carriages.msg_shipment_carriage_delete', + carriage=carriage.rec_name)) class _ShipmentMixin: @@ -127,12 +129,14 @@ class _ShipmentMixin: return cost @classmethod - def delete(cls, shipments): + def on_delete(cls, shipments): pool = Pool() Carriage = pool.get('stock.shipment.carriage') + callback = super().on_delete(shipments) carriages = sum((s.carriages for s in shipments), ()) - super().delete(shipments) - Carriage.delete(carriages) + if carriages: + callback.append(lambda: Carriage.delete(carriages)) + return callback @classmethod def copy(cls, shipments, default=None): diff --git a/modules/company/company.py b/modules/company/company.py index 69eb8c2333..d792ae204b 100644 --- a/modules/company/company.py +++ b/modules/company/company.py @@ -99,27 +99,21 @@ class Company(ModelSQL, ModelView): return [('party.rec_name',) + tuple(clause[1:])] @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - if logo := values.get('logo'): - values['logo'] = cls._logo_convert(logo) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if logo := values.get('logo'): + values['logo'] = cls._logo_convert(logo) + return values @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for companies, values in zip(actions, actions): - if logo := values.get('logo'): - values = values.copy() - values['logo'] = cls._logo_convert(logo) - args.append(companies) - args.append(values) - super().write(*args) - cls._logo_clear_cache(sum(args[0:None:2], [])) - # Restart the cache on the domain_get method - Pool().get('ir.rule')._domain_get_cache.clear() + def on_modification(cls, mode, companies, field_names=None): + pool = Pool() + Rule = pool.get('ir.rule') + super().on_modification(mode, companies, field_names=field_names) + if mode == 'write': + if not field_names or 'logo' in field_names: + cls._logo_clear_cache(companies) + Rule._domain_get_cache.clear() @classmethod def _logo_convert(cls, image, **_params): diff --git a/modules/company/res.py b/modules/company/res.py index 44257ece6c..3cb4e90cb0 100644 --- a/modules/company/res.py +++ b/modules/company/res.py @@ -18,25 +18,10 @@ class UserCompany(ModelSQL): ondelete='CASCADE', required=True) @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() User = pool.get('res.user') - records = super().create(vlist) - User._get_companies_cache.clear() - return records - - @classmethod - def write(cls, *args): - pool = Pool() - User = pool.get('res.user') - super().write(*args) - User._get_companies_cache.clear() - - @classmethod - def delete(cls, records): - pool = Pool() - User = pool.get('res.user') - super().delete(records) + super().on_modification(mode, records, field_names=field_names) User._get_companies_cache.clear() @@ -48,25 +33,10 @@ class UserEmployee(ModelSQL): 'company.employee', "Employee", ondelete='CASCADE', required=True) @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() User = pool.get('res.user') - records = super().create(vlist) - User._get_employees_cache.clear() - return records - - @classmethod - def write(cls, *args): - pool = Pool() - User = pool.get('res.user') - super().write(*args) - User._get_employees_cache.clear() - - @classmethod - def delete(cls, records): - pool = Pool() - User = pool.get('res.user') - super().delete(records) + super().on_modification(mode, records, field_names=field_names) User._get_employees_cache.clear() @@ -277,7 +247,8 @@ class User(metaclass=PoolMeta): return result @classmethod - def write(cls, *args): - super().write(*args) - cls._get_companies_cache.clear() - cls._get_employees_cache.clear() + def on_modification(cls, mode, users, field_names=None): + super().on_modification(mode, users, field_names=field_names) + if mode == 'write': + cls._get_companies_cache.clear() + cls._get_employees_cache.clear() diff --git a/modules/country/country.py b/modules/country/country.py index 82a4e98991..e2719c6d66 100644 --- a/modules/country/country.py +++ b/modules/country/country.py @@ -215,25 +215,12 @@ class Country(DeactivableMixin, ModelSQL, ModelView): return self in organization.countries @classmethod - def create(cls, vlist): - vlist = [x.copy() for x in vlist] - for vals in vlist: - for code in {'code', 'code3', 'code_numeric'}: - if code in vals and vals[code]: - vals[code] = vals[code].upper() - return super().create(vlist) - - @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for countries, values in zip(actions, actions): - for code in {'code', 'code3', 'code_numeric'}: - if values.get(code): - values = values.copy() - values[code] = values[code].upper() - args.extend((countries, values)) - super().write(*args) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + for code in {'code', 'code3', 'code_numeric'}: + if values.get(code): + values[code] = values[code].upper() + return values class Subdivision(DeactivableMixin, ModelSQL, ModelView): @@ -465,23 +452,11 @@ class Subdivision(DeactivableMixin, ModelSQL, ModelView): return Operator(column, value) @classmethod - def create(cls, vlist): - vlist = [x.copy() for x in vlist] - for vals in vlist: - if 'code' in vals and vals['code']: - vals['code'] = vals['code'].upper() - return super().create(vlist) - - @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for subdivisions, values in zip(actions, actions): - if values.get('code'): - values = values.copy() - values['code'] = values['code'].upper() - args.extend((subdivisions, values)) - super().write(*args) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if values.get('code'): + values['code'] = values['code'].upper() + return values class PostalCode(ModelSQL, ModelView): diff --git a/modules/currency_rs/currency.py b/modules/currency_rs/currency.py index 3e874b63b4..53c4f80a8b 100644 --- a/modules/currency_rs/currency.py +++ b/modules/currency_rs/currency.py @@ -100,16 +100,19 @@ class Cron(metaclass=PoolMeta): if r.Date == date.strftime('%d.%m.%Y')} @classmethod - def write(cls, *args): + def check_modification(cls, mode, crons, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for crons, values in zip(actions, actions): - if ({'rs_username', 'rs_password', 'rs_license_id'} - & values.keys()): - warning_name = Warning.format('rs_credential', crons) - if Warning.check(warning_name): - raise RSCredentialWarning( - warning_name, - gettext('currency_rs.msg_rs_credential_modified')) - return super().write(*args) + + super().check_modification( + mode, crons, values=values, external=external) + + if (mode == 'write' + and external + and values.keys() & { + 'rs_username', 'rs_password', 'rs_license_id'}): + warning_name = Warning.format('rs_credential', crons) + if Warning.check(warning_name): + raise RSCredentialWarning( + warning_name, + gettext('currency_rs.msg_rs_credential_modified')) diff --git a/modules/customs/product.py b/modules/customs/product.py index 3362decb94..95a651fc35 100644 --- a/modules/customs/product.py +++ b/modules/customs/product.py @@ -76,18 +76,24 @@ class Category(metaclass=PoolMeta): ] @classmethod - def delete(cls, categories): + def on_delete(cls, categories): pool = Pool() Product_TariffCode = pool.get('product-customs.tariff.code') + + callback = super().on_delete(categories) + + product_tariffcodes = set() products = [str(t) for t in categories] - - super().delete(categories) - for products in grouped_slice(products): - product_tariffcodes = Product_TariffCode.search([ - 'product', 'in', list(products), - ]) - Product_TariffCode.delete(product_tariffcodes) + product_tariffcodes.update(Product_TariffCode.search([ + 'product', 'in', list(products), + ])) + if product_tariffcodes: + product_tariffcodes = Product_TariffCode.browse( + product_tariffcodes) + callback.append( + lambda: Product_TariffCode.delete(product_tariffcodes)) + return callback class Template(metaclass=PoolMeta): @@ -145,18 +151,24 @@ class Template(metaclass=PoolMeta): ] @classmethod - def delete(cls, templates): + def on_delete(cls, templates): pool = Pool() Product_TariffCode = pool.get('product-customs.tariff.code') + + callback = super().on_delete(templates) + + product_tariffcodes = set() products = [str(t) for t in templates] - - super().delete(templates) - for products in grouped_slice(products): - product_tariffcodes = Product_TariffCode.search([ - 'product', 'in', list(products), - ]) - Product_TariffCode.delete(product_tariffcodes) + product_tariffcodes.update(Product_TariffCode.search([ + 'product', 'in', list(products), + ])) + if product_tariffcodes: + product_tariffcodes = Product_TariffCode.browse( + product_tariffcodes) + callback.append( + lambda: Product_TariffCode.delete(product_tariffcodes)) + return callback class Product_TariffCode(sequence_ordered(), ModelSQL, ModelView): diff --git a/modules/dashboard/res.py b/modules/dashboard/res.py index b2187bfa86..06388a8623 100644 --- a/modules/dashboard/res.py +++ b/modules/dashboard/res.py @@ -29,12 +29,11 @@ class User(metaclass=PoolMeta): return 'square' @classmethod - def write(cls, *args): + def on_modification(cls, mode, users, field_names=None): pool = Pool() View = pool.get('ir.ui.view') - super().write(*args) - for values in args[1:None:2]: - if values.keys() & {'dashboard_layout', 'dashboard_actions'}: - View._view_get_cache.clear() - ModelView._fields_view_get_cache.clear() - break + super().on_modification(mode, users, field_names=field_names) + if (mode == 'write' + and field_names & {'dashboard_layout', 'dashboard_actions'}): + View._view_get_cache.clear() + ModelView._fields_view_get_cache.clear() diff --git a/modules/document_incoming_ocr/document.py b/modules/document_incoming_ocr/document.py index 3299c7127d..fcd0cd14ce 100644 --- a/modules/document_incoming_ocr/document.py +++ b/modules/document_incoming_ocr/document.py @@ -478,17 +478,6 @@ class IncomingOCRService(sequence_ordered(), ModelSQL, ModelView, MatchMixin): getattr(self, f'_send_feedback_{self.type}')(document) @classmethod - def create(cls, *args, **kwargs): - services = super().create(*args, **kwargs) - cls._get_service_cache.clear() - return services - - @classmethod - def write(cls, *args, **kwargs): - super().write(*args, **kwargs) - cls._get_service_cache.clear() - - @classmethod - def delete(cls, *args, **kwargs): - super().delete(*args, **kwargs) + def on_modification(cls, mode, services, field_names=None): + super().on_modification(mode, services, field_names=field_names) cls._get_service_cache.clear() diff --git a/modules/document_incoming_ocr_typless/document.py b/modules/document_incoming_ocr_typless/document.py index adf6e1f853..3dadfd9c7b 100644 --- a/modules/document_incoming_ocr_typless/document.py +++ b/modules/document_incoming_ocr_typless/document.py @@ -322,16 +322,17 @@ class IncomingOCRService(metaclass=PoolMeta): return str(line.base) @classmethod - def write(cls, *args): + def check_modification(cls, mode, services, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for services, values in zip(actions, actions): - if 'typless_api_key' in values.keys(): - warning_name = Warning.format('typless_credential', services) - if Warning.check(warning_name): - raise TyplessCredentialWarning( - warning_name, - gettext('document_incoming_ocr_typless' - '.msg_typless_credential_modified')) - return super().write(*args) + + super().check_modification( + mode, services, values=values, external=external) + + if mode == 'write' and external and 'typless_api_key' in values: + warning_name = Warning.format('typless_credential', services) + if Warning.check(warning_name): + raise TyplessCredentialWarning( + warning_name, + gettext('document_incoming_ocr_typless' + '.msg_typless_credential_modified')) diff --git a/modules/incoterm/incoterm.py b/modules/incoterm/incoterm.py index 843f3b9df7..e5e8ce78e2 100644 --- a/modules/incoterm/incoterm.py +++ b/modules/incoterm/incoterm.py @@ -70,23 +70,8 @@ class Incoterm_Company(ModelSQL): company = fields.Many2One('company.company', "Company", required=True) @classmethod - def create(cls, *args, **kwargs): + def on_modification(cls, mode, records, field_names=None): pool = Pool() Incoterm = pool.get('incoterm.incoterm') - records = super().create(*args, **kwargs) - Incoterm._get_incoterms_cache.clear() - return records - - @classmethod - def write(cls, *args, **kwargs): - pool = Pool() - Incoterm = pool.get('incoterm.incoterm') - super().write(*args, **kwargs) - Incoterm._get_incoterms_cache.clear() - - @classmethod - def delete(cls, *args, **kwargs): - pool = Pool() - Incoterm = pool.get('incoterm.incoterm') - super().delete(*args, **kwargs) + super().on_modification(mode, records, field_names=field_names) Incoterm._get_incoterms_cache.clear() diff --git a/modules/ldap_authentication/res.py b/modules/ldap_authentication/res.py index fbe35dc1fd..32612083f5 100644 --- a/modules/ldap_authentication/res.py +++ b/modules/ldap_authentication/res.py @@ -12,7 +12,6 @@ from trytond.exceptions import LoginException from trytond.i18n import gettext from trytond.model.exceptions import AccessError from trytond.pool import PoolMeta -from trytond.transaction import without_check_access logger = logging.getLogger(__name__) section = 'ldap_authentication' @@ -100,7 +99,6 @@ class User(metaclass=PoolMeta): for e in result] @classmethod - @without_check_access def _check_passwd_ldap_user(cls, logins): find = False try: @@ -119,23 +117,18 @@ class User(metaclass=PoolMeta): user=login)) @classmethod - def create(cls, vlist): - tocheck = [] - for values in vlist: - if values.get('password') and 'login' in values: - tocheck.append(values['login']) - if tocheck: - cls._check_passwd_ldap_user(tocheck) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and values.get('password') and 'login' in values: + cls._check_passwd_ldap_user([values['login']]) + return values @classmethod - def write(cls, *args): - actions = iter(args) - for users, values in zip(actions, actions): - if values.get('password'): - logins = [x.login for x in users] - cls._check_passwd_ldap_user(logins) - super().write(*args) + def check_modification(cls, mode, users, values=None, external=False): + super().check_modification( + mode, users, values=values, external=external) + if mode == 'write' and values.get('password'): + cls._check_passwd_ldap_user([u.login for u in users]) @classmethod def _login_ldap(cls, login, parameters): diff --git a/modules/marketing_automation/marketing_automation.py b/modules/marketing_automation/marketing_automation.py index b13f059409..02b47a79d8 100644 --- a/modules/marketing_automation/marketing_automation.py +++ b/modules/marketing_automation/marketing_automation.py @@ -852,13 +852,13 @@ class Record(ModelSQL, ModelView): return '(%s)' % self.id @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': # Ensure to get a different uuid for each record # default methods are called only once values.setdefault('uuid', cls.default_uuid()) - return super().create(vlist) + return values class RecordActivity(Workflow, ModelSQL, ModelView): diff --git a/modules/marketing_campaign/marketing.py b/modules/marketing_campaign/marketing.py index db95577106..324ff5038a 100644 --- a/modules/marketing_campaign/marketing.py +++ b/modules/marketing_campaign/marketing.py @@ -31,23 +31,11 @@ class Parameter(DeactivableMixin, ModelSQL, ModelView): return self.name.title() @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - if values.get('name'): - values['name'] = values['name'].lower() - return super().create(vlist) - - @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for parameters, values in zip(actions, actions): - if values.get('name'): - values = values.copy() - values['name'] = values['name'].lower() - args.extend((parameters, values)) - super().write(*args) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if values.get('name'): + values['name'] = values['name'].lower() + return values @classmethod def from_name(cls, name, create=True): diff --git a/modules/marketing_email/marketing.py b/modules/marketing_email/marketing.py index 0fb1965f53..b585735b89 100644 --- a/modules/marketing_email/marketing.py +++ b/modules/marketing_email/marketing.py @@ -113,29 +113,15 @@ class Email(DeactivableMixin, ModelSQL, ModelView): return result @classmethod - def _format_email(cls, records): - for record in records: - email = normalize_email(record.email).lower() - if email != record.email: - record.email = email - cls.save(records) - - @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': # Ensure to get a different token for each record # default methods are called only once values.setdefault('email_token', cls.default_email_token()) - records = super().create(vlist) - cls._format_email(records) - return records - - @classmethod - def write(cls, *args): - super().write(*args) - records = sum(args[0:None:2], []) - cls._format_email(records) + if values.get('email'): + values['email'] = normalize_email(values['email']).lower() + return values @classmethod def validate_fields(cls, records, fields_names): diff --git a/modules/party/address.py b/modules/party/address.py index 4553bcd59b..31afa8a5e5 100644 --- a/modules/party/address.py +++ b/modules/party/address.py @@ -225,16 +225,15 @@ class Address( ] @classmethod - def write(cls, *args): - actions = iter(args) - for addresses, values in zip(actions, actions): - if 'party' in values: - for address in addresses: - if address.party.id != values['party']: - raise AccessError( - gettext('party.msg_address_change_party', - address=address.rec_name)) - super().write(*args) + def check_modification(cls, mode, addresses, values=None, external=False): + super().check_modification( + mode, addresses, values=values, external=external) + if mode == 'write' and 'party' in values: + for address in addresses: + if address.party.id != values['party']: + raise AccessError(gettext( + 'party.msg_address_change_party', + address=address.rec_name)) @fields.depends('subdivision', 'country') def on_change_country(self): @@ -299,19 +298,8 @@ ${subdivision} ${COUNTRY}""" @classmethod - def create(cls, *args, **kwargs): - records = super().create(*args, **kwargs) - cls._get_format_cache.clear() - return records - - @classmethod - def write(cls, *args, **kwargs): - super().write(*args, **kwargs) - cls._get_format_cache.clear() - - @classmethod - def delete(cls, *args, **kwargs): - super().delete(*args, **kwargs) + def on_modification(cls, mode, formats, field_names=None): + super().on_modification(mode, formats, field_names=field_names) cls._get_format_cache.clear() @classmethod @@ -386,22 +374,6 @@ class SubdivisionType(DeactivableMixin, ModelSQL, ModelView): selection = Subdivision.fields_get(['type'])['type']['selection'] return [(k, v) for k, v in selection if k is not None] - @classmethod - def create(cls, *args, **kwargs): - records = super().create(*args, **kwargs) - cls._get_types_cache.clear() - return records - - @classmethod - def write(cls, *args, **kwargs): - super().write(*args, **kwargs) - cls._get_types_cache.clear() - - @classmethod - def delete(cls, *args, **kwargs): - super().delete(*args, **kwargs) - cls._get_types_cache.clear() - @classmethod def get_types(cls, country): key = country.code if country else None @@ -420,3 +392,8 @@ class SubdivisionType(DeactivableMixin, ModelSQL, ModelView): cls._get_types_cache.set(key, types) return types + + @classmethod + def on_modification(cls, mode, types, field_names=None): + super().on_modification(mode, types, field_names=field_names) + cls._get_types_cache.clear() diff --git a/modules/party/configuration.py b/modules/party/configuration.py index fc95571d90..8e7c3b480e 100644 --- a/modules/party/configuration.py +++ b/modules/party/configuration.py @@ -64,19 +64,8 @@ class Configuration(ModelSingleton, ModelSQL, ModelView, MultiValueMixin): return selection @classmethod - def create(cls, vlist): - records = super().create(vlist) - ModelView._fields_view_get_cache.clear() - return records - - @classmethod - def write(cls, *args): - super().write(*args) - ModelView._fields_view_get_cache.clear() - - @classmethod - def delete(cls, records): - super().delete(records) + def on_modification(cls, mode, configurations, field_names=None): + super().on_modification(mode, configurations, field_names=field_names) ModelView._fields_view_get_cache.clear() @classmethod diff --git a/modules/party/contact_mechanism.py b/modules/party/contact_mechanism.py index f6a74b9493..a1a6511dce 100644 --- a/modules/party/contact_mechanism.py +++ b/modules/party/contact_mechanism.py @@ -285,20 +285,15 @@ class ContactMechanism( return values @classmethod - def write(cls, *args): - actions = iter(args) - all_mechanisms = [] - for mechanisms, values in zip(actions, actions): - all_mechanisms.extend(mechanisms) - if 'party' in values: - for mechanism in mechanisms: - if mechanism.party.id != values['party']: - raise AccessError( - gettext('party' - '.msg_contact_mechanism_change_party') % { - 'contact': mechanism.rec_name, - }) - super().write(*args) + def check_modification(cls, mode, mechanisms, values=None, external=False): + super().check_modification( + mode, mechanisms, values=values, external=external) + if mode == 'write' and 'party' in values: + for mechanism in mechanisms: + if mechanism.party.id != values['party']: + raise AccessError(gettext( + 'party.msg_contact_mechanism_change_party', + contact=mechanism.rec_name)) @classmethod def validate_fields(cls, mechanisms, field_names): diff --git a/modules/party/party.py b/modules/party/party.py index b42973c313..5f09c076a6 100644 --- a/modules/party/party.py +++ b/modules/party/party.py @@ -286,19 +286,14 @@ class Party( return config.get_multivalue('party_sequence', **pattern) @classmethod - def create(cls, vlist): - vlist = [x.copy() for x in vlist] - missing_code = [] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': if not values.get('code'): - missing_code.append(values) + if sequence := cls._code_sequence(): + values['code'] = sequence.get() values.setdefault('addresses', None) - if missing_code: - if sequence := cls._code_sequence(): - for values, code in zip( - missing_code, sequence.get_many(len(missing_code))): - values['code'] = code - return super().create(vlist) + return values @classmethod def copy(cls, parties, default=None): diff --git a/modules/party_relationship/party.py b/modules/party_relationship/party.py index 3e7d67c76e..19887421a2 100644 --- a/modules/party_relationship/party.py +++ b/modules/party_relationship/party.py @@ -195,8 +195,9 @@ class RelationAll(Relation, ModelView): def create(cls, vlist): pool = Pool() Relation = pool.get('party.relation') + vlist = cls._before_create(vlist) relations = Relation.create(vlist) - return cls.browse([r.id * 2 for r in relations]) + return cls.browse(cls._after_create([r.id * 2 for r in relations])) @classmethod def write(cls, *args): @@ -204,23 +205,19 @@ class RelationAll(Relation, ModelView): Relation = pool.get('party.relation') RelationType = pool.get('party.relation.type') - all_records = sum(args[0:None:2], []) + ids, field_names, on_write, trigger_eligibles, *args = ( + cls._before_write(*args)) - # Increase transaction counter - Transaction().counter += 1 - - # Clean local cache - for record in all_records: - for record_id in (record.id, record.reverse_id): - record._local_cache.pop(record_id, None) - - # Clean cursor cache + relations = cls.browse(ids) + # Clean local cache of reverse + for relation in relations: + relation._local_cache.pop(relation.reverse_id, None) + # Clean cursor cache of reverse for cache in Transaction().cache.values(): if cls.__name__ in cache: cache_cls = cache[cls.__name__] - for record in all_records: - for record_id in (record.id, record.reverse_id): - cache_cls.pop(record_id, None) + for relation in relations: + cache_cls.pop(relation.reverse_id, None) actions = iter(args) args = [] @@ -247,25 +244,23 @@ class RelationAll(Relation, ModelView): (cls.convert_instances(reverse_relations), reverse_values)) Relation.write(*args) + cls._after_write(ids, field_names, on_write, trigger_eligibles) + @classmethod def delete(cls, relations): pool = Pool() Relation = pool.get('party.relation') + ids, on_delete = cls._before_delete(relations) - # Increase transaction counter - Transaction().counter += 1 - - # Clean cursor cache + # Clean cursor cache of reverse for cache in list(Transaction().cache.values()): - for cache in (cache, - list(cache.get('_language_cache', {}).values())): - if cls.__name__ in cache: - cache_cls = cache[cls.__name__] - for record in relations: - for record_id in (record.id, record.reverse_id): - cache_cls.pop(record_id, None) + if cls.__name__ in cache: + cache_cls = cache[cls.__name__] + for relation in relations: + cache_cls.pop(relation.reverse_id, None) Relation.delete(cls.convert_instances(relations)) + cls._after_delete(ids, on_delete) class Party(metaclass=PoolMeta): diff --git a/modules/product/category.py b/modules/product/category.py index b70d938956..420962785a 100644 --- a/modules/product/category.py +++ b/modules/product/category.py @@ -97,15 +97,12 @@ class Category(tree(separator=' / '), ModelSQL, ModelView): return config.category_sequence @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - missing_code = [v for v in vlist if not v.get('code')] - if missing_code: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('code'): if sequence := cls._code_sequence(): - for values, code in zip( - missing_code, sequence.get_many(len(missing_code))): - values['code'] = code - return super().create(vlist) + values['code'] = sequence.get() + return values @classmethod def copy(cls, categories, default=None): diff --git a/modules/product/product.py b/modules/product/product.py index a8da7ececa..231f6743ce 100644 --- a/modules/product/product.py +++ b/modules/product/product.py @@ -239,33 +239,23 @@ class Template( return config.template_sequence @classmethod - def create(cls, vlist): - pool = Pool() - Product = pool.get('product.product') - vlist = [v.copy() for v in vlist] - missing_code = [] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': values.setdefault('products', None) if not values.get('code'): - missing_code.append(values) - if missing_code: - if sequence := cls._code_sequence(): - for values, code in zip( - missing_code, sequence.get_many(len(missing_code))): - values['code'] = code - templates = super().create(vlist) - products = sum((t.products for t in templates), ()) - Product.sync_code(products) - return templates + if sequence := cls._code_sequence(): + values['code'] = sequence.get() + return values @classmethod - def write(cls, *args): + def on_modification(cls, mode, templates, field_names=None): pool = Pool() Product = pool.get('product.product') - super().write(*args) - templates = sum(args[0:None:2], []) - products = sum((t.products for t in templates), ()) - Product.sync_code(products) + super().on_modification(mode, templates, field_names=field_names) + if mode in {'create', 'write'}: + products = sum((t.products for t in templates), ()) + Product.sync_code(products) @classmethod def copy(cls, templates, default=None): @@ -673,23 +663,18 @@ class Product( return config.product_sequence @classmethod - def create(cls, vlist): - vlist = [x.copy() for x in vlist] - missing_code = [v for v in vlist if not v.get('suffix_code')] - if missing_code: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('suffix_code'): if sequence := cls._code_sequence(): - for values, code in zip( - missing_code, sequence.get_many(len(missing_code))): - values['suffix_code'] = code - products = super().create(vlist) - cls.sync_code(products) - return products + values['suffix_code'] = sequence.get() + return values @classmethod - def write(cls, *args): - super().write(*args) - products = sum(args[0:None:2], []) - cls.sync_code(products) + def on_modification(cls, mode, products, field_names=None): + super().on_modification(mode, products, field_names=field_names) + if mode in {'create', 'write'}: + cls.sync_code(products) @classmethod def copy(cls, products, default=None): diff --git a/modules/product/uom.py b/modules/product/uom.py index ecb597ef68..dba6752b27 100644 --- a/modules/product/uom.py +++ b/modules/product/uom.py @@ -10,7 +10,6 @@ from trytond.model import ( Check, DeactivableMixin, DigitsMixin, ModelSQL, ModelView, SymbolMixin, fields) from trytond.pyson import Eval, If -from trytond.transaction import Transaction from .exceptions import UOMAccessError, UOMValidationError @@ -150,30 +149,30 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView): uom=uom.rec_name)) @classmethod - def write(cls, *args): - if Transaction().user == 0: - super().write(*args) - return - - all_uoms = sum(args[0:None:2], []) - old_uom = { - uom.id: (uom.factor, uom.rate, uom.category) for uom in all_uoms} - old_digits = {uom.id: uom.digits for uom in all_uoms} - - super().write(*args) - - for uom in all_uoms: - for i, field in enumerate(['factor', 'rate', 'category']): - if getattr(uom, field) != old_uom[uom.id][i]: - raise UOMAccessError( - gettext('product.msg_uom_modify_%s' % field, - uom=uom.rec_name), - gettext('product.msg_uom_modify_options')) - if uom.digits < old_digits[uom.id]: - raise UOMAccessError( - gettext('product.msg_uom_decrease_digits', - uom=uom.rec_name), - gettext('product.msg_uom_modify_options')) + def check_modification(cls, mode, uoms, values=None, external=False): + super().check_modification( + mode, uoms, values=values, external=external) + if (mode == 'write' + and values.keys() & {'factor', 'rate', 'category', 'digits'}): + for uom in uoms: + for field_name in values.keys() & {'factor', 'rate'}: + if values[field_name] != getattr(uom, field_name): + raise UOMAccessError(gettext( + 'product.msg_uom_modify_%s' % field_name, + uom=uom.rec_name), + gettext('product.msg_uom_modify_options')) + if 'category' in values: + if values['category'] != uom.category.id: + raise UOMAccessError(gettext( + 'product.msg_uom_modify_category', + uom=uom.rec_name), + gettext('product.msg_uom_modify_options')) + if 'digits' in values: + if values['digits'] < uom.digits: + raise UOMAccessError(gettext( + 'product.msg_uom_decrease_digits', + uom=uom.rec_name), + gettext('product.msg_uom_modify_options')) @property def accurate_field(self): diff --git a/modules/product_cost_fifo/move.py b/modules/product_cost_fifo/move.py index b8c3d0888b..0c86144600 100644 --- a/modules/product_cost_fifo/move.py +++ b/modules/product_cost_fifo/move.py @@ -155,13 +155,15 @@ class Move(metaclass=PoolMeta): super().cancel(moves) @classmethod - def delete(cls, moves): - for move in moves: - if move.fifo_quantity: - raise AccessError( - gettext('product_cost_fifo.msg_move_delete_fifo', - move=move.rec_name)) - super().delete(moves) + def check_modification(cls, mode, moves, values=None, external=False): + super().check_modification( + mode, moves, values=values, external=external) + if mode == 'delete': + for move in moves: + if move.fifo_quantity: + raise AccessError(gettext( + 'product_cost_fifo.msg_move_delete_fifo', + move=move.rec_name)) @classmethod def copy(cls, moves, default=None): diff --git a/modules/product_image/product.py b/modules/product_image/product.py index 37c5f5df8a..21b3fbf83e 100644 --- a/modules/product_image/product.py +++ b/modules/product_image/product.py @@ -123,25 +123,17 @@ class ImageMixin(_ImageMixin): return set() @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - if values.get('image'): - values['image'] = cls.convert(values['image']) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if values.get('image'): + values['image'] = cls.convert(values['image']) + return values @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for images, values in zip(actions, actions): - if values.get('image'): - values = values.copy() - values['image'] = cls.convert(values['image']) - args.append(images) - args.append(values) - super().write(*args) - cls.clear_cache(sum(args[0:None:2], [])) + def on_modification(cls, mode, images, field_names=None): + super().on_modification(mode, images, field_names=field_names) + if mode == 'write': + cls.clear_cache(images) @classmethod def _round_size(cls, size): diff --git a/modules/product_price_list_cache/product.py b/modules/product_price_list_cache/product.py index 462398e7a0..7ed9ac3425 100644 --- a/modules/product_price_list_cache/product.py +++ b/modules/product_price_list_cache/product.py @@ -180,16 +180,6 @@ class PriceListCache(ModelSQL): cache.product, [p.id for p in sub_products]))) @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_cache.clear() - return super().create(vlist) - - @classmethod - def write(cls, *args): - cls._get_cache.clear() - return super().write(*args) - - @classmethod - def delete(cls, records): - cls._get_cache.clear() - return super().delete(records) diff --git a/modules/production/bom.py b/modules/production/bom.py index 918c65fa76..ab4c8ce7cf 100644 --- a/modules/production/bom.py +++ b/modules/production/bom.py @@ -94,15 +94,12 @@ class BOM(DeactivableMixin, ModelSQL, ModelView): return config.bom_sequence @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - missing_code = [v for v in vlist if not v.get('code')] - if missing_code: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('code'): if sequence := cls._code_sequence(): - for values, code in zip( - missing_code, sequence.get_many(len(missing_code))): - values['code'] = code - return super().create(vlist) + values['code'] = sequence.get() + return values @classmethod def copy(cls, records, default=None): @@ -205,27 +202,28 @@ class BOMOutput(BOMInput): return self.unit.floor(self.quantity * factor) @classmethod - def delete(cls, outputs): + def on_delete(cls, outputs): pool = Pool() ProductBOM = pool.get('product.product-production.bom') + + callback = super().on_delete(outputs) + bom_products = [b for o in outputs for b in o.product.boms] - super().delete(outputs) # Validate that output_products domain on bom is still valid - ProductBOM._validate(bom_products, ['bom']) + callback.append(lambda: ProductBOM._validate(bom_products, ['bom'])) + return callback @classmethod - def write(cls, *args): + def on_write(cls, outputs, values): pool = Pool() ProductBOM = pool.get('product.product-production.bom') - actions = iter(args) - bom_products = [] - for outputs, values in zip(actions, actions): - if 'product' in values: - bom_products.extend( - [b for o in outputs for b in o.product.boms]) - super().write(*args) + + callback = super().on_write(outputs, values) + + bom_products = [b for o in outputs for b in o.product.boms] # Validate that output_products domain on bom is still valid - ProductBOM._validate(bom_products, ['bom']) + callback.append(lambda: ProductBOM._validate(bom_products, ['bom'])) + return callback class BOMTree(ModelView): diff --git a/modules/production/production.py b/modules/production/production.py index 481f451468..e87629af6b 100644 --- a/modules/production/production.py +++ b/modules/production/production.py @@ -650,15 +650,18 @@ class Production(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): cls.save(productions) @classmethod - def create(cls, vlist): - productions = super().create(vlist) - cls._set_move_planned_date(productions) - return productions - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) + def on_modification(cls, mode, productions, field_names=None): + pool = Pool() + Move = pool.get('stock.move') + super().on_modification(mode, productions, field_names=field_names) + if mode in {'create', 'write'}: + cls._set_move_planned_date(productions) + elif mode == 'delete': + moves = [] + for production in productions: + moves.extend(production.inputs) + moves.extend(production.outputs) + Move.delete(moves) @classmethod def copy(cls, productions, default=None): diff --git a/modules/production/stock.py b/modules/production/stock.py index 926420c463..d2327647cf 100644 --- a/modules/production/stock.py +++ b/modules/production/stock.py @@ -125,19 +125,12 @@ class Move(metaclass=PoolMeta): super().set_effective_date() @classmethod - def write(cls, *args): - super().write(*args) - cost_price_update = [] - actions = iter(args) - for moves, values in zip(actions, actions): - for move in moves: - if (move.state == 'done' - and move.production_input - and 'cost_price' in values): - cost_price_update.append(move) - if cost_price_update: + def on_modification(cls, mode, moves, field_names=None): + super().on_modification(mode, moves, field_names=field_names) + if mode == 'write' and 'cost_price' in field_names: cls.write( - cost_price_update, {'production_cost_price_updated': True}) + [m for m in moves if m.state == 'done' and m.production_input], + {'production_cost_price_updated': True}) class ProductQuantitiesByWarehouseMove(metaclass=PoolMeta): diff --git a/modules/production_work/work.py b/modules/production_work/work.py index 1c18fe6116..a4cab6c7d8 100644 --- a/modules/production_work/work.py +++ b/modules/production_work/work.py @@ -264,25 +264,21 @@ class Work(sequence_ordered(), ModelSQL, ModelView): return costs @classmethod - def create(cls, values): - works = super().create(values) - cls.set_state(works) - return works + def on_modification(cls, mode, works, field_names=None): + super().on_modification(mode, works, field_names=field_names) + if mode in {'create', 'write'}: + cls.set_state(works) @classmethod - def write(cls, *args): - super().write(*args) - works = sum(args[0:None:2], []) - cls.set_state(works) - - @classmethod - def delete(cls, works): - for work in works: - if work.state not in {'request', 'draft'}: - raise AccessError( - gettext('production_work.msg_delete_request', - work=work.rec_name)) - super().delete(works) + def check_modification(cls, mode, works, values=None, external=False): + super().check_modification( + mode, works, values=values, external=external) + if mode == 'delete': + for work in works: + if work.state not in {'request', 'draft'}: + raise AccessError( + gettext('production_work.msg_delete_request', + work=work.rec_name)) def set_work_state(func): @@ -290,8 +286,9 @@ def set_work_state(func): def wrapper(cls, cycles): pool = Pool() Work = pool.get('production.work') - func(cls, cycles) + result = func(cls, cycles) Work.set_state(Work.browse({c.work.id for c in cycles})) + return result return wrapper diff --git a/modules/production_work_timesheet/work.py b/modules/production_work_timesheet/work.py index ec06cbb4da..d9bd6ca7bd 100644 --- a/modules/production_work_timesheet/work.py +++ b/modules/production_work_timesheet/work.py @@ -58,28 +58,19 @@ class Work(metaclass=PoolMeta): }) @classmethod - def create(cls, vlist): - works = super().create(vlist) - cls._set_timesheet_work(works) - return works + def on_modification(cls, mode, works, field_names=None): + super().on_modification(mode, works, field_names=field_names) + if mode in {'create', 'write'}: + cls._set_timesheet_work(works) @classmethod - def write(cls, *args): - super().write(*args) - works = sum(args[0:None:2], []) - cls._set_timesheet_work(works) - - @classmethod - def delete(cls, works): + def on_delete(cls, works): pool = Pool() TimesheetWork = pool.get('timesheet.work') - + callback = super().on_delete(works) timesheet_works = [w for pw in works for w in pw.timesheet_works] - - super().delete(works) - - if timesheet_works: - TimesheetWork.delete(timesheet_works) + callback.append(lambda: TimesheetWork.delete(timesheet_works)) + return callback @classmethod def _set_timesheet_work(cls, works): diff --git a/modules/project/work.py b/modules/project/work.py index 45b4790cbd..51c3489652 100644 --- a/modules/project/work.py +++ b/modules/project/work.py @@ -11,7 +11,7 @@ from trytond.model import ( sum_tree, tree) from trytond.pool import Pool from trytond.pyson import Bool, Eval, If, PYSONEncoder, TimeDelta -from trytond.transaction import Transaction, without_check_access +from trytond.transaction import Transaction from .exceptions import WorkProgressValidationError @@ -68,22 +68,10 @@ class WorkStatus(DeactivableMixin, sequence_ordered(), ModelSQL, ModelView): return status @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_default_status_cache.clear() cls._get_window_domains_cache.clear() - return super().create(vlist) - - @classmethod - def write(cls, *args): - super().write(*args) - cls._get_default_status_cache.clear() - cls._get_window_domains_cache.clear() - - @classmethod - def delete(cls, status): - cls._get_default_status_cache.clear() - cls._get_window_domains_cache.clear() - super().delete(status) @classmethod def get_window_domains(cls, action): @@ -428,18 +416,16 @@ class Work(sequence_ordered(), tree(separator='\\'), ModelSQL, ModelView): return new_works @classmethod - def delete(cls, project_works): - TimesheetWork = Pool().get('timesheet.work') - - # Get the timesheet works linked to the project works - timesheet_works = [ - w for pw in project_works for w in pw.timesheet_works] - - super().delete(project_works) - + def on_delete(cls, project_works): + pool = Pool() + TimesheetWork = pool.get('timesheet.work') + callback = super().on_delete(project_works) + timesheet_works = { + w for pw in project_works for w in pw.timesheet_works} if timesheet_works: - with without_check_access(): - TimesheetWork.delete(timesheet_works) + timesheet_works = TimesheetWork.browse(timesheet_works) + callback.append(lambda: TimesheetWork.delete(timesheet_works)) + return callback @classmethod def search_global(cls, text): diff --git a/modules/project_invoice/invoice.py b/modules/project_invoice/invoice.py index e2bfa1722a..314a4975ad 100644 --- a/modules/project_invoice/invoice.py +++ b/modules/project_invoice/invoice.py @@ -11,7 +11,7 @@ from trytond.modules.account_invoice.exceptions import ( InvoiceLineValidationError) from trytond.pool import Pool, PoolMeta from trytond.tools import grouped_slice, reduce_ids -from trytond.transaction import Transaction, without_check_access +from trytond.transaction import Transaction class InvoiceLine(metaclass=PoolMeta): @@ -115,19 +115,3 @@ class InvoiceLine(metaclass=PoolMeta): default.setdefault('project_invoice_works') default.setdefault('project_invoice_progresses') return super().copy(lines, default=default) - - @classmethod - def delete(cls, lines): - pool = Pool() - WorkInvoicedProgress = pool.get('project.work.invoiced_progress') - - progress = [] - for sub_ids in grouped_slice([l.id for l in lines]): - progress += WorkInvoicedProgress.search([ - ('invoice_line', 'in', sub_ids), - ]) - if progress: - with without_check_access(): - WorkInvoicedProgress.delete(progress) - - super().delete(lines) diff --git a/modules/project_invoice/project.py b/modules/project_invoice/project.py index f6fda068a2..eca2165fae 100644 --- a/modules/project_invoice/project.py +++ b/modules/project_invoice/project.py @@ -426,25 +426,22 @@ class Work(Effort, Progress, Timesheet, metaclass=PoolMeta): return super().copy(records, default=default) @classmethod - def write(cls, *args): - actions = iter(args) - for works, values in zip(actions, actions): + def check_modification(cls, mode, works, values=None, external=False): + super().check_modification( + mode, works, values=values, external=external) + if mode == 'write': if ('effort_duration' in values and any(w.invoice_line for w in works)): work = next((w for w in works if w.invoice_line)) raise AccessError(gettext( 'project_invoice.msg_invoiced_work_modify_effort', work=work.rec_name)) - super().write(*args) - - @classmethod - def delete(cls, works): - if any(w.invoice_line for w in works): - work = next((w for w in works if w.invoice_line)) - raise AccessError(gettext( - 'project_invoice.msg_invoiced_work_delete', - work=work.rec_name)) - super().delete(works) + elif mode == 'delete': + if any(w.invoice_line for w in works): + work = next((w for w in works if w.invoice_line)) + raise AccessError(gettext( + 'project_invoice.msg_invoiced_work_delete', + work=work.rec_name)) @classmethod def get_invoice_methods(cls): diff --git a/modules/project_invoice/timesheet.py b/modules/project_invoice/timesheet.py index c020c0b1b9..6f60d481be 100644 --- a/modules/project_invoice/timesheet.py +++ b/modules/project_invoice/timesheet.py @@ -21,10 +21,11 @@ class Line(metaclass=PoolMeta): return super().copy(records, default=default) @classmethod - def write(cls, *args): - actions = iter(args) - for lines, values in zip(actions, actions): - if (('duration' in values or 'work' in values) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'write': + if (values.keys() & {'duration', 'work'} and any(l.invoice_line for l in lines)): line = next(l for l in lines if l.invoice_line) if 'duration' in values: @@ -34,13 +35,9 @@ class Line(metaclass=PoolMeta): raise AccessError(gettext( f'project_invoice.{msg}', line=line.rec_name)) - super().write(*args) - - @classmethod - def delete(cls, lines): - if any(r.invoice_line for r in lines): - line = next((l for l in lines if l.invoice_line)) - raise AccessError(gettext( - 'project_invoice.msg_invoiced_timesheet_line_delete', - line=line.rec_name)) - super().delete(lines) + elif mode == 'delete': + if any(r.invoice_line for r in lines): + line = next((l for l in lines if l.invoice_line)) + raise AccessError(gettext( + 'project_invoice.msg_invoiced_timesheet_line_delete', + line=line.rec_name)) diff --git a/modules/project_plan/allocation.py b/modules/project_plan/allocation.py index 21fab6508e..11df998b74 100644 --- a/modules/project_plan/allocation.py +++ b/modules/project_plan/allocation.py @@ -1,7 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from trytond.model import ModelSQL, ModelView, fields -from trytond.pool import Pool class Allocation(ModelSQL, ModelView): @@ -26,34 +25,21 @@ class Allocation(ModelSQL, ModelView): return [('employee.rec_name',) + tuple(clause[1:])] @classmethod - def write(cls, *args): - Work = Pool().get('project.work') - super().write(*args) - - works = Work.search([ - ('allocations', 'in', - [a.id for allocations in args[::2] for a in allocations]), - ]) - - for work in works: - work.reset_leveling() - for work in works: - work.compute_dates() + def on_modification(cls, mode, allocations, field_names=None): + super().on_modification(mode, allocations, field_names=field_names) + if mode in {'create', 'write'}: + for allocation in allocations: + allocation.work.reset_leveling() + allocation.work.compute_dates() @classmethod - def create(cls, vlist): - allocations = super().create(vlist) - for allocation in allocations: - allocation.work.reset_leveling() - allocation.work.compute_dates() - return allocations - - @classmethod - def delete(cls, allocations): + def on_delete(cls, allocations): + callback = super().on_delete(allocations) works = [a.work for a in allocations] - super().delete(allocations) - for work in works: - work.reset_leveling() - for work in works: - work.compute_dates() + def replan(): + for work in works: + work.reset_leveling() + work.compute_dates() + callback.append(replan) + return callback diff --git a/modules/project_plan/work.py b/modules/project_plan/work.py index 0c90c15e8a..dcaaf7863d 100644 --- a/modules/project_plan/work.py +++ b/modules/project_plan/work.py @@ -6,7 +6,7 @@ from functools import reduce from heapq import heappop, heappush from trytond.model import ModelSQL, fields, tree -from trytond.pool import Pool, PoolMeta +from trytond.pool import PoolMeta from trytond.pyson import Eval from trytond.wizard import StateTransition, Wizard @@ -550,41 +550,33 @@ class Work(tree(parent='successors'), metaclass=PoolMeta): self.parent.compute_dates() @classmethod - def write(cls, *args): - super().write(*args) - - actions = iter(args) - for works, values in zip(actions, actions): - if 'effort' in values: - for work in works: + def on_modification(cls, mode, works, field_names=None): + super().on_modification(mode, works, field_names=field_names) + if mode in {'create', 'write'}: + for work in works: + if not field_names or 'effort' in field_names: work.reset_leveling() - fields = ('constraint_start_time', 'constraint_finish_time', - 'effort') - if reduce(lambda x, y: x or y in values, fields, False): - for work in works: + if not field_names or field_names & { + 'constraint_start_time', 'constraint_finish_time', + 'effort'}: work.compute_dates() @classmethod - def create(cls, vlist): - works = super().create(vlist) - for work in works: - work.reset_leveling() - work.compute_dates() - return works - - @classmethod - def delete(cls, works): + def on_delete(cls, works): + callback = super().on_delete(works) to_update = set() for work in works: if work.parent and work.parent not in works: to_update.add(work.parent) - to_update.update(c for c in work.parent.children - if c not in works) - super().delete(works) + to_update.update( + c for c in work.parent.children if c not in works) - for work in to_update: - work.reset_leveling() - work.compute_dates() + def replan(): + for work in to_update: + work.reset_leveling() + work.compute_dates() + callback.append(replan) + return callback class PredecessorSuccessor(ModelSQL): @@ -595,49 +587,35 @@ class PredecessorSuccessor(ModelSQL): 'project.work', "Successor", ondelete='CASCADE', required=True) @classmethod - def write(cls, *args): - Work = Pool().get('project.work') - super().write(*args) - - work_ids = [v for values in args[1::2] - for k, v in values.values() - if k in ('predecessor', 'successor')] - works = Work.browse(work_ids) - for work in works: - work.reset_leveling() - for work in works: - work.compute_dates() + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if mode == 'create': + if not field_names or field_names & { + 'predecessor', 'successor', 'parent'}: + for record in records: + record.predecessor.reset_leveling() + record.successor.reset_leveling() + if record.predecessor.parent: + record.predecessor.parent.compute_dates() @classmethod - def delete(cls, pred_succs): + def on_delete(cls, records): + callback = super().on_delete(records) works = set() parents = set() - for pred_succ in pred_succs: - works.update((pred_succ.predecessor, - pred_succ.successor)) + for record in records: + works.add(record.predecessor) + works.add(record.successor) + if record.predecessor.parent: + parents.add(record.predecessor.parent) - if pred_succ.predecessor.parent: - parents.add(pred_succ.predecessor.parent) - - super().delete(pred_succs) - - for work in works: - work.reset_leveling() - - for parent in parents: - parent.compute_dates() - - @classmethod - def create(cls, vlist): - pred_succs = super().create(vlist) - - for pred_succ in pred_succs: - pred_succ.predecessor.reset_leveling() - pred_succ.successor.reset_leveling() - - if pred_succ.predecessor.parent: - pred_succ.predecessor.parent.compute_dates() - return pred_succs + def replan(): + for work in works: + work.reset_leveling() + for parent in parents: + parent.compute_dates() + callback.append(replan) + return callback class Leveling(Wizard): diff --git a/modules/purchase/invoice.py b/modules/purchase/invoice.py index 82ec2bd644..9fd260a359 100644 --- a/modules/purchase/invoice.py +++ b/modules/purchase/invoice.py @@ -21,11 +21,12 @@ def process_purchase(func): with without_check_access(): purchases = set( p for i in cls.browse(invoices) for p in i.purchases) - func(cls, invoices) + result = func(cls, invoices) if purchases: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Purchase.__queue__.process(purchases) + return result return wrapper @@ -68,8 +69,8 @@ class Invoice(metaclass=PoolMeta): @classmethod @process_purchase - def delete(cls, invoices): - super().delete(invoices) + def on_delete(cls, invoices): + return super().on_delete(invoices) @classmethod @ModelView.button @@ -160,7 +161,7 @@ class InvoiceLine(metaclass=PoolMeta): return models @classmethod - def delete(cls, lines): + def on_delete(cls, lines): pool = Pool() Purchase = pool.get('purchase.purchase') transaction = Transaction() @@ -169,8 +170,8 @@ class InvoiceLine(metaclass=PoolMeta): invoices = (l.invoice for l in cls.browse(lines) if l.type == 'line' and l.invoice) purchases = set(p for i in invoices for p in i.purchases) - super().delete(lines) if purchases: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Purchase.__queue__.process(purchases) + return super().on_delete(lines) diff --git a/modules/purchase/product.py b/modules/purchase/product.py index 6e3dabb4e8..366d2bffc4 100644 --- a/modules/purchase/product.py +++ b/modules/purchase/product.py @@ -65,13 +65,14 @@ class Template(metaclass=PoolMeta): yield product_supplier @classmethod - def write(cls, *args): + def check_modification(cls, mode, templates, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for templates, values in zip(actions, actions): - if not values.get("purchase_uom"): - continue + + super().check_modification( + mode, templates, values=values, external=external) + + if mode == 'write' and values.get("purchase_uom"): for template in templates: if not template.purchase_uom: continue @@ -84,7 +85,6 @@ class Template(metaclass=PoolMeta): if Warning.check(name): raise PurchaseUOMWarning( name, gettext('purchase.msg_change_purchase_uom')) - super().write(*args) @classmethod def copy(cls, templates, default=None): diff --git a/modules/purchase/purchase.py b/modules/purchase/purchase.py index e58b7b4afc..b9b25912bc 100644 --- a/modules/purchase/purchase.py +++ b/modules/purchase/purchase.py @@ -895,15 +895,15 @@ class Purchase( if l.moves_progress is not None)))) @classmethod - def delete(cls, purchases): - # Cancel before delete - cls.cancel(purchases) - for purchase in purchases: - if purchase.state != 'cancelled': - raise AccessError( - gettext('purchase.msg_purchase_delete_cancel', - purchase=purchase.rec_name)) - super().delete(purchases) + def check_modification(cls, mode, purchases, values=None, external=False): + super().check_modification( + mode, purchases, values=values, external=external) + if mode == 'delete': + for purchase in purchases: + if purchase.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'purchase.msg_purchase_delete_cancel', + purchase=purchase.rec_name)) @classmethod @ModelView.button @@ -2105,26 +2105,22 @@ class Line(sequence_ordered(), ModelSQL, ModelView): ] @classmethod - def create(cls, vlist): - pool = Pool() - Purchase = pool.get('purchase.purchase') - purchase_ids = filter(None, {v.get('purchase') for v in vlist}) - for purchase in Purchase.browse(list(purchase_ids)): - if purchase.state != 'draft': - raise AccessError( - gettext('purchase.msg_purchase_line_create_draft', - purchase=purchase.rec_name)) - return super().create(vlist) - - @classmethod - def delete(cls, lines): - for line in lines: - if line.purchase_state not in {'cancelled', 'draft'}: - raise AccessError( - gettext('purchase.msg_purchase_line_delete_cancel_draft', - line=line.rec_name, - purchase=line.purchase.rec_name)) - super().delete(lines) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'create': + for line in lines: + if line.purchase.state != 'draft': + raise AccessError(gettext( + 'purchase.msg_purchase_line_create_draft', + purchase=line.purchase.rec_name)) + elif mode == 'delete': + for line in lines: + if line.purchase_state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'purchase.msg_purchase_line_delete_cancel_draft', + line=line.rec_name, + purchase=line.purchase.rec_name)) @classmethod def copy(cls, lines, default=None): diff --git a/modules/purchase/stock.py b/modules/purchase/stock.py index 4991eb206f..e38a404473 100644 --- a/modules/purchase/stock.py +++ b/modules/purchase/stock.py @@ -22,11 +22,12 @@ def process_purchase(moves_field): with without_check_access(): purchases = set(m.purchase for s in cls.browse(shipments) for m in getattr(s, moves_field) if m.purchase) - func(cls, shipments) + result = func(cls, shipments) if purchases: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Purchase.__queue__.process(purchases) + return result return wrapper return _process_purchase @@ -124,11 +125,11 @@ def process_purchase_move(without_shipment=False): if without_shipment: p_moves = [m for m in p_moves if not m.shipment] purchases = set(m.purchase for m in p_moves if m.purchase) - func(cls, moves) if purchases: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Purchase.__queue__.process(purchases) + return func(cls, moves) return wrapper return _process_purchase_move @@ -250,8 +251,8 @@ class Move(metaclass=PoolMeta): @classmethod @process_purchase_move() - def delete(cls, moves): - super().delete(moves) + def on_delete(cls, moves): + return super().on_delete(moves) class Location(metaclass=PoolMeta): diff --git a/modules/purchase_request/purchase.py b/modules/purchase_request/purchase.py index b5d0ae6fd8..ec85e1faac 100644 --- a/modules/purchase_request/purchase.py +++ b/modules/purchase_request/purchase.py @@ -19,13 +19,14 @@ def process_request(func): def wrapper(cls, purchases): pool = Pool() Request = pool.get('purchase.request') - func(cls, purchases) + result = func(cls, purchases) with without_check_access(): requests = [ r for p in cls.browse(purchases) for l in p.lines for r in l.requests] Request.update_state(requests) + return result return wrapper @@ -33,19 +34,16 @@ class Purchase(metaclass=PoolMeta): __name__ = 'purchase.purchase' @classmethod - def delete(cls, purchases): - cls.check_delete_purchase_request(purchases) - super().delete(purchases) - - @classmethod - @without_check_access - def check_delete_purchase_request(cls, purchases): - for purchase in purchases: - for line in purchase.lines: - if line.requests: - raise AccessError( - gettext('purchase_request.msg_purchase_delete_request', - purchase=purchase.rec_name)) + def check_modification(cls, mode, purchases, values=None, external=False): + super().check_modification( + mode, purchases, values=values, external=external) + if mode == 'delete': + for purchase in purchases: + for line in purchase.lines: + if line.requests: + raise AccessError(gettext( + 'purchase_request.msg_purchase_delete_request', + purchase=purchase.rec_name)) @classmethod @ModelView.button @@ -103,11 +101,12 @@ class Line(metaclass=PoolMeta): return super().copy(lines, default=default) @classmethod - def delete(cls, lines): + def on_delete(cls, lines): pool = Pool() Request = pool.get('purchase.request') - with without_check_access(): - requests = [r for l in cls.browse(lines) for r in l.requests] - super().delete(lines) - with without_check_access(): - Request.update_state(requests) + callback = super().on_delete(lines) + requests = {r for l in lines for r in l.requests} + if requests: + requests = Request.browse(requests) + callback.append(lambda: Request.update_state(requests)) + return callback diff --git a/modules/purchase_request/purchase_request.py b/modules/purchase_request/purchase_request.py index 8012094f53..44f0977b34 100644 --- a/modules/purchase_request/purchase_request.py +++ b/modules/purchase_request/purchase_request.py @@ -268,21 +268,19 @@ class PurchaseRequest(ModelSQL, ModelView): ] @classmethod - def create(cls, vlist): - transaction = Transaction() - if transaction.user and transaction.check_access: - raise AccessError( - gettext('purchase_request.msg_request_no_create')) - return super().create(vlist) - - @classmethod - def delete(cls, requests): - for request in requests: - if request.purchase_line: + def check_modification(cls, mode, requests, values=None, external=False): + super().check_modification( + mode, requests, values=values, external=external) + if mode == 'create': + if external: raise AccessError( - gettext('purchase_request.msg_request_delete_purchased', - request=request.rec_name)) - super().delete(requests) + gettext('purchase_request.msg_request_no_create')) + elif mode == 'delete': + for request in requests: + if request.purchase_line: + raise AccessError(gettext( + 'purchase_request.msg_request_delete_purchased', + request=request.rec_name)) @classmethod def copy(cls, requests, default=None): diff --git a/modules/purchase_request_quotation/purchase.py b/modules/purchase_request_quotation/purchase.py index d7289145c7..9af3862c04 100644 --- a/modules/purchase_request_quotation/purchase.py +++ b/modules/purchase_request_quotation/purchase.py @@ -27,9 +27,10 @@ def process_request(func): def wrapper(cls, quotations): pool = Pool() Request = pool.get('purchase.request') - func(cls, quotations) + result = func(cls, quotations) requests = [l.request for q in quotations for l in q.lines] Request.update_state(requests) + return result return wrapper @@ -411,12 +412,15 @@ class QuotationLine(ModelSQL, ModelView): return domain @classmethod - def delete(cls, quotationlines): + def on_delete(cls, lines): pool = Pool() Request = pool.get('purchase.request') - requests = [l.request for l in quotationlines] - super().delete(quotationlines) - Request.update_state(requests) + callback = super().on_delete(lines) + requests = {l.request for l in lines} + if requests: + requests = Request.browse(requests) + callback.append(lambda: Request.update_state(requests)) + return callback def get_product(self, name): if self.request and self.request.product: diff --git a/modules/purchase_requisition/purchase.py b/modules/purchase_requisition/purchase.py index 7eb47a2607..b8273d7491 100644 --- a/modules/purchase_requisition/purchase.py +++ b/modules/purchase_requisition/purchase.py @@ -283,30 +283,31 @@ class PurchaseRequisition(Workflow, ModelSQL, ModelView): ] @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Config = pool.get('purchase.configuration') - - config = Config(1) - vlist = [v.copy() for v in vlist] - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'purchase_requisition_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + Configuration = pool.get('purchase.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'purchase_requisition_sequence', + company=company_id): + values['number'] = sequence.get() + return values @classmethod - def delete(cls, requisitions): - # Cancel before delete - cls.cancel(requisitions) - for requisition in requisitions: - if requisition.state != 'cancelled': - raise AccessError( - gettext('purchase_requisition.msg_delete_cancel', - requisition=requisition.rec_name)) - super().delete(requisitions) + def check_modification( + cls, mode, requisitions, values=None, external=False): + super().check_modification( + mode, requisitions, values=values, external=external) + if mode == 'delete': + for requisition in requisitions: + if requisition.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'purchase_requisition.msg_delete_cancel', + requisition=requisition.rec_name)) def check_for_waiting(self): if not self.warehouse: diff --git a/modules/quality/quality.py b/modules/quality/quality.py index eb699dd27f..550818b5de 100644 --- a/modules/quality/quality.py +++ b/modules/quality/quality.py @@ -563,19 +563,30 @@ class Inspection(Workflow, ModelSQL, ModelView): origin=self) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('quality.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'inspection_sequence', company=company_id): + values['number'] = sequence.get() + return values - config = Configuration(1) - default_company = cls.default_company() - vlist = [v.copy() for v in vlist] - for values in vlist: - if not values.get('number'): - values['number'] = config.get_multivalue( - 'inspection_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + @classmethod + def check_modification( + cls, mode, inspections, values=None, external=False): + super().check_modification( + mode, inspections, values=values, external=external) + if mode == 'delete': + for inspection in inspections: + if inspection.state != 'pending': + raise AccessError(gettext( + 'quality.msg_inspection_delete_non_pending', + inspection=inspection.rec_name)) @classmethod def copy(cls, inspections, default=None): @@ -587,15 +598,6 @@ class Inspection(Workflow, ModelSQL, ModelView): default.setdefault('failed_by') return super().copy(inspections, default=default) - @classmethod - def delete(cls, inspections): - for inspection in inspections: - if inspection.state != 'pending': - raise AccessError( - gettext('quality.msg_inspection_delete_non_pending', - inspection=inspection.rec_name)) - super().delete(inspections) - @classmethod def get_from_control(cls, control, origin=None): return cls(control=control, origin=origin) @@ -721,19 +723,18 @@ class Alert(Workflow, ModelSQL, ModelView): pass @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('quality.configuration') - - config = Configuration(1) - default_company = cls.default_company() - vlist = [v.copy() for v in vlist] - for values in vlist: - if not values.get('number'): - values['number'] = config.get_multivalue( - 'alert_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'alert_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, inspections, default=None): diff --git a/modules/sale/invoice.py b/modules/sale/invoice.py index c649ff02e7..d3fe395218 100644 --- a/modules/sale/invoice.py +++ b/modules/sale/invoice.py @@ -19,11 +19,12 @@ def process_sale(func): context = transaction.context with without_check_access(): sales = set(s for i in cls.browse(invoices) for s in i.sales) - func(cls, invoices) + result = func(cls, invoices) if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return result return wrapper @@ -77,8 +78,8 @@ class Invoice(metaclass=PoolMeta): @classmethod @process_sale - def delete(cls, invoices): - super().delete(invoices) + def on_delete(cls, invoices): + return super().on_delete(invoices) @classmethod @process_sale @@ -158,7 +159,7 @@ class Line(metaclass=PoolMeta): return models @classmethod - def delete(cls, lines): + def on_delete(cls, lines): pool = Pool() Sale = pool.get('sale.sale') transaction = Transaction() @@ -167,8 +168,8 @@ class Line(metaclass=PoolMeta): invoices = (l.invoice for l in cls.browse(lines) if l.type == 'line' and l.invoice) sales = set(s for i in invoices for s in i.sales) - super().delete(lines) if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return super().on_delete(lines) diff --git a/modules/sale/sale.py b/modules/sale/sale.py index 8b932361e8..a913de616e 100644 --- a/modules/sale/sale.py +++ b/modules/sale/sale.py @@ -1096,15 +1096,15 @@ class Sale( if l.moves_progress is not None)))) @classmethod - def delete(cls, sales): - # Cancel before delete - cls.cancel(sales) - for sale in sales: - if sale.state != 'cancelled': - raise AccessError( - gettext('sale.msg_sale_delete_cancel', - sale=sale.rec_name)) - super().delete(sales) + def check_modification(cls, mode, sales, values=None, external=False): + super().check_modification( + mode, sales, values=values, external=external) + if mode == 'delete': + for sale in sales: + if sale.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'sale.msg_sale_delete_cancel', + sale=sale.rec_name)) @classmethod @ModelView.button @@ -2240,26 +2240,22 @@ class SaleLine(TaxableMixin, sequence_ordered(), ModelSQL, ModelView): 'spell', Eval('_parent_sale', {}).get('party_lang'))] @classmethod - def create(cls, vlist): - pool = Pool() - Sale = pool.get('sale.sale') - sale_ids = filter(None, {v.get('sale') for v in vlist}) - for sale in Sale.browse(list(sale_ids)): - if sale.state != 'draft': - raise AccessError( - gettext('sale.msg_sale_line_create_draft', - sale=sale.rec_name)) - return super().create(vlist) - - @classmethod - def delete(cls, lines): - for line in lines: - if line.sale_state not in {'cancelled', 'draft'}: - raise AccessError( - gettext('sale.msg_sale_line_delete_cancel_draft', - line=line.rec_name, - sale=line.sale.rec_name)) - super().delete(lines) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'create': + for line in lines: + if line.sale.state != 'draft': + raise AccessError(gettext( + 'sale.msg_sale_line_create_draft', + sale=line.sale.rec_name)) + elif mode == 'delete': + for line in lines: + if line.sale_state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'sale.msg_sale_line_delete_cancel_draft', + line=line.rec_name, + sale=line.sale.rec_name)) @classmethod def copy(cls, lines, default=None): diff --git a/modules/sale/stock.py b/modules/sale/stock.py index b30e5031aa..c7b38af286 100644 --- a/modules/sale/stock.py +++ b/modules/sale/stock.py @@ -22,11 +22,12 @@ def process_sale(moves_field): with without_check_access(): sales = set(m.sale for s in cls.browse(shipments) for m in getattr(s, moves_field) if m.sale) - func(cls, shipments) + result = func(cls, shipments) if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return result return wrapper return _process_sale @@ -112,11 +113,12 @@ def process_sale_move(func): context = transaction.context with without_check_access(): sales = set(m.sale for m in cls.browse(moves) if m.sale) - func(cls, moves) + result = func(cls, moves) if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return result return wrapper @@ -230,5 +232,5 @@ class Move(metaclass=PoolMeta): @classmethod @process_sale_move - def delete(cls, moves): - super().delete(moves) + def on_delete(cls, moves): + return super().on_delete(moves) diff --git a/modules/sale_complaint/complaint.py b/modules/sale_complaint/complaint.py index 55580bcc28..a9cb31f9a1 100644 --- a/modules/sale_complaint/complaint.py +++ b/modules/sale_complaint/complaint.py @@ -279,19 +279,29 @@ class Complaint(Workflow, ModelSQL, ModelView): ] @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('sale.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'complaint_sequence', company=company_id): + values['number'] = sequence.get() + return values - config = Configuration(1) - vlist = [v.copy() for v in vlist] - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'complaint_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + @classmethod + def check_modification(cls, mode, complaints, values=None, external=False): + super().check_modification( + mode, complaints, values=values, external=external) + if mode == 'delete': + for complaint in complaints: + if complaint.state != 'draft': + raise AccessError(gettext( + 'sale_complaint.msg_complaint_delete_draft', + complaint=complaint.rec_name)) @classmethod def copy(cls, complaints, default=None): @@ -306,15 +316,6 @@ class Complaint(Workflow, ModelSQL, ModelView): default.setdefault('cancelled_by') return super().copy(complaints, default=default) - @classmethod - def delete(cls, complaints): - for complaint in complaints: - if complaint.state != 'draft': - raise AccessError( - gettext('sale_complaint.msg_complaint_delete_draft', - complaint=complaint.rec_name)) - super().delete(complaints) - @classmethod @ModelView.button @Workflow.transition('cancelled') @@ -721,13 +722,15 @@ class Action(ModelSQL, ModelView): return credit_note @classmethod - def delete(cls, actions): - for action in actions: - if action.result: - raise AccessError( - gettext('sale_complaint.msg_action_delete_result', - action=action.rec_name)) - super().delete(actions) + def check_modification(cls, mode, actions, values=None, external=False): + super().check_modification( + mode, actions, values=values, external=external) + if mode == 'delete': + for action in actions: + if action.result: + raise AccessError(gettext( + 'sale_complaint.msg_action_delete_result', + action=action.rec_name)) class _Action_Line: diff --git a/modules/sale_opportunity/opportunity.py b/modules/sale_opportunity/opportunity.py index 4c55b75b97..750d349829 100644 --- a/modules/sale_opportunity/opportunity.py +++ b/modules/sale_opportunity/opportunity.py @@ -300,19 +300,30 @@ class SaleOpportunity( } @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Config = pool.get('sale.configuration') + Configuration = pool.get('sale.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'sale_opportunity_sequence', company=company_id): + values['number'] = sequence.get() + return values - config = Config(1) - vlist = [x.copy() for x in vlist] - default_company = cls.default_company() - for vals in vlist: - if vals.get('number') is None: - vals['number'] = config.get_multivalue( - 'sale_opportunity_sequence', - company=vals.get('company', default_company)).get() - return super().create(vlist) + @classmethod + def check_modification( + cls, mode, opportunities, values=None, external=False): + super().check_modification( + mode, opportunities, values=values, external=external) + if mode == 'delete': + for opportunity in opportunities: + if opportunity.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'sale_opportunity.msg_opportunity_delete_cancel', + opportunity=opportunity.rec_name)) @classmethod def copy(cls, opportunities, default=None): @@ -371,17 +382,6 @@ class SaleOpportunity( sale.lines = sale_lines return sale - @classmethod - def delete(cls, opportunities): - # Cancel before delete - cls.cancel(opportunities) - for opportunity in opportunities: - if opportunity.state != 'cancelled': - raise AccessError( - gettext('sale_opportunity.msg_opportunity_delete_cancel', - opportunity=opportunity.rec_name)) - super().delete(opportunities) - @classmethod @ModelView.button @Workflow.transition('lead') diff --git a/modules/sale_opportunity/sale.py b/modules/sale_opportunity/sale.py index 273a8ce45f..c025db69d9 100644 --- a/modules/sale_opportunity/sale.py +++ b/modules/sale_opportunity/sale.py @@ -17,9 +17,10 @@ def process_opportunity(func): opportunities = Opportunity.browse( set(s.origin for s in cls.browse(sales) if isinstance(s.origin, Opportunity))) - func(cls, sales) + result = func(cls, sales) with without_check_access(): Opportunity.process(opportunities) + return result return wrapper @@ -31,28 +32,24 @@ class Sale(metaclass=PoolMeta): return super()._get_origin() + ['sale.opportunity'] @classmethod - def write(cls, *args): + def check_modification(cls, mode, sales, values=None, external=False): pool = Pool() Opportunity = pool.get('sale.opportunity') - actions = iter(args) - for sales, values in zip(actions, actions): - if 'origin' in values: - origin = values['origin'] - if origin and not isinstance(origin, str): - origin = '%s,%s' % tuple(origin) - for sale in sales: - if (isinstance(sale.origin, Opportunity) - and str(sale.origin) != origin): - raise AccessError(gettext( - 'sale_opportunity' - '.msg_modify_origin_opportunity', - sale=sale.rec_name)) - super().write(*args) - @classmethod - @process_opportunity - def delete(cls, sales): - super().delete(sales) + super().check_modification( + mode, sales, values=values, external=external) + + if mode == 'write' and 'origin' in values: + origin = values['origin'] + if origin and not isinstance(origin, str): + origin = '%s,%s' % tuple(origin) + for sale in sales: + if (isinstance(sale.origin, Opportunity) + and str(sale.origin) != origin): + raise AccessError(gettext( + 'sale_opportunity' + '.msg_modify_origin_opportunity', + sale=sale.rec_name)) @classmethod @process_opportunity diff --git a/modules/sale_payment/account.py b/modules/sale_payment/account.py index 74513a7635..cfe633b688 100644 --- a/modules/sale_payment/account.py +++ b/modules/sale_payment/account.py @@ -70,18 +70,20 @@ class Payment(metaclass=PoolMeta): self.currency = currency @classmethod - def create(cls, vlist): - payments = super().create(vlist) - cls.trigger_authorized([p for p in payments if p.is_authorized]) - return payments + def on_modification(cls, mode, payments, field_names=None): + super().on_modification(mode, payments, field_names=field_names) + if mode == 'create': + cls.trigger_authorized([p for p in payments if p.is_authorized]) @classmethod - def write(cls, *args): - payments = sum(args[0:None:2], []) - unauthorized = {p for p in payments if not p.is_authorized} - super().write(*args) - authorized = {p for p in payments if p.is_authorized} - cls.trigger_authorized(cls.browse(unauthorized & authorized)) + def on_write(cls, payments, values): + callback = super().on_write(payments, values) + if unauthorized := {p for p in payments if not p.is_authorized}: + def trigger(): + authorized = {p for p in payments if p.is_authorized} + cls.trigger_authorized(cls.browse(unauthorized & authorized)) + callback.append(trigger) + return callback @property def is_authorized(self): # TODO: move to account_payment diff --git a/modules/sale_point/sale.py b/modules/sale_point/sale.py index a899565396..14be9265e8 100644 --- a/modules/sale_point/sale.py +++ b/modules/sale_point/sale.py @@ -72,24 +72,24 @@ class POS(ModelSQL, ModelView): return True @classmethod - def write(cls, *args): + def check_modification(cls, mode, points, values=None, external=False): pool = Pool() Sale = pool.get('sale.point.sale') - transaction = Transaction() - if transaction.user and transaction.check_access: - actions = iter(args) - for points, values in zip(actions, actions): - if 'tax_included' in values: - for sub_points in grouped_slice(points): - if Sale.search([ - ('point', 'in', - list(map(int, sub_points))), - ], - limit=1, order=[]): - raise AccessError(gettext( - 'sale_point.msg_point_change_tax_included') - ) - super().write(*args) + + super().check_modification( + mode, points, values=values, external=external) + + if mode == 'write': + if 'tax_included' in values: + for sub_points in grouped_slice(points): + if Sale.search([ + ('point', 'in', + list(map(int, sub_points))), + ], + limit=1, order=[]): + raise AccessError(gettext( + 'sale_point.' + 'msg_point_change_tax_included')) class POSSale(Workflow, ModelSQL, ModelView, TaxableMixin): @@ -276,13 +276,15 @@ class POSSale(Workflow, ModelSQL, ModelView, TaxableMixin): return super().copy(sales, default=default) @classmethod - def delete(cls, sales): - for sale in sales: - if sale.number: - raise AccessError( - gettext('sale_point.msg_sale_delete_numbered', - sale=sale.rec_name)) - super().delete(sales) + def check_modification(cls, mode, sales, values=None, external=False): + super().check_modification( + mode, sales, values=values, external=external) + if mode == 'delete': + for sale in sales: + if sale.number: + raise AccessError(gettext( + 'sale_point.msg_sale_delete_numbered', + sale=sale.rec_name)) @classmethod def validate(cls, sales): @@ -909,14 +911,21 @@ class POSCashSession(Workflow, ModelSQL, ModelView): session.currency))) @classmethod - def delete(cls, sessions): - cls.open(sessions) - for session in sessions: - if session.state != 'open': - raise AccessError( - gettext('sale_point.msg_cash_session_delete_open', - session=session.rec_name)) - super().delete(sessions) + def on_modification(cls, mode, sessions, field_names=None): + super().on_modification(mode, sessions, field_names=field_names) + if mode == 'delete': + cls.open(sessions) + + @classmethod + def check_modification(cls, mode, sessions, values=None, external=False): + super().check_modification( + mode, sessions, values=values, external=external) + if mode == 'delete': + for session in sessions: + if session.state != 'open': + raise AccessError(gettext( + 'sale_point.msg_cash_session_delete_open', + session=session.rec_name)) @classmethod @ModelView.button @@ -1054,34 +1063,36 @@ class POSPayment(ModelSQL, ModelView): return self.method.cash @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Method = pool.get('sale.point.payment.method') Sale = pool.get('sale.point.sale') Session = pool.get('sale.point.cash.session') - vlist = [v.copy() for v in vlist] - for values in vlist: + values = super().preprocess_values(mode, values) + if mode == 'create': if values.get('method') and values.get('sale'): method = Method(values['method']) if method.cash: sale = Sale(values['sale']) values['session'] = Session.get_current(sale.point) - return super().create(vlist) + return values @classmethod - def delete(cls, payments): - for payment in payments: - if payment.sale.state != 'open': - raise AccessError( - gettext('sale_point.msg_payment_delete_sale_open', - payment=payment.rec_name, - sale=payment.sale.rec_name)) - if payment.session and payment.session.state != 'open': - raise AccessError( - gettext('sale_point.msg_payment_delete_session_open', - payment=payment.rec_name, - session=payment.session.rec_name)) - super().delete(payments) + def check_modification(cls, mode, payments, values=None, external=False): + super().check_modification( + mode, payments, values=values, external=external) + if mode == 'delete': + for payment in payments: + if payment.sale.state != 'open': + raise AccessError(gettext( + 'sale_point.msg_payment_delete_sale_open', + payment=payment.rec_name, + sale=payment.sale.rec_name)) + if payment.session and payment.session.state != 'open': + raise AccessError(gettext( + 'sale_point.msg_payment_delete_session_open', + payment=payment.rec_name, + session=payment.session.rec_name)) def get_account_move_lines(self): "Return account move lines" @@ -1258,18 +1269,20 @@ class POSCashTransfer(Workflow, ModelSQL, ModelView): return self.point.company.currency if self.point else None @classmethod - def delete(cls, transfers): - for transfer in transfers: - if transfer.state == 'posted': - raise AccessError( - gettext('sale_point.msg_transfer_delete_posted', - transfer=transfer.rec_name)) - if transfer.session.state != 'open': - raise AccessError( - gettext('sale_point.msg_transfer_delete_session_open', - transfer=transfer.rec_name, - session=transfer.session.rec_name)) - super().delete(transfers) + def check_modification(cls, mode, transfers, values=None, external=False): + super().check_modification( + mode, transfers, values=values, external=external) + if mode == 'delete': + for transfer in transfers: + if transfer.state == 'posted': + raise AccessError(gettext( + 'sale_point.msg_transfer_delete_posted', + transfer=transfer.rec_name)) + if transfer.session.state != 'open': + raise AccessError(gettext( + 'sale_point.msg_transfer_delete_session_open', + transfer=transfer.rec_name, + session=transfer.session.rec_name)) @classmethod @ModelView.button @@ -1320,16 +1333,16 @@ class POSCashTransfer(Workflow, ModelSQL, ModelView): return move @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Point = pool.get('sale.point') Session = pool.get('sale.point.cash.session') - vlist = [v.copy() for v in vlist] - for values in vlist: - if values.get('point') and not values.get('session'): + values = super().preprocess_values(mode, values) + if mode == 'create': + if values.get('point') is not None and not values.get('session'): point = Point(values['point']) values['session'] = Session.get_current(point) - return super().create(vlist) + return values class POSCashTransferType(ModelSQL, ModelView): diff --git a/modules/sale_promotion_coupon/sale.py b/modules/sale_promotion_coupon/sale.py index 8fa2a0bf20..242efea6aa 100644 --- a/modules/sale_promotion_coupon/sale.py +++ b/modules/sale_promotion_coupon/sale.py @@ -45,16 +45,12 @@ class Promotion(metaclass=PoolMeta): return super().match(pattern) @classmethod - def write(cls, *args): + def on_modification(cls, mode, promotions, field_names=None): pool = Pool() Number = pool.get('sale.promotion.coupon.number') - super().write(*args) - actions = iter(args) - numbers = [] - for promotions, values in zip(actions, actions): - if {'start_date', 'end_date'} & values.keys(): - numbers.extend(cls._update_coupon_number_dates(promotions)) - Number.save(numbers) + super().on_modification(mode, promotions, field_names=field_names) + if mode == 'write' and field_names & {'start_date', 'end_date'}: + Number.save(list(cls._update_coupon_number_dates(promotions))) @classmethod def _update_coupon_number_dates(cls, promotions): diff --git a/modules/sale_subscription/subscription.py b/modules/sale_subscription/subscription.py index 382b45c87d..77dac71e41 100644 --- a/modules/sale_subscription/subscription.py +++ b/modules/sale_subscription/subscription.py @@ -871,25 +871,17 @@ class LineConsumption(ModelSQL, ModelView): return super().copy(consumptions, default=default) @classmethod - def write(cls, *args): - for consumptions in args[::2]: + def check_modification( + cls, mode, consumptions, values=None, external=False): + super().check_modification( + mode, consumptions, values=values, external=external) + if mode in {'write', 'delete'}: for consumption in consumptions: if consumption.invoice_line: - raise AccessError( - gettext('sale_subscription' - '.msg_consumption_modify_invoiced', + raise AccessError(gettext( + 'sale_subscription.' + 'msg_consumption_modify_invoiced', consumption=consumption.rec_name)) - super().write(*args) - - @classmethod - def delete(cls, consumptions): - for consumption in consumptions: - if consumption.invoice_line: - raise AccessError( - gettext('sale_subscription' - '.msg_consumption_modify_invoiced', - consumption=consumption.rec_name)) - super().delete(consumptions) @classmethod def get_invoice_lines(cls, consumptions, invoice): diff --git a/modules/sale_supply/purchase.py b/modules/sale_supply/purchase.py index 2b7d11ed37..9b793b8816 100644 --- a/modules/sale_supply/purchase.py +++ b/modules/sale_supply/purchase.py @@ -38,7 +38,7 @@ class Request(metaclass=PoolMeta): return super().copy(requests, default=default) @classmethod - def delete(cls, requests): + def on_delete(cls, requests): pool = Pool() Sale = pool.get('sale.sale') SaleLine = pool.get('sale.line') @@ -54,9 +54,8 @@ class Request(metaclass=PoolMeta): 'purchase_request': None, }) - super().delete(requests) - if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return super().on_delete(requests) diff --git a/modules/sale_supply_drop_shipment/message.xml b/modules/sale_supply_drop_shipment/message.xml index e77aa6d7fd..2f7e145735 100644 --- a/modules/sale_supply_drop_shipment/message.xml +++ b/modules/sale_supply_drop_shipment/message.xml @@ -9,9 +9,6 @@ this repository contains the full copyright notices and license terms. --> You cannot reset move "%(move)s" to draft because it was generated by a sale or a purchase. - - To delete drop shipment "%(shipment)s" you must cancel it. - To split move from drop shipment, you must use the split wizard. diff --git a/modules/sale_supply_drop_shipment/stock.py b/modules/sale_supply_drop_shipment/stock.py index b579b50f07..ec9ac35b3d 100644 --- a/modules/sale_supply_drop_shipment/stock.py +++ b/modules/sale_supply_drop_shipment/stock.py @@ -243,25 +243,18 @@ class ShipmentDrop( Move.write(*to_write) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Config = pool.get('stock.configuration') - - vlist = [x.copy() for x in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - values['number'] = config.get_multivalue( - 'shipment_drop_sequence', - company=values.get('company', default_company)).get() - shipments = super().create(vlist) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) + Configuration = pool.get('stock.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'shipment_drop_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, shipments, default=None): @@ -274,22 +267,6 @@ class ShipmentDrop( default.setdefault('customer_moves', None) return super().copy(shipments, default=default) - @classmethod - def delete(cls, shipments): - pool = Pool() - Move = pool.get('stock.move') - - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('sale_supply_drop_shipment' - '.msg_drop_shipment_delete_cancel', - shipment=shipment.rec_name, - )) - Move.delete([m for s in shipments for m in s.supplier_moves]) - super().delete(shipments) - @classmethod @ModelView.button @Workflow.transition('cancelled') diff --git a/modules/sale_supply_production/production.py b/modules/sale_supply_production/production.py index 1d59d7eb9e..8966526207 100644 --- a/modules/sale_supply_production/production.py +++ b/modules/sale_supply_production/production.py @@ -26,11 +26,12 @@ def process_sale_supply(func): sales.update([s.id for s in Sale.search([ ('lines.productions', 'in', ids), ])]) - func(cls, productions) + result = func(cls, productions) if sales: with transaction.set_context( queue_batch=context.get('queue_batch', True)): Sale.__queue__.process(sales) + return result return wrapper @@ -43,8 +44,8 @@ class Production(metaclass=PoolMeta): @classmethod @process_sale_supply - def delete(cls, productions): - super().delete(productions) + def on_delete(cls, productions): + return super().on_delete(productions) @classmethod @ModelView.button diff --git a/modules/stock/inventory.py b/modules/stock/inventory.py index e2d833d8ac..fb8fddfac8 100644 --- a/modules/stock/inventory.py +++ b/modules/stock/inventory.py @@ -147,17 +147,6 @@ class Inventory(Workflow, ModelSQL, ModelView): ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] - @classmethod - def delete(cls, inventories): - # Cancel before delete - cls.cancel(inventories) - for inventory in inventories: - if inventory.state != 'cancelled': - raise AccessError( - gettext('stock.msg_inventory_delete_cancel', - inventory=inventory.rec_name)) - super().delete(inventories) - @classmethod @ModelView.button @Workflow.transition('done') @@ -215,26 +204,36 @@ class Inventory(Workflow, ModelSQL, ModelView): Line.cancel_move([l for i in inventories for l in i.lines]) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Configuration = pool.get('stock.configuration') - config = Configuration(1) - vlist = [x.copy() for x in vlist] - default_company = cls.default_company() - for values in vlist: - if values.get('number') is None: - values['number'] = config.get_multivalue( - 'inventory_sequence', - company=values.get('company', default_company)).get() - inventories = super().create(vlist) - cls.complete_lines(inventories, fill=False) - return inventories + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'inventory_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod - def write(cls, *args): - super().write(*args) - inventories = cls.browse(set(sum(args[::2], []))) - cls.complete_lines(inventories, fill=False) + def on_modification(cls, mode, inventories, field_names=None): + super().on_modification(mode, inventories, field_names=field_names) + if mode in {'create', 'write'}: + cls.complete_lines(inventories, fill=False) + + @classmethod + def check_modification( + cls, mode, inventories, values=None, external=False): + super().check_modification( + mode, inventories, values=values, external=external) + if mode == 'delete': + for inventory in inventories: + if inventory.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'stock.msg_inventory_delete_cancel', + inventory=inventory.rec_name)) @classmethod def copy(cls, inventories, default=None): @@ -249,9 +248,7 @@ class Inventory(Workflow, ModelSQL, ModelView): default.setdefault('lines.moves', None) default.setdefault('number', None) - new_inventories = super().copy(inventories, default=default) - cls.complete_lines(new_inventories, fill=False) - return new_inventories + return super().copy(inventories, default=default) @staticmethod def grouping(): @@ -514,14 +511,16 @@ class InventoryLine(ModelSQL, ModelView): self.expected_quantity = quantity @classmethod - def delete(cls, lines): - for line in lines: - if line.inventory_state not in {'cancelled', 'draft'}: - raise AccessError( - gettext('stock.msg_inventory_line_delete_cancel', - line=line.rec_name, - inventory=line.inventory.rec_name)) - super().delete(lines) + def check_modification(cls, mode, lines, values=None, external=False): + super().check_modification( + mode, lines, values=values, external=external) + if mode == 'delete': + for line in lines: + if line.inventory_state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'stock.msg_inventory_line_delete_cancel', + line=line.rec_name, + inventory=line.inventory.rec_name)) class Count(Wizard): diff --git a/modules/stock/location.py b/modules/stock/location.py index 10738d8230..158e39a11e 100644 --- a/modules/stock/location.py +++ b/modules/stock/location.py @@ -545,43 +545,23 @@ class Location(DeactivableMixin, tree(), ModelSQL, ModelView): cls.save(to_save) @classmethod - def create(cls, vlist): - locations = super().create(vlist) - cls._set_warehouse_parent(locations) + def on_modification(cls, mode, locations, field_names=None): + super().on_modification(mode, locations, field_names=field_names) + if mode in {'create', 'write'}: + cls._set_warehouse_parent(locations) cls._default_warehouse_cache.clear() - return locations @classmethod - def write(cls, *args): - super().write(*args) - locations = sum(args[::2], []) - cls._set_warehouse_parent(locations) - cls._default_warehouse_cache.clear() - - ids = [l.id for l in locations] - warehouses = cls.search([ - ('type', '=', 'warehouse'), - ['OR', - ('storage_location', 'in', ids), - ('input_location', 'in', ids), - ('output_location', 'in', ids), - ]]) - - fields = ('storage_location', 'input_location', 'output_location') - wh2childs = {} - for warehouse in warehouses: - in_out_sto = (getattr(warehouse, f).id for f in fields) - for location in locations: - if location.id not in in_out_sto: - continue - childs = wh2childs.setdefault(warehouse.id, cls.search([ - ('parent', 'child_of', warehouse.id), - ])) - if location not in childs: - raise LocationValidationError( - gettext('stock.msg_location_child_of_warehouse', - location=location.rec_name, - warehouse=warehouse.rec_name)) + def check_modification(cls, mode, locations, values=None, external=False): + super().check_modification( + mode, locations, values=values, external=external) + if mode == 'write' and 'parent' in values: + warehouses = cls.search([ + ('type', '=', 'warehouse'), + ]) + cls.validate_fields( + warehouses, + {'storage_location', 'input_location', 'output_location'}) @classmethod def delete(cls, locations): @@ -594,7 +574,6 @@ class Location(DeactivableMixin, tree(), ModelSQL, ModelView): location.storage_location, ])) super().delete(locations + extra_locations) - cls._default_warehouse_cache.clear() @classmethod def copy(cls, locations, default=None): diff --git a/modules/stock/message.xml b/modules/stock/message.xml index 8f56de2d64..2bb2dde0d1 100644 --- a/modules/stock/message.xml +++ b/modules/stock/message.xml @@ -26,9 +26,6 @@ You must use the "Modify Cost Price" wizard. You cannot change the type of location "%(location)s" to "%(type)s" because the type does not support moves and location has existing moves. - - To set location "%(location)s" as storage/input/output of warehouse "%(warehouse)s", it must be in the warehouse. - To inactivate location "%(location)s", you must empty it. diff --git a/modules/stock/move.py b/modules/stock/move.py index 133d8922b3..e8617e54f0 100644 --- a/modules/stock/move.py +++ b/modules/stock/move.py @@ -3,7 +3,7 @@ import copy import datetime import operator -from collections import OrderedDict, defaultdict +from collections import defaultdict from decimal import Decimal from itertools import groupby @@ -991,72 +991,52 @@ class Move(Workflow, ModelSQL, ModelView): return values @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Product = pool.get('product.product') context = Transaction().context - - vlist = [x.copy() for x in vlist] - # Use ordered dict to optimize cache alignment - products = OrderedDict() - for vals in vlist: - if vals.get('product') is not None: - products[vals['product']] = None - id2product = {p.id: p for p in Product.browse(products.keys())} - for vals in vlist: - assert vals.get('state', cls.default_state() - ) in ['draft', 'staging'] - if vals.get('product') is not None: - product = id2product[int(vals['product'])] - if context.get('_product_replacement', True): - vals['product'] = product.replacement.id - moves = super().create(vlist) - cls.check_period_closed(moves) - return moves + values = super().preprocess_values(mode, values) + if mode == 'create': + assert values.get('state', cls.default_state()) in { + 'draft', 'staging'} + if context.get('_product_replacement', True): + product_id = values.get('product') + if product_id is not None: + product = Product(product_id) + values['product'] = product.replacement.id + elif mode == 'write': + if {'unit_price', 'currency'} & values.keys(): + values['unit_price_updated'] = True + return values @classmethod - def write(cls, *args): - actions = iter(args) - for moves, values in zip(actions, actions): - vals_set = set(values) - if cls._deny_modify_assigned & vals_set: + def check_modification(cls, mode, moves, values=None, external=False): + super().check_modification( + mode, moves, values=values, external=external) + if mode == 'create': + cls.check_period_closed(moves) + elif mode == 'write': + if values.keys() - cls._allow_modify_closed_period: + cls.check_period_closed(moves) + if values.keys() & cls._deny_modify_assigned: for move in moves: if move.state == 'assigned': - raise AccessError( - gettext('stock.msg_move_modify_assigned', + raise AccessError(gettext( + 'stock.msg_move_modify_assigned', move=move.rec_name)) - if cls._deny_modify_done_cancel & vals_set: + if values.keys() & cls._deny_modify_done_cancel: for move in moves: - if move.state in ('done', 'cancelled'): - raise AccessError( - gettext('stock.msg_move_modify_%s' % move.state, + if move.state in {'done', 'cancelled'}: + raise AccessError(gettext( + 'stock.msg_move_modify_%s' % move.state, move=move.rec_name)) - - super().write(*args) - - unit_price_update = [] - actions = iter(args) - for moves, values in zip(actions, actions): - if any(f not in cls._allow_modify_closed_period for f in values): - cls.check_period_closed(moves) + elif mode == 'delete': for move in moves: - if (move.state == 'done' - and ('unit_price' in values - or 'currency' in values)): - unit_price_update.append(move) - - if unit_price_update: - cls.write(unit_price_update, {'unit_price_updated': True}) - - @classmethod - def delete(cls, moves): - for move in moves: - if (move.state not in {'staging', 'draft', 'cancelled'} - and move.quantity): - raise AccessError( - gettext('stock.msg_move_delete_draft_cancel', - move=move.rec_name)) - super().delete(moves) + if (move.state not in {'staging', 'draft', 'cancelled'} + and move.quantity): + raise AccessError(gettext( + 'stock.msg_move_delete_draft_cancel', + move=move.rec_name)) @staticmethod def check_origin_types(): diff --git a/modules/stock/product.py b/modules/stock/product.py index 7cf3e96715..3a27febeb5 100644 --- a/modules/stock/product.py +++ b/modules/stock/product.py @@ -1,7 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime -import functools from collections import defaultdict from copy import deepcopy from decimal import Decimal @@ -29,124 +28,73 @@ from .move import StockMixin from .shipment import ShipmentAssignMixin -def check_no_move(func): - - def find_moves(cls, records): - pool = Pool() - Move = pool.get('stock.move') - if cls.__name__ == 'product.template': - field = 'product.template' - else: - field = 'product' - for sub_records in grouped_slice(records): - moves = Move.search([ - (field, 'in', list(map(int, sub_records))), - ], - limit=1, order=[]) - if moves: - return moves - return False - - @functools.wraps(func) - def decorator(cls, *args): - pool = Pool() - Template = pool.get('product.template') - transaction = Transaction() - if transaction.user and transaction.check_access: - actions = iter(args) - for records, values in zip(actions, actions): - for field, msg in Template._modify_no_move: - if field in values: - if find_moves(cls, records): - raise AccessError(gettext(msg)) - # No moves for those records - break - - if not values.get('template'): - continue - template = Template(values['template']) - for record in records: - for field, msg in Template._modify_no_move: - if isinstance( - getattr(Template, field), fields.Function): - continue - if getattr(record, field) != getattr(template, field): - if find_moves(cls, [record]): - raise AccessError(gettext(msg)) - # No moves for this record - break - func(cls, *args) - return decorator +def _find_moves(cls, records): + pool = Pool() + Move = pool.get('stock.move') + if cls.__name__ == 'product.template': + field = 'product.template' + else: + field = 'product' + for sub_records in grouped_slice(records): + moves = Move.search([ + (field, 'in', list(map(int, sub_records))), + ], + limit=1, order=[]) + if moves: + return moves + return False -def check_no_stock_if_inactive(func): +def _check_no_stock(cls, records): + pool = Pool() + Company = pool.get('company.company') + Location = pool.get('stock.location') + Product = pool.get('product.product') + Warning = pool.get('res.user.warning') - def get_product_locations( - company, location_ids, sub_products, grouping=('product',)): - pool = Pool() - Product = pool.get('product.product') - - product2locations = defaultdict(list) - product_ids = list(map(int, sub_products)) + def get_record_locations(company, location_ids, records, grouping): + record2locations = defaultdict(list) + record_ids = list(map(int, records)) with Transaction().set_context(company=company.id): quantities = Product.products_by_location( location_ids, with_childs=True, - grouping=grouping, grouping_filter=(product_ids,)) + grouping=grouping, grouping_filter=(record_ids,)) for key, quantity in quantities.items(): location_id, product_id, = key if quantity: - product2locations[product_id].append(location_id) - return product2locations + record2locations[product_id].append(location_id) + return record2locations - def raise_warning(cls, company, product2locations): - pool = Pool() - Location = pool.get('stock.location') - Warning = pool.get('res.user.warning') - - for product_id, location_ids in product2locations.items(): - product = cls(product_id) + def raise_warning(cls, company, record2locations): + for record_id, location_ids in record2locations.items(): + record = cls(record_id) locations = ','.join( l.rec_name for l in Location.browse(location_ids[:5])) if len(location_ids) > 5: locations += '...' warning_name = Warning.format( - 'deactivate_product_with_stock', [product]) + 'deactivate_product_with_stock', [record]) if Warning.check(warning_name): raise ProductStockWarning(warning_name, gettext( 'stock.msg_product_location_quantity', - product=product.rec_name, + product=record.rec_name, company=company.rec_name, locations=locations), gettext('stock.msg_product_location_quantity_description')) - @functools.wraps(func) - def decorator(cls, *args): - pool = Pool() - Company = pool.get('company.company') - Location = pool.get('stock.location') + if cls.__name__ == 'product.template': + grouping = ('product.template',) + else: + grouping = ('product',) - if cls.__name__ == 'product.template': - grouping = ('product.template',) - else: - grouping = ('product',) - - to_check = [] - actions = iter(args) - for products, values in zip(actions, actions): - if not values.get('active', True): - to_check.extend(products) - if to_check: - with without_check_access(): - locations = Location.search([('type', '=', 'storage')]) - location_ids = list(map(int, locations)) - for company in Company.search([]): - for sub_products in grouped_slice(to_check): - product2locations = get_product_locations( - company, location_ids, sub_products, grouping) - raise_warning(cls, company, product2locations) - return func(cls, *args) - return decorator + locations = Location.search([('type', '=', 'storage')]) + location_ids = list(map(int, locations)) + for company in Company.search([]): + for sub_records in grouped_slice(records): + record2locations = get_record_locations( + company, location_ids, sub_records, grouping) + raise_warning(cls, company, record2locations) class Template(metaclass=PoolMeta): @@ -185,10 +133,19 @@ class Template(metaclass=PoolMeta): ] @classmethod - @check_no_move - @check_no_stock_if_inactive - def write(cls, *args): - super().write(*args) + def check_modification(cls, mode, templates, values=None, external=False): + super().check_modification( + mode, templates, values=values, external=external) + if mode == 'write' and external: + for field_name, msg in cls._modify_no_move: + if field_name in values: + if _find_moves(cls, templates): + raise AccessError(gettext(msg)) + # No moves for those records + break + + if not values.get('active', True): + _check_no_stock(cls, templates) @classmethod def recompute_cost_price(cls, templates, start=None): @@ -262,10 +219,36 @@ class Product(StockMixin, object, metaclass=PoolMeta): return cost_values @classmethod - @check_no_move - @check_no_stock_if_inactive - def write(cls, *args): - super().write(*args) + def check_modification(cls, mode, products, values=None, external=False): + pool = Pool() + Template = pool.get('product.template') + super().check_modification( + mode, products, values=values, external=external) + if mode == 'write' and external: + for field_name, msg in Template._modify_no_move: + if field_name in values: + if _find_moves(cls, products): + raise AccessError(gettext(msg)) + # No moves for those records + break + + if values.get('template'): + template = Template(values['template']) + for product in products: + for field_name, msg in Template._modify_no_move: + if isinstance( + getattr(Template, field_name), + fields.Function): + continue + if (getattr(product, field_name) + != getattr(template, field_name)): + if _find_moves(cls, [product]): + raise AccessError(gettext(msg)) + # No moves for this record + break + + if not values.get('active', True): + _check_no_stock(cls, products) def can_be_deactivated(self): pool = Pool() diff --git a/modules/stock/shipment.py b/modules/stock/shipment.py index 75c669f927..c1141c03cd 100644 --- a/modules/stock/shipment.py +++ b/modules/stock/shipment.py @@ -148,6 +148,34 @@ class ShipmentMixin: default.setdefault('number') return super().copy(shipments, default=default) + @classmethod + def on_modification(cls, mode, shipments, field_names=None): + pool = Pool() + Move = pool.get('stock.move') + super().on_modification(mode, shipments, field_names=field_names) + if (mode in {'create', 'write'} + and (not field_names or 'planned_date' in field_names)): + cls._set_move_planned_date(shipments) + elif mode == 'delete': + if hasattr(cls, 'moves'): + moves = [m for s in shipments for m in s.moves] + Move.delete(moves) + + @classmethod + def check_modification(cls, mode, shipments, values=None, external=False): + super().check_modification( + mode, shipments, values=values, external=external) + if mode == 'delete': + for shipment in shipments: + if shipment.state not in {'cancelled', 'request', 'draft'}: + raise AccessError(gettext( + 'stock.msg_shipment_delete_cancel', + shipment=shipment.rec_name)) + + @classmethod + def _set_move_planned_date(cls, shipments): + raise NotImplementedError + class ShipmentAssignMixin(ShipmentMixin): __slots__ = () @@ -620,16 +648,18 @@ class ShipmentIn( (m.origin_name for m in self.incoming_moves)))) @classmethod - def create(cls, vlist): - shipments = super().create(vlist) - cls.set_number(shipments) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) + def preprocess_values(cls, mode, values): + pool = Pool() + Configuration = pool.get('stock.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'shipment_in_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, shipments, default=None): @@ -682,19 +712,6 @@ class ShipmentIn( shipment.moves = moves cls.save(shipments) - @classmethod - def delete(cls, shipments): - Move = Pool().get('stock.move') - # Cancel before delete - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('stock.msg_shipment_delete_cancel', - shipment=shipment.rec_name)) - Move.delete([m for s in shipments for m in s.moves]) - super().delete(shipments) - @classmethod @ModelView.button @Workflow.transition('cancelled') @@ -955,30 +972,6 @@ class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): return ', '.join(set(filter(None, (m.origin_name for m in self.moves)))) - @classmethod - def create(cls, vlist): - shipments = super().create(vlist) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) - - @classmethod - def delete(cls, shipments): - Move = Pool().get('stock.move') - # Cancel before delete - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('stock.msg_shipment_delete_cancel', - shipment=shipment.rec_name)) - Move.delete([m for s in shipments for m in s.moves]) - super().delete(shipments) - @classmethod def copy(cls, shipments, default=None): if default is None: @@ -1738,17 +1731,6 @@ class ShipmentOut( if to_write: Move.write(*to_write) - @classmethod - def create(cls, vlist): - shipments = super().create(vlist) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) - @classmethod def copy(cls, shipments, default=None): if default is None: @@ -1761,19 +1743,6 @@ class ShipmentOut( default.setdefault('done_by', None) return super().copy(shipments, default=default) - @classmethod - def delete(cls, shipments): - Move = Pool().get('stock.move') - # Cancel before delete - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('stock.msg_shipment_delete_cancel', - shipment=shipment.rec_name)) - Move.delete([m for s in shipments for m in s.moves]) - super().delete(shipments) - @classmethod @ModelView.button_action('stock.wizard_shipment_out_assign') def assign_wizard(cls, shipments): @@ -2123,16 +2092,18 @@ class ShipmentOutReturn( (m.origin_name for m in self.incoming_moves)))) @classmethod - def create(cls, vlist): - shipments = super().create(vlist) - cls.set_number(shipments) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) + def preprocess_values(cls, mode, values): + pool = Pool() + Configuration = pool.get('stock.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'shipment_out_return_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, shipments, default=None): @@ -2144,19 +2115,6 @@ class ShipmentOutReturn( default.setdefault('done_by', None) return super().copy(shipments, default=default) - @classmethod - def delete(cls, shipments): - Move = Pool().get('stock.move') - # Cance before delete - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('stock.msg_shipment_delete_cancel', - shipment=shipment.rec_name)) - Move.delete([m for s in shipments for m in s.moves]) - super().delete(shipments) - @classmethod @ModelView.button @Workflow.transition('draft') @@ -2611,30 +2569,6 @@ class ShipmentInternal( 'moves': value, }) - @classmethod - def create(cls, vlist): - shipments = super().create(vlist) - cls._set_move_planned_date(shipments) - return shipments - - @classmethod - def write(cls, *args): - super().write(*args) - cls._set_move_planned_date(sum(args[::2], [])) - - @classmethod - def delete(cls, shipments): - Move = Pool().get('stock.move') - # Cancel before delete - cls.cancel(shipments) - for shipment in shipments: - if shipment.state != 'cancelled': - raise AccessError( - gettext('stock.msg_shipment_delete_cancel', - shipment=shipment.rec_name)) - Move.delete([m for s in shipments for m in s.moves]) - super().delete(shipments) - @classmethod def copy(cls, shipments, default=None): def shipment_field(data, name): diff --git a/modules/stock/tests/test_module.py b/modules/stock/tests/test_module.py index 989bb75918..eb9713bd5d 100644 --- a/modules/stock/tests/test_module.py +++ b/modules/stock/tests/test_module.py @@ -16,7 +16,7 @@ from trytond.modules.stock.exceptions import ( ProductStockWarning) from trytond.pool import Pool from trytond.tests.test_tryton import ModuleTestCase, with_transaction -from trytond.transaction import Transaction +from trytond.transaction import Transaction, check_access class StockTestCase( @@ -1609,11 +1609,12 @@ class StockTestCase( 'currency': company.currency.id, 'company': company.id, }]) - with self.assertRaises(ProductStockWarning): - Product.write(products, {'active': False}) + with check_access(): + with self.assertRaises(ProductStockWarning): + Product.write(products, {'active': False}) - with self.assertRaises(ProductStockWarning): - Template.write([template], {'active': False}) + with self.assertRaises(ProductStockWarning): + Template.write([template], {'active': False}) Move.create([{ 'product': product.id, @@ -1634,8 +1635,9 @@ class StockTestCase( 'currency': company.currency.id, 'company': company.id, }]) - Template.write([template], {'active': False}) - Product.write(products, {'active': False}) + with check_access(): + Template.write([template], {'active': False}) + Product.write(products, {'active': False}) @with_transaction() def test_inactive_products_one_with_stock(self): @@ -1670,11 +1672,12 @@ class StockTestCase( 'currency': company.currency.id, 'company': company.id, }]) - with self.assertRaises(ProductStockWarning): - Product.write(products, {'active': False}) + with check_access(): + with self.assertRaises(ProductStockWarning): + Product.write(products, {'active': False}) - with self.assertRaises(ProductStockWarning): - Template.write([template], {'active': False}) + with self.assertRaises(ProductStockWarning): + Template.write([template], {'active': False}) Move.create([{ 'product': product.id, @@ -1686,8 +1689,9 @@ class StockTestCase( 'currency': company.currency.id, 'company': company.id, }]) - Template.write([template], {'active': False}) - Product.write(products, {'active': False}) + with check_access(): + Template.write([template], {'active': False}) + Product.write(products, {'active': False}) @with_transaction() def test_location_inactive_with_consumable_product(self): diff --git a/modules/stock_forecast/forecast.py b/modules/stock_forecast/forecast.py index 779ea2e753..38c3bd4836 100644 --- a/modules/stock_forecast/forecast.py +++ b/modules/stock_forecast/forecast.py @@ -182,15 +182,15 @@ class Forecast(Workflow, ModelSQL, ModelView): ] @classmethod - def delete(cls, forecasts): - # Cancel before delete - cls.cancel(forecasts) - for forecast in forecasts: - if forecast.state != 'cancelled': - raise AccessError( - gettext('stock_forecast.msg_forecast_delete_cancel', - forecast=forecast.rec_name)) - super().delete(forecasts) + def check_modification(cls, mode, forecasts, values=None, external=False): + super().check_modification( + mode, forecasts, values=values, external=external) + if mode == 'delete': + for forecast in forecasts: + if forecast.state not in {'cancelled', 'draft'}: + raise AccessError(gettext( + 'stock_forecast.msg_forecast_delete_cancel', + forecast=forecast.rec_name)) @classmethod @ModelView.button diff --git a/modules/stock_location_move/stock.py b/modules/stock_location_move/stock.py index 9253262231..783108d76f 100644 --- a/modules/stock_location_move/stock.py +++ b/modules/stock_location_move/stock.py @@ -226,13 +226,14 @@ def deactivate_empty_location(func): def wrapper(cls, shipments, *args, **kwargs): pool = Pool() Location = pool.get('stock.location') - func(cls, shipments, *args, **kwargs) + result = func(cls, shipments, *args, **kwargs) locations = set() for shipment in shipments: locations.update( move.from_location for move in shipment.moves if move.from_location.movable) Location.deactivate_empty(list(locations)) + return result return wrapper diff --git a/modules/stock_lot/stock.py b/modules/stock_lot/stock.py index 1d67e7713e..74acbdd905 100644 --- a/modules/stock_lot/stock.py +++ b/modules/stock_lot/stock.py @@ -3,7 +3,6 @@ import datetime from collections import defaultdict from copy import copy -from functools import wraps from sql import Cast, Column, Null from sql.conditionals import Case @@ -24,37 +23,6 @@ from trytond.transaction import Transaction from trytond.wizard import Button, StateTransition, StateView, Wizard -def check_no_move(func): - def find_moves(cls, records, state=None): - pool = Pool() - Move = pool.get('stock.move') - for sub_records in grouped_slice(records): - domain = [ - ('lot', 'in', [r.id for r in sub_records]) - ] - if state: - domain.append(('state', '=', state)) - moves = Move.search(domain, limit=1, order=[]) - if moves: - return True - return False - - @wraps(func) - def decorator(cls, *args): - transaction = Transaction() - if transaction.user and transaction.check_access: - actions = iter(args) - for records, values in zip(actions, actions): - for field, state, error in cls._modify_no_move: - if field in values: - if find_moves(cls, records, state): - raise AccessError(gettext(error)) - # No moves for those records - break - func(cls, *args) - return decorator - - class LotMixin: __slots__ = () number = fields.Char( @@ -144,26 +112,46 @@ class Lot(DeactivableMixin, ModelSQL, ModelView, LotMixin, StockMixin): return super().copy(lots, default=default) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Product = pool.get('product.product') - vlist = [v.copy() for v in vlist] - missing_number = defaultdict(list) - for values in vlist: - if not values.get('number') and values.get('product') is not None: - product = Product(values['product']) + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + product_id = values.get('product') + if product_id is not None: + product = Product(product_id) if product.lot_sequence: - missing_number[product.lot_sequence].append(values) - - for sequence, values in missing_number.items(): - for vals, number in zip(values, sequence.get_many(len(values))): - vals['number'] = number - return super().create(vlist) + values['number'] = product.lot_sequence.get() + return values @classmethod - @check_no_move - def write(cls, *args): - super().write(*args) + def check_modification(cls, mode, lots, values=None, external=False): + pool = Pool() + Move = pool.get('stock.move') + transaction = Transaction() + + def find_moves(cls, state=None): + for sub_records in grouped_slice(lots): + domain = [ + ('lot', 'in', [r.id for r in sub_records]) + ] + if state: + domain.append(('state', '=', state)) + moves = Move.search(domain, limit=1, order=[]) + if moves: + return True + return False + + super().check_modification( + mode, lots, values=values, external=external) + if mode == 'write': + if transaction.user and transaction.check_access: + for field, state, error in cls._modify_no_move: + if field in values: + if find_moves(state): + raise AccessError(gettext(error)) + # No moves + break @classmethod @ModelView.button_action('stock_lot.act_lot_trace_upward_relate') diff --git a/modules/stock_lot_sled/stock.py b/modules/stock_lot_sled/stock.py index 569a1f2bac..3d4bffaf9d 100644 --- a/modules/stock_lot_sled/stock.py +++ b/modules/stock_lot_sled/stock.py @@ -104,14 +104,13 @@ class Lot(LotSledMixin, metaclass=PoolMeta): __name__ = 'stock.lot' @classmethod - def write(cls, *args): - super().write(*args) - - actions = iter(args) - for lots, values in zip(actions, actions): - if any(f in ['shelf_life_expiration_date', 'expiration_date'] - for f in values): - cls.check_sled_period_closed(lots) + def check_modification(cls, mode, lots, values=None, external=False): + super().check_modification( + mode, lots, values=values, external=external) + if (mode == 'write' + and values.keys() & { + 'shelf_life_expiration_date', 'expiration_date'}): + cls.check_sled_period_closed(lots) @classmethod def check_sled_period_closed(cls, lots): diff --git a/modules/stock_package/stock.py b/modules/stock_package/stock.py index 438e9c5947..1d2e062d12 100644 --- a/modules/stock_package/stock.py +++ b/modules/stock_package/stock.py @@ -315,18 +315,18 @@ class Package(tree(), MeasurementsMixin, ModelSQL, ModelView): children_volume, self.packaging_volume_uom))) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() - Config = pool.get('stock.configuration') - - vlist = [v.copy() for v in vlist] - config = Config(1) - default_company = cls.default_company() - for values in vlist: - values['number'] = config.get_multivalue( - 'package_sequence', - company=values.get('company', default_company)).get() - return super().create(vlist) + Configuration = pool.get('stock.configuration') + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('number'): + company_id = values.get('company', cls.default_company()) + if company_id is not None: + configuration = Configuration(1) + if sequence := configuration.get_multivalue( + 'package_sequence', company=company_id): + values['number'] = sequence.get() + return values @classmethod def copy(cls, packages, default=None): diff --git a/modules/stock_package_shipping_dpd/carrier.py b/modules/stock_package_shipping_dpd/carrier.py index 70b08d898d..a607cade9c 100644 --- a/modules/stock_package_shipping_dpd/carrier.py +++ b/modules/stock_package_shipping_dpd/carrier.py @@ -52,19 +52,21 @@ class CredentialDPD(sequence_ordered(), ModelSQL, ModelView, MatchMixin): self.save() @classmethod - def write(cls, *args): + def check_modification( + cls, mode, credentials, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for credentials, values in zip(actions, actions): - if {'user_id', 'password', 'server'} & values.keys(): - warning_name = Warning.format('dpd_credential', credentials) - if Warning.check(warning_name): - raise DPDCredentialWarning( - warning_name, - gettext('stock_package_shipping_dpd' - '.msg_dpd_credential_modified')) - return super().write(*args) + super().check_modification( + mode, credentials, values=values, external=external) + if (mode == 'write' + and external + and values.keys() & {'user_id', 'password', 'server'}): + warning_name = Warning.format('dpd_credential', credentials) + if Warning.check(warning_name): + raise DPDCredentialWarning( + warning_name, + gettext('stock_package_shipping_dpd' + '.msg_dpd_credential_modified')) class Carrier(metaclass=PoolMeta): diff --git a/modules/stock_package_shipping_mygls/carrier.py b/modules/stock_package_shipping_mygls/carrier.py index 03265a9803..10fd219b2f 100644 --- a/modules/stock_package_shipping_mygls/carrier.py +++ b/modules/stock_package_shipping_mygls/carrier.py @@ -34,20 +34,22 @@ class CredentialMyGLS(sequence_ordered(), ModelSQL, ModelView, MatchMixin): return 'testing' @classmethod - def write(cls, *args): + def check_modification( + cls, mode, credentials, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for credentials, values in zip(actions, actions): - if ({'server', 'username', 'password', 'client_number'} - & values.keys()): - warning_name = Warning.format('mygls_credential', credentials) - if Warning.check(warning_name): - raise MyGLSCredentialWarning( - warning_name, - gettext('stock_package_shipping_mygls' - '.msg_mygls_credential_modified')) - return super().write(*args) + super().check_modification( + mode, credentials, values=values, external=external) + if (mode == 'write' + and external + and values.keys() & { + 'server', 'username', 'password', 'client_number'}): + warning_name = Warning.format('mygls_credential', credentials) + if Warning.check(warning_name): + raise MyGLSCredentialWarning( + warning_name, + gettext('stock_package_shipping_mygls' + '.msg_mygls_credential_modified')) class Carrier(metaclass=PoolMeta): diff --git a/modules/stock_package_shipping_sendcloud/carrier.py b/modules/stock_package_shipping_sendcloud/carrier.py index 74c9042c44..4e4e5c7f1b 100644 --- a/modules/stock_package_shipping_sendcloud/carrier.py +++ b/modules/stock_package_shipping_sendcloud/carrier.py @@ -184,20 +184,22 @@ class CredentialSendcloud(sequence_ordered(), ModelSQL, ModelView, MatchMixin): return response.content @classmethod - def write(cls, *args): + def check_modification( + cls, mode, credentials, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for credentials, values in zip(actions, actions): - if {'public_key', 'secret_key'} & values.keys(): - warning_name = Warning.format( - 'sendcloud_credential', credentials) - if Warning.check(warning_name): - raise SendcloudCredentialWarning( - warning_name, - gettext('stock_package_shipping_sendcloud' - '.msg_sendcloud_credential_modified')) - return super().write(*args) + super().check_modification( + mode, credentials, values=values, external=external) + if (mode == 'write' + and external + and values.keys() & {'public_key', 'secret_key'}): + warning_name = Warning.format( + 'sendcloud_credential', credentials) + if Warning.check(warning_name): + raise SendcloudCredentialWarning( + warning_name, + gettext('stock_package_shipping_sendcloud' + '.msg_sendcloud_credential_modified')) class SendcloudAddress(sequence_ordered(), ModelSQL, ModelView, MatchMixin): diff --git a/modules/stock_package_shipping_ups/carrier.py b/modules/stock_package_shipping_ups/carrier.py index e17de0088b..3fd4835093 100644 --- a/modules/stock_package_shipping_ups/carrier.py +++ b/modules/stock_package_shipping_ups/carrier.py @@ -64,20 +64,22 @@ class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin): return 'https://wwwcie.ups.com/api/shipments/v2403/ship' @classmethod - def write(cls, *args): + def check_modification( + cls, mode, credentials, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for credentials, values in zip(actions, actions): - if ({'client_id', 'client_secret', 'account_number'} - & values.keys()): - warning_name = Warning.format('dpd_credential', credentials) - if Warning.check(warning_name): - raise UPSCredentialWarning( - warning_name, - gettext('stock_package_shipping_ups' - '.msg_ups_credential_modified')) - return super().write(*args) + super().check_modification( + mode, credentials, values=values, external=external) + if (mode == 'write' + and external + and values.keys() & { + 'client_id', 'client_secret', 'account_number'}): + warning_name = Warning.format('dpd_credential', credentials) + if Warning.check(warning_name): + raise UPSCredentialWarning( + warning_name, + gettext('stock_package_shipping_ups' + '.msg_ups_credential_modified')) class Carrier(metaclass=PoolMeta): diff --git a/modules/timesheet_cost/company.py b/modules/timesheet_cost/company.py index 7f657eac08..97a379cad3 100644 --- a/modules/timesheet_cost/company.py +++ b/modules/timesheet_cost/company.py @@ -106,20 +106,8 @@ class EmployeeCostPrice(ModelSQL, ModelView): return str(self.date) @classmethod - def delete(cls, prices): - Employee = Pool().get('company.employee') - super().delete(prices) - Employee._cost_prices_cache.clear() - - @classmethod - def create(cls, vlist): - Employee = Pool().get('company.employee') - prices = super().create(vlist) - Employee._cost_prices_cache.clear() - return prices - - @classmethod - def write(cls, *args): - Employee = Pool().get('company.employee') - super().write(*args) + def on_modification(cls, mode, prices, field_names=None): + pool = Pool() + Employee = pool.get('company.employee') + super().on_modification(mode, prices, field_names=field_names) Employee._cost_prices_cache.clear() diff --git a/modules/timesheet_cost/timesheet.py b/modules/timesheet_cost/timesheet.py index 5db69c5a16..215060ab2e 100644 --- a/modules/timesheet_cost/timesheet.py +++ b/modules/timesheet_cost/timesheet.py @@ -19,13 +19,12 @@ class Line(metaclass=PoolMeta): return 0 @classmethod - def create(cls, vlist): - # XXX Remove cost_price because proteus set it as default value - vlist = [v.copy() for v in vlist] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': + # XXX Remove cost_price because proteus set it as default value values.pop('cost_price', None) - lines = super().create(vlist) - return lines + return values @classmethod def compute_fields(self, field_names=None): diff --git a/modules/user_role/res.py b/modules/user_role/res.py index 4a1bb7e03f..2a840e6afd 100644 --- a/modules/user_role/res.py +++ b/modules/user_role/res.py @@ -13,16 +13,15 @@ class Role(ModelSQL, ModelView): groups = fields.Many2Many('res.role-res.group', 'role', 'group', "Groups") @classmethod - def write(cls, *args): + def on_modification(cls, mode, roles, field_names=None): pool = Pool() User = pool.get('res.user') - roles = sum(args[0:None:2], []) - super().write(*args) - - users = User.search([ - ('roles.role', 'in', [r.id for r in roles]), - ]) - User.sync_roles(users) + super().on_modification(mode, roles, field_names=field_names) + if mode == 'write': + users = User.search([ + ('roles.role', 'in', [r.id for r in roles]), + ]) + User.sync_roles(users) class RoleGroup(ModelSQL): @@ -48,16 +47,10 @@ class User(metaclass=PoolMeta): cls._context_fields.append('roles') @classmethod - def create(cls, vlist): - users = super().create(vlist) - cls.sync_roles(users) - return users - - @classmethod - def write(cls, *args): - users = sum(args[0:None:2], []) - super().write(*args) - cls.sync_roles(users) + def on_modification(cls, mode, users, field_names=None): + super().on_modification(mode, users, field_names=field_names) + if mode in {'create', 'write'}: + cls.sync_roles(users) @classmethod def sync_roles(cls, users=None, date=None): diff --git a/modules/web_shop/web.py b/modules/web_shop/web.py index 72ea25291a..84c841fdcc 100644 --- a/modules/web_shop/web.py +++ b/modules/web_shop/web.py @@ -35,12 +35,13 @@ class Inactivate(ModelSQL, DeactivableMixin): domain, active_test=active_test, tables=tables) @classmethod - def delete(cls, records): - cls.copy([r for r in records if r.active and r.shop.to_sync], - default={ - 'active': False, - }) - super().delete(records) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if mode == 'delete': + cls.copy([r for r in records if r.active and r.shop.to_sync], + default={ + 'active': False, + }) class Shop(DeactivableMixin, ModelSQL, ModelView): @@ -157,14 +158,10 @@ class Shop(DeactivableMixin, ModelSQL, ModelView): return super().copy(shops, default=default) @classmethod - def write(cls, *args): - cls._name_cache.clear() - super().write(*args) - - @classmethod - def delete(cls, shops): - cls._name_cache.clear() - super().delete(shops) + def on_modification(cls, mode, shops, field_names=None): + super().on_modification(mode, shops, field_names=field_names) + if mode == 'delete' or (mode == 'write' and 'name' in field_names): + cls._name_cache.clear() @property def to_sync(self): diff --git a/modules/web_shop_shopify/common.py b/modules/web_shop_shopify/common.py index c493496f7b..b0d80bac2d 100644 --- a/modules/web_shop_shopify/common.py +++ b/modules/web_shop_shopify/common.py @@ -77,15 +77,13 @@ class IdentifiersUpdateMixin: raise NotImplementedError @classmethod - def write(cls, *args): - actions = iter(args) - to_update = set() - for records, values in zip(actions, actions): - if values.keys() & cls._shopify_fields: - to_update.update(records) - if to_update: - cls.set_shopify_to_update(cls.browse(list(to_update))) - super().write(*args) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if mode in {'create', 'delete'}: + cls.set_shopify_to_update(records) + elif mode == 'write': + if field_names & cls._shopify_fields: + cls.set_shopify_to_update(records) class IdentifiersMixin(IdentifiersUpdateMixin): @@ -148,11 +146,13 @@ class IdentifiersMixin(IdentifiersUpdateMixin): return super().copy(records, default=default) @classmethod - def delete(cls, records): + def on_modification(cls, mode, records, field_names=None): pool = Pool() Identifier = pool.get('web.shop.shopify_identifier') - Identifier.delete(sum((r.shopify_identifiers for r in records), ())) - super().delete(records) + super().on_modification(mode, records, field_names=field_names) + if mode == 'delete': + Identifier.delete( + sum((r.shopify_identifiers for r in records), ())) @classmethod def get_shopify_identifier_to_update(cls, records): diff --git a/modules/web_shop_shopify/product.py b/modules/web_shop_shopify/product.py index 21add964dc..c2d9d660ec 100644 --- a/modules/web_shop_shopify/product.py +++ b/modules/web_shop_shopify/product.py @@ -50,17 +50,6 @@ class TemplateCategory(IdentifiersUpdateMixin, metaclass=PoolMeta): super().__setup__() cls._shopify_fields.update(['template', 'category']) - @classmethod - def create(cls, vlist): - records = super().create(vlist) - cls.set_shopify_to_update(records) - return records - - @classmethod - def delete(cls, records): - cls.set_shopify_to_update(records) - super().delete(records) - @classmethod def get_shopify_identifier_to_update(cls, records): return sum((list(r.template.shopify_identifiers) for r in records), []) @@ -280,17 +269,16 @@ class Product(IdentifiersMixin, metaclass=PoolMeta): factor=self.shopify_uom_factor, rate=self.shopify_uom_rate) @classmethod - def write(cls, *args): - actions = iter(args) - for products, values in zip(actions, actions): - if 'template' in values: - for product in products: - if (product.template.id != values.get('template') - and product.shopify_identifiers): - raise AccessError(gettext( - 'web_shop_shopify.msg_product_change_template', - product=product.rec_name)) - super().write(*args) + def check_modification(cls, mode, products, values=None, external=False): + super().check_modification( + mode, products, values=values, external=external) + if mode == 'write' and 'template' in values: + for product in products: + if (product.template.id != values.get('template') + and product.shopify_identifiers): + raise AccessError(gettext( + 'web_shop_shopify.msg_product_change_template', + product=product.rec_name)) class ShopifyInventoryItem(IdentifiersMixin, ModelSQL, ModelView): @@ -376,17 +364,6 @@ class Product_TariffCode(IdentifiersUpdateMixin, metaclass=PoolMeta): super().__setup__() cls._shopify_fields.update(['product', 'tariff_code']) - @classmethod - def create(cls, vlist): - identifiers = super().create(vlist) - cls.set_shopify_to_update(identifiers) - return identifiers - - @classmethod - def delete(cls, records): - cls.set_shopify_to_update(records) - super().delete(records) - @classmethod def get_shopify_identifier_to_update(cls, records): pool = Pool() @@ -417,17 +394,6 @@ class ProductIdentifier(IdentifiersUpdateMixin, metaclass=PoolMeta): super().__setup__() cls._shopify_fields.update(['product', 'code']) - @classmethod - def create(cls, vlist): - identifiers = super().create(vlist) - cls.set_shopify_to_update(identifiers) - return identifiers - - @classmethod - def delete(cls, identifiers): - cls.set_shopify_to_update(identifiers) - super().delete(identifiers) - @classmethod def get_shopify_identifier_to_update(cls, identifiers): return sum(( @@ -549,28 +515,17 @@ class Image(IdentifiersMixin, metaclass=PoolMeta): cls._shopify_fields.update(['template', 'product', 'attributes']) @classmethod - def create(cls, vlist): - images = super().create(vlist) - cls.set_shopify_to_update(images) - return images - - @classmethod - def write(cls, *args): + def on_write(cls, images, values): pool = Pool() Identifier = pool.get('web.shop.shopify_identifier') - actions = iter(args) - to_delete = [] - for images, values in zip(actions, actions): - if values.keys() & {'image', 'template', 'web_shop'}: - for image in images: - to_delete.extend(image.shopify_identifiers) - super().write(*args) - Identifier.delete(to_delete) - - @classmethod - def delete(cls, images): - cls.set_shopify_to_update(images) - super().delete(images) + callback = super().on_write(images, values) + if values.keys() & {'image', 'template', 'web_shop'}: + to_delete = [] + for image in images: + to_delete.extend(image.shopify_identifiers) + if to_delete: + callback.append(lambda: Identifier.delete(to_delete)) + return callback @classmethod def get_shopify_identifier_to_update(cls, images): diff --git a/modules/web_shop_shopify/web.py b/modules/web_shop_shopify/web.py index ab81f293f5..3194de0b6e 100644 --- a/modules/web_shop_shopify/web.py +++ b/modules/web_shop_shopify/web.py @@ -582,21 +582,22 @@ class Shop(metaclass=PoolMeta): Sale.__queue__.process(to_update.keys()) @classmethod - def write(cls, *args): + def check_modification(cls, mode, shops, values=None, external=False): pool = Pool() Warning = pool.get('res.user.warning') - actions = iter(args) - for shops, values in zip(actions, actions): - if ({'shopify_url', 'shopify_password', - 'shopify_webhook_shared_secret'} - & values.keys()): - warning_name = Warning.format('shopify_credential', shops) - if Warning.check(warning_name): - raise ShopifyCredentialWarning( - warning_name, - gettext('web_shop_shopify' - '.msg_shopidy_credential_modified')) - return super().write(*args) + super().check_modification( + mode, shops, values=values, external=external) + if (mode == 'write' + and external + and values.keys() & { + 'shopify_url', 'shopify_password', + 'shopify_webhook_shared_secret'}): + warning_name = Warning.format('shopify_credential', shops) + if Warning.check(warning_name): + raise ShopifyCredentialWarning( + warning_name, + gettext('web_shop_shopify' + '.msg_shopidy_credential_modified')) class ShopShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView): diff --git a/modules/web_user/user.py b/modules/web_user/user.py index 0e4b91ed13..13104d20bd 100644 --- a/modules/web_user/user.py +++ b/modules/web_user/user.py @@ -144,14 +144,6 @@ class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView): if not self.email and self.party: self.email = self.party.email - @classmethod - def _format_email(cls, users): - for user in users: - email = normalize_email(user.email).lower() - if email != user.email: - user.email = email - cls.save(users) - @classmethod def copy(cls, users, default=None): default = default.copy() if default is not None else {} @@ -160,16 +152,11 @@ class User(avatar_mixin(100), DeactivableMixin, ModelSQL, ModelView): return super().copy(users, default=default) @classmethod - def create(cls, vlist): - users = super().create(vlist) - cls._format_email(users) - return users - - @classmethod - def write(cls, *args): - super().write(*args) - users = sum(args[0:None:2], []) - cls._format_email(users) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if values.get('email'): + values['email'] = normalize_email(values['email']).lower() + return values @classmethod def validate_fields(cls, users, fields_names): @@ -468,13 +455,13 @@ class UserSession(ModelSQL): cls.write(sessions, {}) @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create': # Ensure to get a different key for each record # default methods are called only once values.setdefault('key', cls.default_key()) - return super().create(vlist) + return values class EmailValidation(Report): diff --git a/trytond/trytond/ir/action.py b/trytond/trytond/ir/action.py index 2822fdc262..4984ebee85 100644 --- a/trytond/trytond/ir/action.py +++ b/trytond/trytond/ir/action.py @@ -89,10 +89,11 @@ class Action(DeactivableMixin, ModelSQL, ModelView): return None @classmethod - def write(cls, actions, values, *args): + def on_modification(cls, mode, actions, field_names=None): pool = Pool() - super().write(actions, values, *args) - pool.get('ir.action.keyword')._get_keyword_cache.clear() + ActionKeyword = pool.get('ir.action.keyword') + super().on_modification(mode, actions, field_names=field_names) + ActionKeyword._get_keyword_cache.clear() @classmethod @inactive_records @@ -159,36 +160,20 @@ class ActionGroup(ModelSQL): 'res.group', "Group", ondelete='CASCADE', required=True) @classmethod - def create(cls, vlist): + def preprocess_values(cls, mode, values): pool = Pool() Action = pool.get('ir.action') - vlist = [x.copy() for x in vlist] - for vals in vlist: - if vals.get('action'): - vals['action'] = Action.get_action_id(vals['action']) - Action._groups_cache.clear() - return super().create(vlist) + values = super().preprocess_values(mode, values) + if values.get('action') is not None: + values['action'] = Action.get_action_id(values['action']) + return values @classmethod - def write(cls, records, values, *args): + def on_modification(cls, mode, records, field_names=None): pool = Pool() Action = pool.get('ir.action') - actions = iter((records, values) + args) - args = [] - for records, values in zip(actions, actions): - if values.get('action'): - values = values.copy() - values['action'] = Action.get_action_id(values['action']) - args.extend((records, values)) + super().on_modification(mode, records, field_names=field_names) Action._groups_cache.clear() - super().write(*args) - - @classmethod - def delete(cls, records): - pool = Pool() - Action = pool.get('ir.action') - Action._groups_cache.clear() - super().delete(records) class ActionKeyword(ModelSQL, ModelView): @@ -251,14 +236,20 @@ class ActionKeyword(ModelSQL, ModelView): gettext('ir.msg_action_wrong_wizard_model', name=action_wizard.rec_name)) - @staticmethod - def _convert_vals(vals): - vals = vals.copy() + @classmethod + def preprocess_values(cls, mode, values): pool = Pool() Action = pool.get('ir.action') - if 'action' in vals: - vals['action'] = Action.get_action_id(vals['action']) - return vals + values = super().preprocess_values(mode, values) + if values.get('action') is not None: + values['action'] = Action.get_action_id(values['action']) + return values + + @classmethod + def on_modification(cls, mode, keywords, field_names=None): + super().on_modification(mode, keywords, field_names=field_names) + ModelView._view_toolbar_get_cache.clear() + cls._get_keyword_cache.clear() @staticmethod def models_get(): @@ -273,31 +264,6 @@ class ActionKeyword(ModelSQL, ModelView): def search_groups(cls, name, clause): return [('action.' + clause[0],) + tuple(clause[1:])] - @classmethod - def delete(cls, keywords): - ModelView._view_toolbar_get_cache.clear() - cls._get_keyword_cache.clear() - super().delete(keywords) - - @classmethod - def create(cls, vlist): - ModelView._view_toolbar_get_cache.clear() - cls._get_keyword_cache.clear() - new_vlist = [] - for vals in vlist: - new_vlist.append(cls._convert_vals(vals)) - return super().create(new_vlist) - - @classmethod - def write(cls, keywords, values, *args): - actions = iter((keywords, values) + args) - args = [] - for keywords, values in zip(actions, actions): - args.extend((keywords, cls._convert_vals(values))) - super().write(*args) - ModelView._view_toolbar_get_cache.clear() - cls._get_keyword_cache.clear() - @classmethod def get_keyword(cls, keyword, value): pool = Pool() @@ -455,10 +421,21 @@ class ActionMixin(ModelSQL): Action._groups_cache.set(key, list(groups)) return groups + @classmethod + def on_modification(cls, mode, records, field_names=None): + pool = Pool() + Action = pool.get('ir.action') + ActionKeyword = pool.get('ir.action.keyword') + super().on_modification(mode, records, field_names=field_names) + ModelView._view_toolbar_get_cache.clear() + ActionKeyword._get_keyword_cache.clear() + if mode == 'delete': + actions = [x.action for x in records] + Action.delete(actions) + @classmethod def create(cls, vlist): pool = Pool() - ModelView._view_toolbar_get_cache.clear() Action = pool.get('ir.action') ir_action = cls.__table__() new_records = [] @@ -497,23 +474,6 @@ class ActionMixin(ModelSQL): cls.write(*to_write) return new_records - @classmethod - def write(cls, records, values, *args): - pool = Pool() - ActionKeyword = pool.get('ir.action.keyword') - super().write(records, values, *args) - ModelView._view_toolbar_get_cache.clear() - ActionKeyword._get_keyword_cache.clear() - - @classmethod - def delete(cls, records): - pool = Pool() - ModelView._view_toolbar_get_cache.clear() - Action = pool.get('ir.action') - actions = [x.action for x in records] - super().delete(records) - Action.delete(actions) - @classmethod def copy(cls, records, default=None): pool = Pool() @@ -757,19 +717,18 @@ class ActionReport( return super().copy(reports, default=default) @classmethod - def write(cls, reports, values, *args): + def preprocess_values(cls, mode, values): context = Transaction().context - if 'module' in context: - actions = iter((reports, values) + args) - args = [] - for reports, values in zip(actions, actions): - values = values.copy() - values['module'] = context['module'] - args.extend((reports, values)) - reports, values = args[:2] - args = args[2:] - cls._template_cache.clear() - super().write(reports, values, *args) + values = super().preprocess_values(mode, values) + if 'module' in context and not values.get('module'): + values['module'] = context['module'] + return values + + @classmethod + def on_modification(cls, mode, reports, field_names=None): + super().on_modification(mode, reports, field_names=field_names) + if mode == 'write': + cls._template_cache.clear() def get_template_cached(self): return self._template_cache.get(self.id) @@ -1028,23 +987,11 @@ class ActionActWindowView( return self.act_window.res_model @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() - windows = super().create(vlist) - pool.get('ir.action.keyword')._get_keyword_cache.clear() - return windows - - @classmethod - def write(cls, windows, values, *args): - pool = Pool() - super().write(windows, values, *args) - pool.get('ir.action.keyword')._get_keyword_cache.clear() - - @classmethod - def delete(cls, windows): - pool = Pool() - super().delete(windows) - pool.get('ir.action.keyword')._get_keyword_cache.clear() + Keyword = pool.get('ir.action.keyword') + super().on_modification(mode, records, field_names=field_names) + Keyword._get_keyword_cache.clear() class ActionActWindowDomain( @@ -1105,23 +1052,11 @@ class ActionActWindowDomain( action=action.rec_name)) from exception @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() - domains = super().create(vlist) - pool.get('ir.action.keyword')._get_keyword_cache.clear() - return domains - - @classmethod - def write(cls, domains, values, *args): - pool = Pool() - super().write(domains, values, *args) - pool.get('ir.action.keyword')._get_keyword_cache.clear() - - @classmethod - def delete(cls, domains): - pool = Pool() - super().delete(domains) - pool.get('ir.action.keyword')._get_keyword_cache.clear() + Keyword = pool.get('ir.action.keyword') + super().on_modification(mode, records, field_names=field_names) + Keyword._get_keyword_cache.clear() class ActionWizard( diff --git a/trytond/trytond/ir/avatar.py b/trytond/trytond/ir/avatar.py index 6aa5aa9af5..dc8b6b141d 100644 --- a/trytond/trytond/ir/avatar.py +++ b/trytond/trytond/ir/avatar.py @@ -58,17 +58,17 @@ class Avatar(ImageMixin, ResourceMixin, ModelSQL): return uuid.uuid4().hex @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - values.setdefault('uuid', cls.default_uuid()) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and 'uuid' not in values: + values['uuid'] = cls.default_uuid() + return values @classmethod - def write(cls, *args): - avatars = sum(args[0:None:2], []) - super().write(*args) - cls.clear_cache(avatars) + def on_modification(cls, mode, avatars, field_names=None): + super().on_modification(mode, avatars, field_names=field_names) + if mode == 'write': + cls.clear_cache(avatars) @classmethod def copy(cls, avatars, default=None): diff --git a/trytond/trytond/ir/configuration.py b/trytond/trytond/ir/configuration.py index 2aa880da89..5c3a8f26e1 100644 --- a/trytond/trytond/ir/configuration.py +++ b/trytond/trytond/ir/configuration.py @@ -32,17 +32,6 @@ class Configuration(ModelSingleton, ModelSQL): pass @classmethod - def create(cls, vlist): - records = super().create(vlist) - cls._get_language_cache.clear() - return records - - @classmethod - def write(cls, *args): - super().write(*args) - cls._get_language_cache.clear() - - @classmethod - def delete(cls, records): - super().delete(records) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_language_cache.clear() diff --git a/trytond/trytond/ir/email_.py b/trytond/trytond/ir/email_.py index 281428f471..04a242f8d3 100644 --- a/trytond/trytond/ir/email_.py +++ b/trytond/trytond/ir/email_.py @@ -562,20 +562,10 @@ class EmailTemplate(ModelSQL, ModelView): return record.language @classmethod - def create(cls, vlist): - ModelView._view_toolbar_get_cache.clear() - return super().create(vlist) - - @classmethod - def write(cls, *args): - if any({'name', 'model'} & v.keys() for v in args[1:None:2]): + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if not field_names or {'name', 'model'} & set(field_names): ModelView._view_toolbar_get_cache.clear() - super().write(*args) - - @classmethod - def delete(cls, records): - ModelView._view_toolbar_get_cache.clear() - super().delete(records) class EmailTemplate_Report(ModelSQL): diff --git a/trytond/trytond/ir/export.py b/trytond/trytond/ir/export.py index bd5383f68a..7cc80d85d7 100644 --- a/trytond/trytond/ir/export.py +++ b/trytond/trytond/ir/export.py @@ -9,20 +9,11 @@ from trytond.transaction import Transaction class _ClearCache(ModelSQL): - @classmethod - def create(cls, vlist): - ModelView._view_toolbar_get_cache.clear() - return super().create(vlist) @classmethod - def write(cls, *args): + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) ModelView._view_toolbar_get_cache.clear() - super().write(*args) - - @classmethod - def delete(cls, records): - ModelView._view_toolbar_get_cache.clear() - super().delete(records) class Export(_ClearCache, ModelSQL, ModelView): diff --git a/trytond/trytond/ir/lang.py b/trytond/trytond/ir/lang.py index 3c44fd4dd0..988b3effcf 100644 --- a/trytond/trytond/ir/lang.py +++ b/trytond/trytond/ir/lang.py @@ -15,6 +15,7 @@ from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.model import ( Check, DeactivableMixin, ModelSQL, ModelView, Unique, fields) +from trytond.model.exceptions import AccessError from trytond.modules import create_graph, load_translations from trytond.pool import Pool from trytond.pyson import Eval @@ -40,7 +41,7 @@ class TranslatableError(UserError): pass -class DeleteDefaultError(UserError): +class DeleteDefaultError(AccessError): pass @@ -396,43 +397,34 @@ class Lang(DeactivableMixin, ModelSQL, ModelView): return res @classmethod - def create(cls, vlist): - pool = Pool() - Translation = pool.get('ir.translation') - # Clear cache - cls._lang_cache.clear() - languages = super().create(vlist) - Translation._get_language_cache.clear() - _parents.clear() - return languages - - @classmethod - def write(cls, langs, values, *args): - pool = Pool() - Translation = pool.get('ir.translation') - # Clear cache - cls._lang_cache.clear() - cls._code_cache.clear() - super().write(langs, values, *args) - Translation._get_language_cache.clear() - _parents.clear() - - @classmethod - def delete(cls, langs): + def check_modification(cls, mode, languages, values=None, external=False): pool = Pool() Config = pool.get('ir.configuration') + + super().check_modification( + mode, languages, values=values, external=external) + + if mode == 'write' and 'code' in values: + for language in languages: + if (language.code == Config.get_language() + and (not values + or language.code != values['code'])): + raise DeleteDefaultError( + gettext('ir.msg_language_delete_default', + language=language.rec_name)) + + @classmethod + def on_modification(cls, mode, languages, field_names=None): + pool = Pool() Translation = pool.get('ir.translation') - for lang in langs: - if lang.code == Config.get_language(): - raise DeleteDefaultError( - gettext('ir.msg_language_delete_default', - language=lang.rec_name)) - # Clear cache + + super().on_modification(mode, languages, field_names=field_names) + cls._lang_cache.clear() - cls._code_cache.clear() - super().delete(langs) Translation._get_language_cache.clear() _parents.clear() + if mode in {'write', 'delete'}: + cls._code_cache.clear() @classmethod @inactive_records diff --git a/trytond/trytond/ir/message.py b/trytond/trytond/ir/message.py index 135b3618a2..d32c6705c1 100644 --- a/trytond/trytond/ir/message.py +++ b/trytond/trytond/ir/message.py @@ -91,6 +91,19 @@ class Message(ModelSQL, ModelView): return translation language = get_parent(language) + @classmethod + def on_modification(cls, mode, messages, field_names=None): + pool = Pool() + Translation = pool.get('ir.translation') + super().on_modification(mode, messages, field_names=field_names) + if mode in {'create', 'write'}: + cls._set_translations(messages) + if mode in {'write', 'delete'}: + cls._message_cache.clear() + if mode == 'delete': + Translation.delete_ids( + cls.__name__, 'model', [m.id for m in messages]) + @classmethod def _set_translations(cls, messages): pool = Pool() @@ -142,27 +155,6 @@ class Message(ModelSQL, ModelView): if to_save: Translation.save(to_save) - @classmethod - def create(cls, vlist): - messages = super().create(vlist) - cls._set_translations(messages) - return messages - - @classmethod - def write(cls, messages, values, *args): - super().write(messages, values, *args) - cls._set_translations(messages) - cls._message_cache.clear() - - @classmethod - def delete(cls, messages): - pool = Pool() - Translation = pool.get('ir.translation') - ids = [m.id for m in messages] - super().delete(messages) - Translation.delete_ids(cls.__name__, 'model', ids) - cls._message_cache.clear() - @classmethod def search_rec_name(cls, name, clause): return [('text',) + tuple(clause[1:])] diff --git a/trytond/trytond/ir/model.py b/trytond/trytond/ir/model.py index b2a66b4aab..c50c83974c 100644 --- a/trytond/trytond/ir/model.py +++ b/trytond/trytond/ir/model.py @@ -754,24 +754,8 @@ class ModelAccess( return True @classmethod - def write(cls, accesses, values, *args): - super().write(accesses, values, *args) - # Restart the cache - cls._get_access_cache.clear() - ModelView._fields_view_get_cache.clear() - - @classmethod - def create(cls, vlist): - res = super().create(vlist) - # Restart the cache - cls._get_access_cache.clear() - ModelView._fields_view_get_cache.clear() - return res - - @classmethod - def delete(cls, accesses): - super().delete(accesses) - # Restart the cache + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_access_cache.clear() ModelView._fields_view_get_cache.clear() @@ -947,24 +931,8 @@ class ModelFieldAccess( return True @classmethod - def write(cls, field_accesses, values, *args): - super().write(field_accesses, values, *args) - # Restart the cache - cls._get_access_cache.clear() - ModelView._fields_view_get_cache.clear() - - @classmethod - def create(cls, vlist): - res = super().create(vlist) - # Restart the cache - cls._get_access_cache.clear() - ModelView._fields_view_get_cache.clear() - return res - - @classmethod - def delete(cls, field_accesses): - super().delete(field_accesses) - # Restart the cache + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_access_cache.clear() ModelView._fields_view_get_cache.clear() @@ -1044,25 +1012,8 @@ class ModelButton( cls._order.insert(0, ('model', 'ASC')) @classmethod - def create(cls, vlist): - result = super().create(vlist) - cls._rules_cache.clear() - cls._reset_cache.clear() - cls._groups_cache.clear() - cls._view_attributes_cache.clear() - return result - - @classmethod - def write(cls, buttons, values, *args): - super().write(buttons, values, *args) - cls._rules_cache.clear() - cls._reset_cache.clear() - cls._groups_cache.clear() - cls._view_attributes_cache.clear() - - @classmethod - def delete(cls, buttons): - super().delete(buttons) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._rules_cache.clear() cls._reset_cache.clear() cls._groups_cache.clear() @@ -1172,26 +1123,11 @@ class ModelButtonGroup(DeactivableMixin, ModelSQL): 'res.group', "Group", ondelete='CASCADE', required=True) @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() - result = super().create(vlist) - # Restart the cache for get_groups - pool.get('ir.model.button')._groups_cache.clear() - return result - - @classmethod - def write(cls, records, values, *args): - pool = Pool() - super().write(records, values, *args) - # Restart the cache for get_groups - pool.get('ir.model.button')._groups_cache.clear() - - @classmethod - def delete(cls, records): - pool = Pool() - super().delete(records) - # Restart the cache for get_groups - pool.get('ir.model.button')._groups_cache.clear() + Button = pool.get('ir.model.button') + super().on_modification(mode, records, field_names=field_names) + Button._groups_cache.clear() class ModelButtonRule(ModelSQL, ModelView): @@ -1249,28 +1185,10 @@ class ModelButtonRule(ModelSQL, ModelView): return len(users) >= self.number_user @classmethod - def create(cls, vlist): + def on_modification(cls, mode, records, field_names=None): pool = Pool() ModelButton = pool.get('ir.model.button') - result = super().create(vlist) - # Restart the cache for get_rules - ModelButton._rules_cache.clear() - return result - - @classmethod - def write(cls, buttons, values, *args): - pool = Pool() - ModelButton = pool.get('ir.model.button') - super().write(buttons, values, *args) - # Restart the cache for get_rules - ModelButton._rules_cache.clear() - - @classmethod - def delete(cls, buttons): - pool = Pool() - ModelButton = pool.get('ir.model.button') - super().delete(buttons) - # Restart the cache for get_rules + super().on_modification(mode, records, field_names=field_names) ModelButton._rules_cache.clear() @@ -1430,22 +1348,11 @@ class ModelData( return False @classmethod - def create(cls, *args): - records = super().create(*args) - cls._has_model_cache.clear() - return records - - @classmethod - def write(cls, data, values, *args): - super().write(data, values, *args) - # Restart the cache for get_id - cls._get_id_cache.clear() - cls._has_model_cache.clear() - - @classmethod - def delete(cls, records): - super().delete(records) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._has_model_cache.clear() + if mode == 'write': + cls._get_id_cache.clear() @classmethod def has_model(cls, model): diff --git a/trytond/trytond/ir/module.py b/trytond/trytond/ir/module.py index 05d3869bcc..9d7bdaa3e8 100644 --- a/trytond/trytond/ir/module.py +++ b/trytond/trytond/ir/module.py @@ -123,16 +123,18 @@ class Module(ModelSQL, ModelView): return child_ids @classmethod - def delete(cls, records): - for module in records: - if module.state in ( - 'activated', - 'to upgrade', - 'to remove', - 'to activate', - ): - raise AccessError(gettext('ir.msg_module_delete_state')) - return super().delete(records) + def check_modification(cls, mode, records, values=None, external=False): + super().check_modification( + mode, records, values=values, external=external) + if mode == 'delete': + for module in records: + if module.state in ( + 'activated', + 'to upgrade', + 'to remove', + 'to activate', + ): + raise AccessError(gettext('ir.msg_module_delete_state')) @classmethod def on_written(cls, modules): diff --git a/trytond/trytond/ir/resource.py b/trytond/trytond/ir/resource.py index 0762000935..b096251d8a 100644 --- a/trytond/trytond/ir/resource.py +++ b/trytond/trytond/ir/resource.py @@ -117,13 +117,13 @@ class ResourceAccessMixin(ModelStorage): @classmethod def write(cls, records, values, *args): - all_records = [] + all_ids = [] actions = iter((records, values) + args) for other_records, _ in zip(actions, actions): - all_records += other_records - cls.check_access([a.id for a in all_records], mode='write') + all_ids.extend(r.id for r in other_records) + cls.check_access(all_ids, mode='write') super().write(records, values, *args) - cls.check_access(all_records, mode='write') + cls.check_access(all_ids, mode='write') @classmethod def create(cls, vlist): diff --git a/trytond/trytond/ir/rule.py b/trytond/trytond/ir/rule.py index 43071d5f79..5e6ca02cb1 100644 --- a/trytond/trytond/ir/rule.py +++ b/trytond/trytond/ir/rule.py @@ -144,23 +144,11 @@ class RuleGroup( return True @classmethod - def delete(cls, groups): - super().delete(groups) - # Restart the cache on the domain_get method of ir.rule - Pool().get('ir.rule')._domain_get_cache.clear() - - @classmethod - def create(cls, vlist): - res = super().create(vlist) - # Restart the cache on the domain_get method of ir.rule - Pool().get('ir.rule')._domain_get_cache.clear() - return res - - @classmethod - def write(cls, groups, vals, *args): - super().write(groups, vals, *args) - # Restart the cache on the domain_get method of ir.rule - Pool().get('ir.rule')._domain_get_cache.clear() + def on_modification(cls, mode, groups, field_names=None): + pool = Pool() + Rule = pool.get('ir.rule') + super().on_modification(mode, groups, field_names=field_names) + Rule._domain_get_cache.clear() class RuleGroup_Group(ModelSQL): @@ -395,20 +383,6 @@ class Rule(ModelSQL, ModelView): **Model.__names__())) @classmethod - def delete(cls, rules): - super().delete(rules) - # Restart the cache on the domain_get method of ir.rule - cls._domain_get_cache.clear() - - @classmethod - def create(cls, vlist): - res = super().create(vlist) - # Restart the cache on the domain_get method of ir.rule - cls._domain_get_cache.clear() - return res - - @classmethod - def write(cls, rules, vals, *args): - super().write(rules, vals, *args) - # Restart the cache on the domain_get method + def on_modification(cls, mode, rules, field_names=None): + super().on_modification(mode, rules, field_names=field_names) cls._domain_get_cache.clear() diff --git a/trytond/trytond/ir/session.py b/trytond/trytond/ir/session.py index 850de0429a..fd831ed311 100644 --- a/trytond/trytond/ir/session.py +++ b/trytond/trytond/ir/session.py @@ -42,9 +42,9 @@ class Session(ModelSQL): return token_hex(nbytes) @classmethod - def write(cls, *args): - super().write(*args) - for sessions in args[0:None:2]: + def on_modification(cls, mode, sessions, field_names=None): + super().on_modification(mode, sessions, field_names=field_names) + if mode == 'write': for session in sessions: cls._session_reset_cache.set(session.key, session.write_date) @@ -148,13 +148,11 @@ class Session(ModelSQL): cls.delete(sessions) @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - # Ensure to get a different key for each record - # default methods are called only once - values.setdefault('key', cls.default_key()) - return super().create(vlist) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if mode == 'create' and 'key' not in values: + values['key'] = cls.default_key() + return values class SessionWizard(ModelSQL): diff --git a/trytond/trytond/ir/translation.py b/trytond/trytond/ir/translation.py index ce912c4d0c..7d090fa49f 100644 --- a/trytond/trytond/ir/translation.py +++ b/trytond/trytond/ir/translation.py @@ -707,26 +707,16 @@ class Translation( return super().copy(translations, default=default) @classmethod - def delete(cls, translations): - cls.__clear_cache_for(translations) - return super().delete(translations) + def preprocess_values(cls, mode, values): + context = Transaction().context + values = super().preprocess_values(mode, values) + if mode == 'create' and not values.get('module'): + values['module'] = context.get('module') + return values @classmethod - def create(cls, vlist): - vlist = [x.copy() for x in vlist] - - for vals in vlist: - if not vals.get('module'): - if Transaction().context.get('module'): - vals['module'] = Transaction().context['module'] - translations = super().create(vlist) - cls.__clear_cache_for(translations) - return translations - - @classmethod - def write(cls, *args): - super().write(*args) - translations = sum(args[0:None:2], []) + def on_modification(cls, mode, translations, field_names=None): + super().on_modification(mode, translations, field_names=field_names) cls.__clear_cache_for(translations) @classmethod diff --git a/trytond/trytond/ir/trigger.py b/trytond/trytond/ir/trigger.py index 50d7852d11..00c53dd65f 100644 --- a/trytond/trytond/ir/trigger.py +++ b/trytond/trytond/ir/trigger.py @@ -278,22 +278,8 @@ class Trigger(DeactivableMixin, ModelSQL, ModelView): trigger.trigger_action(records) @classmethod - def create(cls, vlist): - res = super().create(vlist) - # Restart the cache on the get_triggers method of ir.trigger - cls._get_triggers_cache.clear() - return res - - @classmethod - def write(cls, triggers, values, *args): - super().write(triggers, values, *args) - # Restart the cache on the get_triggers method of ir.trigger - cls._get_triggers_cache.clear() - - @classmethod - def delete(cls, records): - super().delete(records) - # Restart the cache on the get_triggers method of ir.trigger + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._get_triggers_cache.clear() diff --git a/trytond/trytond/ir/ui/icon.py b/trytond/trytond/ir/ui/icon.py index a8825a51f1..8a4b71905f 100644 --- a/trytond/trytond/ir/ui/icon.py +++ b/trytond/trytond/ir/ui/icon.py @@ -61,17 +61,6 @@ class Icon( return fp.read() @classmethod - def create(cls, vlist): - icons = super().create(vlist) - cls._list_icons.clear() - return icons - - @classmethod - def write(cls, *args): - super().write(*args) - cls._list_icons.clear() - - @classmethod - def delete(cls, icons): - super().delete(icons) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._list_icons.clear() diff --git a/trytond/trytond/ir/ui/view.py b/trytond/trytond/ir/ui/view.py index 6f3955da7d..858d6b70d0 100644 --- a/trytond/trytond/ir/ui/view.py +++ b/trytond/trytond/ir/ui/view.py @@ -266,24 +266,8 @@ class View( cls.write(views, {'data': value}) @classmethod - def delete(cls, views): - super().delete(views) - # Restart the cache - cls._view_get_cache.clear() - ModelView._fields_view_get_cache.clear() - - @classmethod - def create(cls, vlist): - views = super().create(vlist) - # Restart the cache - cls._view_get_cache.clear() - ModelView._fields_view_get_cache.clear() - return views - - @classmethod - def write(cls, views, values, *args): - super().write(views, values, *args) - # Restart the cache + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._view_get_cache.clear() ModelView._fields_view_get_cache.clear() @@ -492,19 +476,8 @@ class ViewTreeWidth( ] @classmethod - def delete(cls, records): - ModelView._fields_view_get_cache.clear() - super().delete(records) - - @classmethod - def create(cls, vlist): - res = super().create(vlist) - ModelView._fields_view_get_cache.clear() - return res - - @classmethod - def write(cls, records, values, *args): - super().write(records, values, *args) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) ModelView._fields_view_get_cache.clear() @classmethod @@ -602,20 +575,9 @@ class ViewTreeOptional( view=record.view.rec_name)) @classmethod - def create(cls, vlist): - records = super().create(vlist) + def on_modification(cls, mode, record, field_names=None): + super().on_modification(mode, record, field_names=field_names) ModelView._fields_view_get_cache.clear() - return records - - @classmethod - def write(cls, *args): - super().write(*args) - ModelView._fields_view_get_cache.clear() - - @classmethod - def delete(cls, records): - ModelView._fields_view_get_cache.clear() - super().delete(records) @classmethod def set_optional(cls, view_id, fields): diff --git a/trytond/trytond/model/avatar.py b/trytond/trytond/model/avatar.py index 9e5ba32ce2..26adcc20c5 100644 --- a/trytond/trytond/model/avatar.py +++ b/trytond/trytond/model/avatar.py @@ -87,15 +87,9 @@ def avatar_mixin(size=64, default=None): if default: @classmethod - def create(cls, vlist): - records = super().create(vlist) - cls.generate_avatar(records, field=default) - return records - - @classmethod - def write(cls, *args): - records = sum(args[0:None:2], []) - super().write(*args) - cls.generate_avatar(records, field=default) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if mode in {'create', 'write'}: + cls.generate_avatar(records, field=default) return AvatarMixin diff --git a/trytond/trytond/model/dictschema.py b/trytond/trytond/model/dictschema.py index a72ae49e1c..392dbd603c 100644 --- a/trytond/trytond/model/dictschema.py +++ b/trytond/trytond/model/dictschema.py @@ -214,19 +214,8 @@ class DictSchemaMixin(object): return fields @classmethod - def create(cls, vlist): - records = super().create(vlist) - cls._relation_fields_cache.clear() - return records - - @classmethod - def write(cls, *args): - super().write(*args) - cls._relation_fields_cache.clear() - - @classmethod - def delete(cls, records): - super().delete(records) + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) cls._relation_fields_cache.clear() def format(self, value, lang=None): diff --git a/trytond/trytond/model/digits.py b/trytond/trytond/model/digits.py index 16c7c3392e..47a29511a3 100644 --- a/trytond/trytond/model/digits.py +++ b/trytond/trytond/model/digits.py @@ -29,6 +29,7 @@ class DigitsMixin: return (None, self.digits) @classmethod - def write(cls, *args): - super().write(*args) - cls._digits_cache.clear() + def on_modification(cls, mode, records, field_names=None): + super().on_modification(mode, records, field_names=field_names) + if mode == 'write': + cls._digits_cache.clear() diff --git a/trytond/trytond/model/fields/fmany2one.py b/trytond/trytond/model/fields/fmany2one.py index a730e3e9d8..43caa6a688 100644 --- a/trytond/trytond/model/fields/fmany2one.py +++ b/trytond/trytond/model/fields/fmany2one.py @@ -37,23 +37,11 @@ def fmany2one( on_delete=getattr(cls, name).ondelete) @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: - if name in values: - values[sources[0]] = convert_value(values.pop(name)) - return super().create(vlist) - - @classmethod - def write(cls, *args): - actions = iter(args) - args = [] - for records, values in zip(actions, actions): - if name in values: - values = values.copy() - values[sources[0]] = convert_value(values.pop(name)) - args.extend((records, values)) - super().write(*args) + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if name in values: + values[sources[0]] = convert_value(values.pop(name)) + return values @classmethod def getter(cls, records, name): diff --git a/trytond/trytond/res/group.py b/trytond/trytond/res/group.py index c2b995808a..f577b47981 100644 --- a/trytond/trytond/res/group.py +++ b/trytond/trytond/res/group.py @@ -58,11 +58,11 @@ class Group(DeactivableMixin, tree(), ModelSQL, ModelView): cls._order.insert(0, ('name', 'ASC')) @classmethod - def write(cls, *args): - super().write(*args) + def on_modification(cls, mode, groups, field_names=None): pool = Pool() - # Restart the cache for get_groups - pool.get('res.user')._get_groups_cache.clear() + User = pool.get('res.user') + super().on_modification(mode, groups, field_names=field_names) + User._get_groups_cache.clear() @classmethod def copy(cls, groups, default=None): diff --git a/trytond/trytond/res/user.py b/trytond/trytond/res/user.py index 1197d76919..908f25de2a 100644 --- a/trytond/trytond/res/user.py +++ b/trytond/trytond/res/user.py @@ -328,16 +328,36 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView): attrgetter('create_uid.id')))) return result - @staticmethod - def _convert_vals(vals): - vals = vals.copy() + @classmethod + def preprocess_values(cls, mode, values): pool = Pool() Action = pool.get('ir.action') - if 'menu' in vals: - vals['menu'] = Action.get_action_id(vals['menu']) - if vals.get('email'): - vals['email'] = normalize_email(vals['email']) - return vals + values = super().preprocess_values(mode, values) + if values.get('menu') is not None: + values['menu'] = Action.get_action_id(values['menu']) + if values.get('email'): + values['email'] = normalize_email(values['email']) + return values + + @classmethod + def on_modification(cls, mode, users, field_names=None): + pool = Pool() + Session = pool.get('ir.session') + UserDevice = pool.get('res.user.device') + + super().on_modification(mode, users, field_names=field_names) + + if mode == 'write': + if (field_names is None + or {'active', 'password'} & set(field_names)): + Session.clear(users) + UserDevice.clear([u.login for u in users]) + + # Clean cursor cache as it could be filled by domain_get + for cache in Transaction().cache.values(): + if cls.__name__ in cache: + for user in users: + cache[cls.__name__].pop(user.id, None) @classmethod def check_xml_record(cls, records, values): @@ -369,41 +389,6 @@ class User(avatar_mixin(100, 'login'), DeactivableMixin, ModelSQL, ModelView): cache[user.id]['password_hash'] = None return users - @classmethod - def create(cls, vlist): - vlist = [cls._convert_vals(vals) for vals in vlist] - return super().create(vlist) - - @classmethod - def write(cls, users, values, *args): - pool = Pool() - Session = pool.get('ir.session') - UserDevice = pool.get('res.user.device') - - actions = iter((users, values) + args) - all_users = [] - session_to_clear = [] - users_to_clear = [] - args = [] - for users, values in zip(actions, actions): - all_users += users - args.extend((users, cls._convert_vals(values))) - - if values.keys() & {'active', 'password'}: - session_to_clear += users - users_to_clear += [u.login for u in users] - - super().write(*args) - - Session.clear(session_to_clear) - UserDevice.clear(users_to_clear) - - # Clean cursor cache as it could be filled by domain_get - for cache in Transaction().cache.values(): - if cls.__name__ in cache: - for user in all_users: - cache[cls.__name__].pop(user.id, None) - @classmethod def delete(cls, users): raise DeleteError(gettext('res.msg_user_delete_forbidden')) @@ -911,28 +896,15 @@ class UserAction(ModelSQL): action = fields.Many2One('ir.action', 'Action', ondelete='CASCADE', required=True) - @staticmethod - def _convert_values(values): + @classmethod + def preprocess_values(cls, mode, values): pool = Pool() Action = pool.get('ir.action') - values = values.copy() - if values.get('action'): + values = super().preprocess_values(mode, values) + if values.get('action') is not None: values['action'] = Action.get_action_id(values['action']) return values - @classmethod - def create(cls, vlist): - vlist = [cls._convert_values(values) for values in vlist] - return super().create(vlist) - - @classmethod - def write(cls, records, values, *args): - actions = iter((records, values) + args) - args = [] - for records, values in zip(actions, actions): - args.extend((records, cls._convert_values(values))) - super().write(*args) - class UserGroup(ModelSQL): __name__ = 'res.user-res.group' @@ -942,26 +914,11 @@ class UserGroup(ModelSQL): required=True) @classmethod - def create(cls, vlist): - records = super().create(vlist) + def on_modification(cls, mode, records, field_names=None): pool = Pool() - # Restart the cache for get_groups - pool.get('res.user')._get_groups_cache.clear() - return records - - @classmethod - def write(cls, groups, values, *args): - super().write(groups, values, *args) - pool = Pool() - # Restart the cache for get_groups - pool.get('res.user')._get_groups_cache.clear() - - @classmethod - def delete(cls, groups): - super().delete(groups) - pool = Pool() - # Restart the cache for get_groups - pool.get('res.user')._get_groups_cache.clear() + User = pool.get('res.user') + super().on_modification(mode, records, field_names=field_names) + User._get_groups_cache.clear() @classmethod def user_group_all_table(cls): @@ -1117,14 +1074,13 @@ class UserApplication(Workflow, ModelSQL, ModelView): return record @classmethod - def create(cls, vlist): - vlist = [v.copy() for v in vlist] - for values in vlist: + def preprocess_values(cls, mode, values): + values = super().preprocess_values(mode, values) + if 'key' not in values: # Ensure we get a different key for each record # default methods are called only once - values.setdefault('key', cls.default_key()) - applications = super().create(vlist) - return applications + values['key'] = cls.default_key() + return values class EmailResetPassword(Report):