Add Erase Party wizard
issue7169 review37031002
This commit is contained in:
1 parent
11fbeb58a9
commit
611b5b5fd2
8 files changed
+264
-23
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Add Erase Party wizard
|
||||
* Search party also by contact mechanisms
|
||||
* Add contact_mechanism_get
|
||||
* Add name to contact mechanism
|
||||
|
||||
+22
-20
@@ -2,30 +2,32 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from trytond.pool import Pool
|
||||
from .category import *
|
||||
from .party import *
|
||||
from .address import *
|
||||
from .contact_mechanism import *
|
||||
from .configuration import *
|
||||
from . import category
|
||||
from . import party
|
||||
from . import address
|
||||
from . import contact_mechanism
|
||||
from . import configuration
|
||||
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
Category,
|
||||
Party,
|
||||
PartyLang,
|
||||
PartyCategory,
|
||||
PartyIdentifier,
|
||||
CheckVIESResult,
|
||||
PartyReplaceAsk,
|
||||
Address,
|
||||
AddressFormat,
|
||||
ContactMechanism,
|
||||
Configuration,
|
||||
ConfigurationSequence,
|
||||
ConfigurationLang,
|
||||
category.Category,
|
||||
party.Party,
|
||||
party.PartyLang,
|
||||
party.PartyCategory,
|
||||
party.PartyIdentifier,
|
||||
party.CheckVIESResult,
|
||||
party.PartyReplaceAsk,
|
||||
party.PartyEraseAsk,
|
||||
address.Address,
|
||||
address.AddressFormat,
|
||||
contact_mechanism.ContactMechanism,
|
||||
configuration.Configuration,
|
||||
configuration.ConfigurationSequence,
|
||||
configuration.ConfigurationLang,
|
||||
module='party', type_='model')
|
||||
Pool.register(
|
||||
CheckVIES,
|
||||
PartyReplace,
|
||||
party.CheckVIES,
|
||||
party.PartyReplace,
|
||||
party.PartyErase,
|
||||
module='party', type_='wizard')
|
||||
@@ -29,6 +29,9 @@ parties with the VIES web service.
|
||||
The *Replace* wizard allows to replace duplicate record by the original and
|
||||
relink all the related documents.
|
||||
|
||||
The *Erase* wizard allows to erase all personal data of a party from the system
|
||||
included the historized data and the resources attached for all the parties
|
||||
which were replaced by this one.
|
||||
|
||||
Address
|
||||
*******
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import stdnum.eu.vat as vat
|
||||
import stdnum.exceptions
|
||||
from sql import Null, Column
|
||||
from sql.functions import CharLength
|
||||
from sql import Null, Column, Cast, Literal
|
||||
from sql.functions import CharLength, Substring, Position
|
||||
|
||||
from trytond.model import (ModelView, ModelSQL, MultiValueMixin, ValueMixin,
|
||||
fields, Unique, sequence_ordered)
|
||||
@@ -16,7 +16,8 @@ from trytond.tools.multivalue import migrate_property
|
||||
|
||||
__all__ = ['Party', 'PartyLang', 'PartyCategory', 'PartyIdentifier',
|
||||
'CheckVIESResult', 'CheckVIES',
|
||||
'PartyReplace', 'PartyReplaceAsk']
|
||||
'PartyReplace', 'PartyReplaceAsk',
|
||||
'PartyErase', 'PartyEraseAsk']
|
||||
|
||||
VAT_COUNTRIES = [('', '')]
|
||||
STATES = {
|
||||
@@ -576,3 +577,139 @@ class PartyReplaceAsk(ModelView):
|
||||
def on_change_source(self):
|
||||
if self.source and self.source.replaced_by:
|
||||
self.destination = self.source.replaced_by
|
||||
|
||||
|
||||
class PartyErase(Wizard):
|
||||
"Erase Party"
|
||||
__name__ = 'party.erase'
|
||||
start_state = 'ask'
|
||||
ask = StateView('party.erase.ask', 'party.erase_ask_view_form', [
|
||||
Button("Cancel", 'end', 'tryton-cancel'),
|
||||
Button("Erase", 'erase', 'tryton-clear', default=True),
|
||||
])
|
||||
erase = StateTransition()
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(PartyErase, cls).__setup__()
|
||||
cls._error_messages.update({
|
||||
'active_party': (
|
||||
'The party "%(party)s" can not be erased '
|
||||
'because he is still active.'),
|
||||
})
|
||||
|
||||
def transition_erase(self):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
cursor = Transaction().connection.cursor()
|
||||
|
||||
def convert_from(table, tables):
|
||||
right, condition = tables[None]
|
||||
if table:
|
||||
table = table.join(right, condition=condition)
|
||||
else:
|
||||
table = right
|
||||
for k, sub_tables in tables.iteritems():
|
||||
if k is None:
|
||||
continue
|
||||
table = convert_from(table, sub_tables)
|
||||
return table
|
||||
|
||||
resources = self.get_resources()
|
||||
parties = replacing = [self.ask.party]
|
||||
with Transaction().set_context(active_test=False):
|
||||
while replacing:
|
||||
replacing = Party.search([
|
||||
('replaced_by', 'in', map(int, replacing)),
|
||||
])
|
||||
parties += replacing
|
||||
for party in parties:
|
||||
self.check_erase(party)
|
||||
to_erase = self.to_erase(party.id)
|
||||
for Model, domain, resource, columns, values in to_erase:
|
||||
assert issubclass(Model, ModelSQL)
|
||||
assert len(columns) == len(values)
|
||||
if 'active' in Model._fields:
|
||||
records = Model.search(domain)
|
||||
Model.write(records, {'active': False})
|
||||
|
||||
tables, where = Model.search_domain(domain, active_test=False)
|
||||
from_ = convert_from(None, tables)
|
||||
table, _ = tables[None]
|
||||
query = from_.select(table.id, where=where)
|
||||
|
||||
model_tables = [Model.__table__()]
|
||||
if Model._history:
|
||||
model_tables.append(Model.__table_history__())
|
||||
for table in model_tables:
|
||||
sql_columns, sql_values = [], []
|
||||
for column, value in zip(columns, values):
|
||||
column = Column(table, column)
|
||||
sql_columns.append(column)
|
||||
sql_values.append(
|
||||
value(column) if callable(value) else value)
|
||||
cursor.execute(*table.update(
|
||||
sql_columns, sql_values,
|
||||
where=table.id.in_(query)))
|
||||
if not resource:
|
||||
continue
|
||||
for Resource in resources:
|
||||
model_tables = [Resource.__table__()]
|
||||
if Resource._history:
|
||||
model_tables.append(Resource.__table_history__())
|
||||
for table in model_tables:
|
||||
cursor.execute(*table.delete(
|
||||
where=table.resource.like(
|
||||
Model.__name__ + ',%')
|
||||
& Cast(Substring(table.resource,
|
||||
Position(',', table.resource) +
|
||||
Literal(1)),
|
||||
Model.id.sql_type().base).in_(query)))
|
||||
return 'end'
|
||||
|
||||
def check_erase(self, party):
|
||||
if party.active:
|
||||
self.raise_user_error('active_party', {
|
||||
'party': party.rec_name,
|
||||
})
|
||||
|
||||
def to_erase(self, party_id):
|
||||
pool = Pool()
|
||||
Party = pool.get('party.party')
|
||||
Identifier = pool.get('party.identifier')
|
||||
Address = pool.get('party.address')
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
return [
|
||||
(Party, [('id', '=', party_id)], True,
|
||||
['name'],
|
||||
[None]),
|
||||
(Identifier, [('party', '=', party_id)], True,
|
||||
['type', 'code'],
|
||||
[None, '****']),
|
||||
(Address, [('party', '=', party_id)], True,
|
||||
['name', 'street', 'zip', 'city', 'country', 'subdivision'],
|
||||
[None, None, None, None, None, None]),
|
||||
(ContactMechanism, [('party', '=', party_id)], True,
|
||||
['value', 'name', 'comment'],
|
||||
[None, None, None]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_resources(cls):
|
||||
pool = Pool()
|
||||
Attachment = pool.get('ir.attachment')
|
||||
Note = pool.get('ir.note')
|
||||
return [Attachment, Note]
|
||||
|
||||
|
||||
class PartyEraseAsk(ModelView):
|
||||
"Erase Party"
|
||||
__name__ = 'party.erase.ask'
|
||||
party = fields.Many2One('party.party', "Party", required=True,
|
||||
help="The party to be erased.")
|
||||
|
||||
@classmethod
|
||||
def default_party(cls):
|
||||
context = Transaction().context
|
||||
if context.get('active_model') == 'party.party':
|
||||
return context.get('active_id')
|
||||
@@ -161,5 +161,26 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="type">form</field>
|
||||
<field name="name">replace_ask_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="wizard_erase">
|
||||
<field name="name">Erase</field>
|
||||
<field name="wiz_name">party.erase</field>
|
||||
<field name="model">party.party</field>
|
||||
</record>
|
||||
<record model="ir.action-res.group" id="wizard_erase-group_party_admin">
|
||||
<field name="action" ref="wizard_erase"/>
|
||||
<field name="group" ref="group_party_admin"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="wizard_erase_keyword1">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">party.party,-1</field>
|
||||
<field name="action" ref="wizard_erase"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="erase_ask_view_form">
|
||||
<field name="model">party.erase.ask</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">erase_ask_form</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -0,0 +1,65 @@
|
||||
====================
|
||||
Party Erase Scenario
|
||||
====================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
|
||||
Install party::
|
||||
|
||||
>>> config = activate_modules('party')
|
||||
|
||||
Create a party::
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> Attachment = Model.get('ir.attachment')
|
||||
>>> party = Party(name='Pam')
|
||||
>>> _ = party.identifiers.new(code="Identifier")
|
||||
>>> _ = party.contact_mechanisms.new(type='other', value="mechanism")
|
||||
>>> party.save()
|
||||
>>> address, = party.addresses
|
||||
>>> address.street = "St sample, 15"
|
||||
>>> address.city = "City"
|
||||
>>> address.save()
|
||||
>>> identifier, = party.identifiers
|
||||
>>> contact_mechanism, = party.contact_mechanisms
|
||||
>>> attachment = Attachment()
|
||||
>>> attachment.resource = party
|
||||
>>> attachment.name = "Attachment"
|
||||
>>> attachment.save()
|
||||
|
||||
Try erase active party::
|
||||
|
||||
>>> erase = Wizard('party.erase', models=[party])
|
||||
>>> erase.form.party == party
|
||||
True
|
||||
>>> erase.execute('erase') # doctest: +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
UserError: ...
|
||||
|
||||
Erase inactive party::
|
||||
|
||||
>>> party.active = False
|
||||
>>> party.save()
|
||||
|
||||
>>> erase = Wizard('party.erase', models=[party])
|
||||
>>> erase.form.party == party
|
||||
True
|
||||
>>> erase.execute('erase')
|
||||
|
||||
Check fields have been erased::
|
||||
|
||||
>>> party.name
|
||||
>>> identifier.reload()
|
||||
>>> identifier.code
|
||||
u'****'
|
||||
>>> address.reload()
|
||||
>>> address.street
|
||||
>>> address.city
|
||||
>>> contact_mechanism.reload()
|
||||
>>> contact_mechanism.value
|
||||
>>> Attachment.find()
|
||||
[]
|
||||
@@ -423,4 +423,9 @@ def suite():
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
suite.addTests(doctest.DocFileSuite(
|
||||
'scenario_party_erase.rst',
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
return suite
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="party"/>
|
||||
<field name="party"/>
|
||||
</form>
|
||||
Reference in new issue
Block a user