locations: тип «Сервис / поставщик» для Вайтер Юг и аналогов.
Выбор в журнале работ и код объекта в it_monthly; сид Вайтер Юг. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -131,6 +131,7 @@ class LocationResolver:
|
||||
self._by_num: dict[int, list[Location]] = {}
|
||||
self._office: Location | None = None
|
||||
self._warehouse: Location | None = None
|
||||
self._vendors_by_name: dict[str, Location] = {}
|
||||
self._built = False
|
||||
|
||||
def build(self):
|
||||
@@ -147,6 +148,11 @@ class LocationResolver:
|
||||
self._by_num.setdefault(int(num), []).append(pharmacy)
|
||||
self._office = Location.objects.filter(location_type='office', is_active=True).order_by('id').first()
|
||||
self._warehouse = Location.objects.filter(location_type='warehouse', is_active=True).order_by('id').first()
|
||||
self._vendors_by_name = {}
|
||||
for loc in Location.objects.filter(location_type='vendor', is_active=True):
|
||||
key = normalize_name(loc.name or '')
|
||||
if key:
|
||||
self._vendors_by_name[key] = loc
|
||||
|
||||
def resolve(self, object_code: str) -> Location | None:
|
||||
self.build()
|
||||
@@ -158,6 +164,9 @@ class LocationResolver:
|
||||
return self._office
|
||||
if upper in ('СКЛАД',):
|
||||
return self._warehouse
|
||||
vendor = self._vendors_by_name.get(normalize_name(raw))
|
||||
if vendor is not None:
|
||||
return vendor
|
||||
m = OBJ_CODE_RE.fullmatch(raw.replace(' ', ''))
|
||||
if not m:
|
||||
return None
|
||||
|
||||
@@ -84,12 +84,12 @@ class LocationAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
css = {'all': ('locations/css/location_admin.css',)}
|
||||
js = ('assets/js/admin_leaflet_resize_fix.js',)
|
||||
|
||||
list_display = ['address_display', 'location_type', 'location_region',
|
||||
list_display = ['address_display', 'name', 'location_type', 'location_region',
|
||||
'coordinates_display', 'is_active', 'updated_at']
|
||||
list_filter = ['location_type', 'is_active', HasCoordinatesFilter]
|
||||
search_fields = ['address', 'street', 'house_number', 'comments']
|
||||
search_fields = ['name', 'address', 'street', 'house_number', 'comments']
|
||||
list_select_related = ['location_region']
|
||||
ordering = ['location_region', 'address']
|
||||
ordering = ['location_type', 'name', 'location_region', 'address']
|
||||
|
||||
# address тепер лише генерується в Location.save() з street/house_number/building/
|
||||
# apartment — ручний ввід сюди все одно перезатреться при збереженні
|
||||
@@ -97,7 +97,11 @@ class LocationAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
|
||||
fieldsets = (
|
||||
(_('Тип и регион'), {
|
||||
'fields': ('location_type', 'location_region', 'is_active'),
|
||||
'fields': ('location_type', 'name', 'location_region', 'is_active'),
|
||||
'description': _(
|
||||
'«Сервис / поставщик» — обслуживающие организации (Вайтер Юг и т.п.): '
|
||||
'укажите название, адрес по желанию.'
|
||||
),
|
||||
}),
|
||||
(_('Адрес'), {
|
||||
'fields': ('street', 'house_number', 'building', 'apartment', 'address'),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 6.0.3 on 2026-10-07 12:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def seed_waiter_yug(apps, schema_editor):
|
||||
Location = apps.get_model('locations', 'Location')
|
||||
Directory = apps.get_model('core', 'Directory')
|
||||
if Location.objects.filter(location_type='vendor', name__iexact='Вайтер Юг').exists():
|
||||
return
|
||||
region = (
|
||||
Directory.objects.filter(directory_type__code='region', code='od').first()
|
||||
or Directory.objects.filter(directory_type__code='region').order_by('id').first()
|
||||
)
|
||||
if region is None:
|
||||
return
|
||||
Location.objects.create(
|
||||
location_type='vendor',
|
||||
name='Вайтер Юг',
|
||||
location_region_id=region.pk,
|
||||
address='Вайтер Юг',
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
def unseed_waiter_yug(apps, schema_editor):
|
||||
Location = apps.get_model('locations', 'Location')
|
||||
Location.objects.filter(location_type='vendor', name__iexact='Вайтер Юг').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('locations', '0014_alter_location_options_grouplocationaccess'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, help_text='Для сервиса/поставщика (Вайтер Юг и т.п.) и при необходимости офиса/склада', max_length=150, verbose_name='Название'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='location_type',
|
||||
field=models.CharField(choices=[('pharmacy', 'Аптека'), ('warehouse', 'Склад'), ('office', 'Офис'), ('vendor', 'Сервис / поставщик')], max_length=20, verbose_name='Тип локации'),
|
||||
),
|
||||
migrations.RunPython(seed_waiter_yug, unseed_waiter_yug),
|
||||
]
|
||||
@@ -77,12 +77,14 @@ _OBJECT_CODE_RE = re.compile(r'^([АAПPТTГGЛL])(\d+)$', re.I)
|
||||
|
||||
|
||||
def location_sort_key(location) -> tuple:
|
||||
"""Сортировка локаций: офис/склад, затем аптеки по коду А1…А70, П1…"""
|
||||
"""Сортировка локаций: офис/склад/сервис, затем аптеки по коду А1…А70, П1…"""
|
||||
loc_type = getattr(location, 'location_type', '') or ''
|
||||
if loc_type == 'office':
|
||||
return (0, 0, 0, (getattr(location, 'address', None) or '').casefold())
|
||||
return (0, 0, 0, (getattr(location, 'name', None) or getattr(location, 'address', None) or '').casefold())
|
||||
if loc_type == 'warehouse':
|
||||
return (0, 1, 0, (getattr(location, 'address', None) or '').casefold())
|
||||
return (0, 1, 0, (getattr(location, 'name', None) or getattr(location, 'address', None) or '').casefold())
|
||||
if loc_type == 'vendor':
|
||||
return (0, 2, 0, (getattr(location, 'name', None) or getattr(location, 'address', None) or '').casefold())
|
||||
|
||||
pharmacy = None
|
||||
try:
|
||||
@@ -126,6 +128,7 @@ class Location(BaseModel):
|
||||
('pharmacy', _('Аптека')),
|
||||
('warehouse', _('Склад')),
|
||||
('office', _('Офис')),
|
||||
('vendor', _('Сервис / поставщик')),
|
||||
])
|
||||
location_region = models.ForeignKey(
|
||||
'core.Directory', on_delete=models.PROTECT,
|
||||
@@ -133,6 +136,10 @@ class Location(BaseModel):
|
||||
related_name='region_locations', verbose_name=_('Город/район')
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
_('Название'), max_length=150, blank=True,
|
||||
help_text=_('Для сервиса/поставщика (Вайтер Юг и т.п.) и при необходимости офиса/склада'),
|
||||
)
|
||||
street = models.CharField(_('Улица'), max_length=100, blank=True)
|
||||
house_number = models.CharField(_('Номер дома'), max_length=20, blank=True)
|
||||
building = models.CharField(_('Корпус/строение'), max_length=20, blank=True)
|
||||
@@ -176,6 +183,7 @@ class Location(BaseModel):
|
||||
|
||||
def _display_label(self) -> str:
|
||||
addr = self.get_full_address() or self.address
|
||||
title = (self.name or '').strip()
|
||||
if self.location_type == 'pharmacy':
|
||||
# Номер без організації неоднозначний (різні підприємства можуть мати №12).
|
||||
try:
|
||||
@@ -202,7 +210,13 @@ class Location(BaseModel):
|
||||
status_label = status
|
||||
head = f'{head} [{status_label}]'
|
||||
return f'{head} — {addr}' if addr else head
|
||||
if self.location_type == 'vendor':
|
||||
head = title or self.get_location_type_display()
|
||||
return f'{head} — {addr}' if addr and addr != head else head
|
||||
type_label = self.get_location_type_display()
|
||||
if title:
|
||||
head = f'{type_label} — {title}'
|
||||
return f'{head} — {addr}' if addr and addr != title else head
|
||||
return f'{type_label} — {addr}' if addr else type_label
|
||||
|
||||
@property
|
||||
@@ -223,7 +237,8 @@ class Location(BaseModel):
|
||||
except Exception:
|
||||
text = f'{text} {status}'
|
||||
return text
|
||||
return self._display_label()
|
||||
parts = [self.name or '', self._display_label(), self.address or '']
|
||||
return ' '.join(p for p in parts if p)
|
||||
except Exception:
|
||||
return str(self.pk or '')
|
||||
|
||||
@@ -231,6 +246,8 @@ class Location(BaseModel):
|
||||
# "Полный адрес" — поле лише для читання (help_text це й обіцяв, але ніхто
|
||||
# ніколи насправді не проставляв значення) — перераховуємо з частин щоразу
|
||||
self.address = self.get_full_address()
|
||||
if not self.address and (self.name or '').strip():
|
||||
self.address = self.name.strip()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -680,3 +680,30 @@ class LocationSortKeyTests(TestCase):
|
||||
street='Старый', house_number='2', is_active=False,
|
||||
)
|
||||
self.assertNotIn(dead_office.pk, ids)
|
||||
|
||||
def test_vendor_location_in_select_and_report_code(self):
|
||||
from apps.it_manager.views import _it_locations_sorted
|
||||
from apps.locations.models.base import Location
|
||||
from apps.reports.it_monthly.builder import location_object_code
|
||||
from core.test_utils import grant_all_locations
|
||||
from apps.employees.tests import make_employee
|
||||
|
||||
region_type, _ = DirectoryType.objects.get_or_create(
|
||||
code='region', defaults={'name': 'Регион'},
|
||||
)
|
||||
region = Directory.objects.create(
|
||||
directory_type=region_type, code='od-vendor', name='Одесса-vendor',
|
||||
)
|
||||
vendor = Location.objects.create(
|
||||
location_type='vendor',
|
||||
name='Вайтер Юг',
|
||||
location_region=region,
|
||||
is_active=True,
|
||||
)
|
||||
self.assertEqual(str(vendor), 'Вайтер Юг')
|
||||
self.assertEqual(location_object_code(vendor), 'Вайтер Юг')
|
||||
|
||||
user = make_employee('it-loc-vendor')
|
||||
grant_all_locations(user)
|
||||
ids = {loc.pk for loc in _it_locations_sorted(user)}
|
||||
self.assertIn(vendor.pk, ids)
|
||||
|
||||
@@ -196,6 +196,8 @@ def location_object_code(location) -> str:
|
||||
return 'ОФИС'
|
||||
if location.location_type == 'warehouse':
|
||||
return 'Склад'
|
||||
if location.location_type == 'vendor':
|
||||
return (getattr(location, 'name', None) or '').strip() or 'Сервис'
|
||||
pharmacy = getattr(location, 'pharmacy', None)
|
||||
if pharmacy is not None:
|
||||
return pharmacy_object_code(pharmacy)
|
||||
|
||||
Reference in New Issue
Block a user