Skip to content

Commit

Permalink
Merge pull request #5721 from uktrade/bugfix/TET-606-fix-company-merge
Browse files Browse the repository at this point in the history
Bugfix/tet 606 fix company merge
  • Loading branch information
DeanElliott96 authored Oct 25, 2024
2 parents b55b85a + 24570ec commit 83e13ef
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 113 deletions.
12 changes: 12 additions & 0 deletions datahub/cleanup/test/commands/test_delete_orphaned_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ContactFactory,
ExportFactory,
ObjectiveFactory,
OneListCoreTeamMemberFactory,
)
from datahub.company_activity.tests.factories import (
CompanyActivityGreatFactory,
Expand All @@ -32,6 +33,7 @@
from datahub.export_win.test.factories import (
BreakdownFactory,
CustomerResponseFactory,
LegacyExportWinsToDataHubCompanyFactory,
WinAdviserFactory,
WinFactory,
)
Expand All @@ -49,8 +51,13 @@
InvestmentProjectTeamMemberFactory,
InvestmentSectorFactory,
)
from datahub.investment_lead.test.factories import EYBLeadFactory
from datahub.metadata.test.factories import SectorFactory
from datahub.omis.order.test.factories import OrderFactory
from datahub.reminder.test.factories import (
NewExportInteractionReminderFactory,
NoRecentExportInteractionReminderFactory,
)
from datahub.task.test.factories import TaskFactory
from datahub.user.company_list.test.factories import (
CompanyListItemFactory,
Expand All @@ -66,24 +73,29 @@
'company.CompanyExportCountryHistory': CompanyExportCountryHistoryFactory,
'company.Contact': ContactFactory,
'company.Objective': ObjectiveFactory,
'company.OneListCoreTeamMember': OneListCoreTeamMemberFactory,
'company_activity.CompanyActivity': CompanyActivityInteractionFactory,
'company_activity.Great': CompanyActivityGreatFactory,
'company_activity.IngestedFile': CompanyActivityIngestedFileFactory,
'company_list.CompanyListItem': CompanyListItemFactory,
'company_list.PipelineItem': PipelineItemFactory,
'company_referral.CompanyReferral': CompanyReferralFactory,
'event.Event': EventFactory,
'export_win.LegacyExportWinsToDataHubCompany': LegacyExportWinsToDataHubCompanyFactory,
'interaction.InteractionDITParticipant': InteractionDITParticipantFactory,
'interaction.Interaction': CompanyInteractionFactory,
'interaction.InteractionExportCountry': InteractionExportCountryFactory,
'investment.InvestmentProject': InvestmentProjectFactory,
'investment.InvestmentProjectTeamMember': InvestmentProjectTeamMemberFactory,
'investment.InvestmentActivity': InvestmentActivityFactory,
'investment.InvestmentSector': InvestmentSectorFactory,
'investment_lead.EYBLead': EYBLeadFactory,
'investor_profile.LargeCapitalInvestorProfile': LargeCapitalInvestorProfileFactory,
'opportunity.LargeCapitalOpportunity': LargeCapitalOpportunityFactory,
'metadata.Sector': SectorFactory,
'order.Order': OrderFactory,
'reminder.NewExportInteractionReminder': NewExportInteractionReminderFactory,
'reminder.NoRecentExportInteractionReminder': NoRecentExportInteractionReminderFactory,
'task.Task': TaskFactory,
'export_win.Win': WinFactory,
'export_win.CustomerResponse': CustomerResponseFactory,
Expand Down
20 changes: 18 additions & 2 deletions datahub/company/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,27 @@ def _default_object_updater(obj, field, target, source):
return

setattr(obj, field, target)
obj.save(update_fields=(field, 'modified_on'))

update_fields = (field, )

# Not all models have modified_on field.
if hasattr(obj, 'modified_on'):
update_fields = (field, 'modified_on')

obj.save(update_fields=update_fields)


class MergeConfiguration(NamedTuple):
"""Specifies how merging should be handled for a particular related model."""
"""
Used to specify which `model` and its `field`/s to be merged into the `source_model`.
:param model: The model related to the `source_model` model. i.e. `Interaction`.
:param fields: The field/s in the given `model` which relates to the company.
:param source_model: The model to merge into.
:param object_updater: A function to update the related field/s. If the default function is
not suitable you can pass your own. For example if you need have unique constraints you
need to handle.
"""

model: Type[models.Model]
fields: Sequence[str]
Expand Down
75 changes: 53 additions & 22 deletions datahub/company/merge_company.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,74 @@
import reversion

from datahub.company.merge import (
_default_object_updater,
is_model_a_valid_merge_source,
is_model_a_valid_merge_target,
MergeConfiguration,
MergeNotAllowedError,
update_objects,
)
from datahub.company.merge_utils.merge_relations import (
company_list_item_updater,
large_capital_opportunity_updater,
one_list_core_team_member_updater,
pipeline_item_updater,
)
from datahub.company.models import (
Company,
CompanyExport,
CompanyExportCountry,
CompanyExportCountryHistory,
Contact,
Objective,
OneListCoreTeamMember,
)
from datahub.company_activity.models import CompanyActivity
from datahub.company_referral.models import CompanyReferral
from datahub.dnb_api.utils import _get_rollback_version
from datahub.export_win.models import LegacyExportWinsToDataHubCompany
from datahub.interaction.models import Interaction
from datahub.investment.investor_profile.models import LargeCapitalInvestorProfile
from datahub.investment.opportunity.models import LargeCapitalOpportunity
from datahub.investment.project.models import InvestmentProject
from datahub.investment_lead.models import EYBLead
from datahub.omis.order.models import Order
from datahub.reminder.models import (
NewExportInteractionReminder,
NoRecentExportInteractionReminder,
)
from datahub.task.models import Task
from datahub.user.company_list.models import CompanyListItem, PipelineItem

logger = logging.getLogger(__name__)

# Merging is not allowed if the source company has any relations that aren't in
# this list. This is to avoid references to the source company being inadvertently
# left behind.
# EXCLUDE RELATIONS: To Exclude relations, add them here, don't add them to MERGE_CONFIGURATION.
ALLOWED_RELATIONS_FOR_MERGING = {
# These relations are moved to the target company on merge
Company._meta.get_field('company_list_items').remote_field,
Company._meta.get_field('pipeline_list_items').remote_field,
Company._meta.get_field('wins').remote_field,
CompanyActivity.company.field,
CompanyExport.company.field,
CompanyReferral.company.field,
Contact.company.field,
EYBLead.company.field,
Interaction.company.field,
Interaction.companies.field,
InvestmentProject.investor_company.field,
InvestmentProject.intermediate_company.field,
InvestmentProject.uk_company.field,
LargeCapitalInvestorProfile.investor_company.field,
LargeCapitalOpportunity.promoters.field,
LegacyExportWinsToDataHubCompany.company.field,
NewExportInteractionReminder.company.field,
NoRecentExportInteractionReminder.company.field,
Objective.company.field,
OneListCoreTeamMember.company.field,
Order.company.field,
Task.company.field,

# Merging is allowed if the source company has export countries, but note that
# they aren't moved to the target company (these can be manually moved in
Expand All @@ -66,40 +94,43 @@
}


def _company_list_item_updater(list_item, field, target_company, source_company):
# If there is already a list item for the target company, delete this list item instead
# as duplicates are not allowed
if CompanyListItem.objects.filter(list_id=list_item.list_id, company=target_company).exists():
list_item.delete()
else:
_default_object_updater(list_item, field, target_company, source_company)


def _pipeline_item_updater(pipeline_item, field, target_company, source_company):
# If there is already a pipeline item for the adviser for the target company
# delete this item instead as the same company can't be added for the same adviser again
if PipelineItem.objects.filter(adviser=pipeline_item.adviser, company=target_company).exists():
pipeline_item.delete()
else:
_default_object_updater(pipeline_item, field, target_company, source_company)


# Models related to a company for merging companies and how to merge them.
# If its not a simple relation (like OneToMany) then you can specify a function for how each item
# is merged.
# Relations NOT added here but included in ALLOWED_RELATIONS_FOR_MERGING will NOT be merged.
MERGE_CONFIGURATION = [
MergeConfiguration(Interaction, ('company', 'companies'), Company),
MergeConfiguration(CompanyReferral, ('company',), Company),
MergeConfiguration(CompanyActivity, ('company',), Company),
MergeConfiguration(CompanyExport, ('company',), Company),
MergeConfiguration(Contact, ('company',), Company),
MergeConfiguration(EYBLead, ('company',), Company),
MergeConfiguration(InvestmentProject, INVESTMENT_PROJECT_COMPANY_FIELDS, Company),
MergeConfiguration(LargeCapitalInvestorProfile, ('investor_company',), Company),
MergeConfiguration(
LargeCapitalOpportunity, ('promoters',), Company, large_capital_opportunity_updater,
),
MergeConfiguration(LegacyExportWinsToDataHubCompany, ('company',), Company),
MergeConfiguration(Order, ('company',), Company),
MergeConfiguration(CompanyListItem, ('company',), Company, _company_list_item_updater),
MergeConfiguration(PipelineItem, ('company',), Company, _pipeline_item_updater),
MergeConfiguration(NewExportInteractionReminder, ('company',), Company),
MergeConfiguration(NoRecentExportInteractionReminder, ('company',), Company),
MergeConfiguration(Task, ('company',), Company),
MergeConfiguration(CompanyListItem, ('company',), Company, company_list_item_updater),
MergeConfiguration(PipelineItem, ('company',), Company, pipeline_item_updater),
MergeConfiguration(Objective, ('company',), Company),
MergeConfiguration(
OneListCoreTeamMember, ('company',), Company, one_list_core_team_member_updater,
),
]


def merge_companies(source_company: Company, target_company: Company, user):
"""
Merges the source company into the target company.
MergeNotAllowedError will be raised if the merge is not allowed.
Companies with relations to another Company are not allowed to be merged. They would need to
be manually updated in the Admin before merging.
"""
is_source_valid, invalid_obj = is_model_a_valid_merge_source(
source_company,
Expand All @@ -112,7 +143,7 @@ def merge_companies(source_company: Company, target_company: Company, user):
logger.error(
f"""MergeNotAllowedError {source_company.id}
for company {target_company.id}.
Invalid bojects: {invalid_obj}""",
Invalid objects: {invalid_obj}""",
)
raise MergeNotAllowedError()

Expand Down
2 changes: 1 addition & 1 deletion datahub/company/merge_contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def merge_contacts(source_contact: Contact, target_contact: Contact, user):
logger.error(
f"""MergeNotAllowedError {source_contact.id}
for contact {target_contact.id}.
Invalid bojects: {invalid_obj}""",
Invalid objects: {invalid_obj}""",
)
raise MergeNotAllowedError()

Expand Down
Empty file.
56 changes: 56 additions & 0 deletions datahub/company/merge_utils/merge_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Any relations in the company merge which requires additional logic when merging.
from datahub.company.merge import (
_default_object_updater,
)
from datahub.company.models import (
OneListCoreTeamMember,
)
from datahub.investment.opportunity.models import LargeCapitalOpportunity
from datahub.user.company_list.models import CompanyListItem, PipelineItem


def company_list_item_updater(list_item, field, target_company, source_company):
# If there is already a list item for the target company, delete this list item instead
# as duplicates are not allowed
if CompanyListItem.objects.filter(list_id=list_item.list_id, company=target_company).exists():
list_item.delete()
else:
_default_object_updater(list_item, field, target_company, source_company)


def one_list_core_team_member_updater(one_list_item, field, target_company, source_company):
"""
The OneListCoreTeamMember model has a unique together contraint for company and adviser.
Before copying, if the target company already contains the adviser from the source company,
ignore it.
"""
if OneListCoreTeamMember.objects.filter(
adviser_id=one_list_item.adviser_id,
company=target_company,
).exists():
return
else:
_default_object_updater(one_list_item, field, target_company, source_company)


def large_capital_opportunity_updater(large_capital_opp, field, target_company, source_company):
"""
If the LargeCapitalOpportunity already exists in the target, ignore it. Otherwise add it.
"""
if LargeCapitalOpportunity.objects.filter(
id=large_capital_opp.id,
promoters__id=target_company.id,
).exists():
return
else:
_default_object_updater(large_capital_opp, field, target_company, source_company)


def pipeline_item_updater(pipeline_item, field, target_company, source_company):
# If there is already a pipeline item for the adviser for the target company
# delete this item instead as the same company can't be added for the same adviser again
if PipelineItem.objects.filter(adviser=pipeline_item.adviser, company=target_company).exists():
pipeline_item.delete()
else:
_default_object_updater(pipeline_item, field, target_company, source_company)
1 change: 1 addition & 0 deletions datahub/company/models/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ def delete_export_country(self, country_id, adviser):
export_country.delete()


@reversion.register_base_model()
class OneListCoreTeamMember(models.Model):
"""
Adviser who is a member of the One List Core Team of a company.
Expand Down
Loading

0 comments on commit 83e13ef

Please sign in to comment.