4
from typing import Any, Optional, List
6
from sqlalchemy import select
7
from starlette.exceptions import HTTPException
8
from starlette.requests import Request
9
from app.inventory.location.enums import VirtualLocationClass
10
from app.inventory.order.enums.exceptions_move_enums import MoveErrors
11
from app.inventory.order.models.order_models import Move, MoveType, Order, OrderType, MoveStatus, \
13
from app.inventory.order.schemas.move_schemas import MoveCreateScheme, MoveUpdateScheme, MoveFilter
14
from app.inventory.quant import Quant
16
from core.helpers.broker.tkq import list_brocker
17
from core.exceptions.module import ModuleException
18
from core.permissions import permit
19
from core.service.base import BaseService, UpdateSchemaType, ModelType, FilterSchemaType, CreateSchemaType
21
logging.basicConfig(level=logging.INFO)
22
logger = logging.getLogger(__name__)
25
class MoveService(BaseService[Move, MoveCreateScheme, MoveUpdateScheme, MoveFilter]):
27
Сервис для работы с Мувами
29
Мув нельзя создать, если запаса недостаточно (это прям правило)
31
CREATED: мув создан, но уже зарезервированы кванты что бы их никто не забрал
32
WAITING: мув ждет распределение в Order - этот шаг может быть пропущен, если мув исполняют без ордера
33
CONFIRMED: мув получил Order - этот шаг может быть пропущен, если мув исполняют без ордера
34
ASSIGNED: мув взял в работу сотрудник
35
DONE: мув завершен (terminal)
36
CANCELED: мув отменен (terminal)
39
def __init__(self, request: Request):
40
super(MoveService, self).__init__(request, Move, MoveCreateScheme, MoveUpdateScheme)
43
async def update(self, id: Any, obj: UpdateSchemaType, commit: bool = True) -> Optional[ModelType]:
44
return await super(MoveService, self).update(id, obj, commit)
47
async def list(self, _filter: FilterSchemaType, size: int):
48
return await super(MoveService, self).list(_filter, size)
50
async def create_suggests(self, move: uuid.UUID | Move):
52
Создаются саджесты в зависимости от OrderType и Product
54
if isinstance(move, uuid.UUID):
55
move = await self.get(move)
56
suggest_service = self.env['suggest'].service
57
location_service = self.env['location'].service
58
if move.type == MoveType.PRODUCT:
59
"""Если это перемещение товара из виртуальцой локации, то идентификация локации не нужна"""
60
location_src = await location_service.get(move.location_src_id)
61
location_dest = await location_service.get(move.location_dest_id)
62
if not location_src.location_class in VirtualLocationClass:
63
await suggest_service.create(obj={
66
"type": SuggestType.IN_LOCATION,
67
"value": f'{location_src.id}',
71
"""Далее саджест на идентификацию товара"""
72
await suggest_service.create(obj={
75
"type": SuggestType.IN_PRODUCT,
76
"value": f'{move.product_id}',
79
"""Далее саджест ввода количества"""
80
await suggest_service.create(obj={
83
"type": SuggestType.IN_QUANTITY,
84
"value": f'{move.quantity}',
87
"""Далее саджест на ввод срока годности"""
88
"""Далее саджест идентификации локации назначения"""
89
await suggest_service.create(obj={
92
"type": SuggestType.IN_LOCATION,
93
"value": f'{location_dest.id}',
98
@permit('move_user_assign')
99
async def user_assign(self, move_id: uuid.UUID, user_id: uuid.UUID):
101
Если прикрепился пользователь, то значит, что необходимо создать саджесты для выполнения
103
move = await self.get(move_id)
104
await move.create_suggests()
105
move.user_id = user_id
106
await self.session.commit()
109
async def confirm(self, moves: List[uuid.UUID] | List[Move], parent: Order | Move | None = None,
110
user_id: uuid.UUID = None):
111
user_id = user_id or self.user.user_id
113
move = await self._confirm(m, parent, user_id)
116
await self.session.commit()
117
await self.session.refresh(move)
118
message = await self.prepare_bus(move, 'update')
119
await self.update_notify.kiq('move', message)
120
except Exception as ex:
121
await self.session.rollback()
122
raise HTTPException(status_code=500, detail=f"ERROR: {str(ex)}")
125
"detail": "Move is confirmed"
128
async def _confirm(self, move: uuid.UUID | Move, parent: Order | Move | None = None, user_id: uuid.UUID = None):
130
Входом для конфирма Мува может быть
131
- Order - когда человек или система целенаправлено создают некий Ордер ( Ордер на примеку или Ордер на перемещение)
132
- OrderType - когда Выбирается некий тип(правила) применяемые к определенному обьекту, Товарру/Упаковке
133
- Move - Когда идет двуэтапное движение, тогда правила беруться из Родительского Мува
134
- None - Тогда система подберет автоматически OrderType согласно подходящем
137
Если указана локация в move, то проверяется допустимость этой локации согласно правилам OrderType
138
Если указан Quant, то сразу идем в Quant и фильтруем только его
139
Если локация не указана, то поиск доступных квантов будет происходить в допустимых в OrderType
141
if isinstance(move, uuid.UUID):
142
move = await self.get(move)
143
if move.status == MoveStatus.CONFIRMED:
145
if move.status != MoveStatus.CREATED:
146
raise ModuleException(status_code=406, enum=MoveErrors.WRONG_STATUS)
147
location_service = self.env['location'].service
148
order_type_service = self.env['order_type'].service
149
quant_service = self.env['quant'].service
150
if move.type == MoveType.PRODUCT:
152
""" ПОДБОР ИСХОДЯЩЕГО КВАНТА/ЛОКАЦИИ"""
154
quant_src_entity: Quant | None = None
155
quant_dest_entity: Quant | None = None
157
assert move.product_id, 'Product is required if Move Type is Product'
158
if not move.order_type_id:
159
order_type_entity = order_type_service.get_by_attrs(**obj.model_dump())
160
elif move.order_type_id:
161
order_type_entity = await order_type_service.get(move.order_type_id)
162
elif isinstance(parent, Order):
163
order_type_entity = parent.order_type_rel
164
elif isinstance(parent, Move):
165
order_type_entity = await order_type_service.get(parent.order_type_id)
166
elif isinstance(parent, OrderType):
167
order_type_entity = parent
168
elif isinstance(parent, OrderType):
169
order_type_entity = parent
170
"""Проверяем, что мув может быть создан согласно праавилам в Order type """
172
location_class_src_ids = list(
173
set(order_type_entity.allowed_location_class_src_ids) -
174
set(order_type_entity.exclude_location_class_src_ids)
176
location_type_src_ids = list(
177
set(order_type_entity.allowed_location_type_src_ids) -
178
set(order_type_entity.exclude_location_type_src_ids)
180
location_src_ids = list(
181
set(order_type_entity.allowed_location_src_ids) -
182
set(order_type_entity.exclude_location_src_ids)
184
if move.location_src_id:
185
"""Если мы указали локацию, то нас уже не интересуют правила из OrderType"""
186
location_class_src_ids, location_type_src_ids, location_src_ids = [], [], [move.location_src_id, ]
188
available_src_quants = await quant_service.get_available_quants(
189
product_id=move.product_id,
190
store_id=move.store_id,
191
id=move.quant_src_id,
192
location_class_ids=location_class_src_ids,
193
location_ids=location_src_ids,
194
location_type_ids=location_type_src_ids,
195
lot_ids=[move.lot_id] if move.lot_id else None,
196
partner_id=move.partner_id if move.partner_id else None
199
if not available_src_quants:
200
"""Поиск локаций, которые могут быть negative"""
201
location_src_search_params = {
202
"location_class__in": location_class_src_ids,
203
"location_type_id__in": location_type_src_ids,
204
"id__in": location_src_ids,
206
"is_can_negative": True
208
locations_src = await location_service.list(_filter=location_src_search_params)
209
for loc_src in locations_src:
210
quant_src_entity = await quant_service.create(obj={
211
"product_id": move.product_id,
212
"store_id": move.store_id,
213
"location_id": loc_src.id,
214
"location_class": loc_src.location_class,
215
"location_type_id": loc_src.location_type_id,
216
"lot_id": move.lot_id.id if move.lot_id else None,
217
"partner_id": move.partner_id.id if move.partner_id else None,
219
"reserved_quantity": 0.0,
220
"incoming_quantity": 0.0,
221
"uom_id": move.uom_id,
223
available_src_quants = [quant_src_entity, ]
226
if available_src_quants:
227
"""Если кванты нашлись"""
228
remainder = move.quantity
229
for src_quant in available_src_quants:
230
if move.uom_id == src_quant.uom_id:
231
if src_quant.available_quantity <= 0.0:
233
elif remainder <= src_quant.available_quantity:
234
src_quant.reserved_quantity += remainder
236
quant_src_entity = src_quant
237
self.session.add(quant_src_entity)
239
elif remainder >= src_quant.available_quantity:
240
remainder -= src_quant.available_quantity
241
src_quant.quantity = 0.0
242
quant_src_entity = src_quant
243
self.session.add(quant_src_entity)
248
if remainder == move.quantity:
249
"Если не нашли свободного количества вообще"
250
"снова идем по квантам и берем то, у которого локация позволяет сделать отрицательный остсток"
251
for src_quant in available_src_quants:
252
if move.uom_id == src_quant.uom_id:
253
q_location = await location_service.get(src_quant.location_id)
254
if q_location.is_can_negative:
255
src_quant.reserved_quantity += remainder
257
quant_src_entity = src_quant
258
self.session.add(quant_src_entity)
263
"Если квант нашелся, но на частичное количество уменьшаем количество в муве тк 1 мув = 1квант"
264
logger.warning(f'The number in the move has been reduced')
265
move.quantity -= remainder
266
if not quant_src_entity:
267
raise ModuleException(status_code=406, enum=MoveErrors.SOURCE_QUANT_ERROR)
269
move.quant_src_id = quant_src_entity.id
270
move.location_src_id = quant_src_entity.location_id
271
move.lot_id = quant_src_entity.lot_id
272
move.partner_id = quant_src_entity.partner_id
274
""" ПОИСК КВАНТА/ЛОКАЦИИ НАЗНАЧЕНИЯ """
276
""" Если у муве насильно указали location_dest_id"""
277
""" Нужно проверить, что данная локация подходит под правила OrderType и правила самой выбранной локации"""
279
location_class_dest_ids = list(
280
set(order_type_entity.allowed_location_class_dest_ids) -
281
set(order_type_entity.exclude_location_class_dest_ids)
283
location_type_dest_ids = list(
284
set(order_type_entity.allowed_location_type_dest_ids) -
285
set(order_type_entity.exclude_location_type_dest_ids)
287
location_dest_ids = list(
288
set(order_type_entity.allowed_location_dest_ids) -
289
set(order_type_entity.exclude_location_dest_ids)
292
if move.location_dest_id:
293
"""Если мы указали локацию, то нас уже не интересуют правила из OrderType"""
294
location_class_dest_ids, location_type_dest_ids, location_dest_ids = [], [], [move.location_dest_id, ]
296
available_dest_quants = await quant_service.get_available_quants(
297
product_id=move.product_id,
298
store_id=move.store_id,
299
id=move.quant_dest_id,
300
exclude_id=quant_src_entity.id,
301
location_class_ids=location_class_dest_ids,
302
location_ids=location_dest_ids,
303
location_type_ids=location_type_dest_ids,
304
lot_ids=[move.lot_id] if move.lot_id else None,
305
partner_id=move.partner_id if move.partner_id else None
308
if not available_dest_quants:
309
"""Поиск локаций, которые могут быть negative"""
310
location_dest_search_params = {
311
"location_class__in": location_class_dest_ids,
312
"location_type_id__in": location_type_dest_ids,
313
"id__in": location_dest_ids,
316
locations_dest = await location_service.list(_filter=location_dest_search_params)
317
for loc_dest in locations_dest:
318
quant_dest_entity = await quant_service.create(obj={
319
"product_id": move.product_id,
320
"store_id": move.store_id,
321
"location_id": loc_dest.id,
322
"location_class": loc_dest.location_class,
323
"location_type_id": loc_dest.location_type_id,
324
"lot_id": move.lot_id.id if move.lot_id else None,
325
"partner_id": move.partner_id.id if move.partner_id else None,
327
"reserved_quantity": 0.0,
328
"incoming_quantity": 0.0,
329
"uom_id": move.uom_id,
331
available_dest_quants = [quant_dest_entity, ]
334
for dest_quant in available_dest_quants:
335
quant_dest_entity = dest_quant
336
quant_dest_entity.incoming_quantity += move.quantity
337
self.session.add(quant_dest_entity)
339
if not quant_dest_entity:
340
raise ModuleException(status_code=406, enum=MoveErrors.DEST_QUANT_ERROR)
342
move.quant_dest_id = quant_dest_entity.id
343
move.location_dest_id = quant_dest_entity.location_id
344
move.status = MoveStatus.CONFIRMED
345
if quant_src_entity == quant_dest_entity:
346
raise ModuleException(status_code=406, enum=MoveErrors.EQUAL_QUANT_ERROR)
348
self.session.add(move)
349
if not quant_src_entity.move_ids:
350
quant_src_entity.move_ids = [move.id]
352
quant_src_entity.move_ids.append(move.id)
354
if not quant_dest_entity.move_ids:
355
quant_dest_entity.move_ids = [move.id]
357
quant_dest_entity.move_ids.append(move.id)
359
await self.create_suggests(move)
362
async def set_done(self, move_id: uuid.UUID, sync=False):
364
Метод пытается завершить мув, если все саджесты закончены
367
await move_set_done.kiq(move_id)
370
list_brocker.register_task(set_done)
372
@permit('move_create')
373
async def create(self, obj: CreateSchemaType, parent: Order | Move | None = None, commit=True) -> ModelType:
374
obj.created_by = self.user.user_id
375
obj.edited_by = self.user.user_id
376
return await super(MoveService, self).create(obj)
378
@permit('move_delete')
379
async def delete(self, id: uuid.UUID) -> None:
380
if isinstance(id, uuid.UUID):
381
move = await self.get(id)
382
if move.status != MoveStatus.CREATED:
383
raise ModuleException(status_code=406, enum=MoveErrors.WRONG_STATUS)
384
return await super(MoveService, self).delete(id)
386
@permit('move_move_counstructor')
387
async def move_counstructor(self, move_id: uuid.UUID, moves: list) -> None:
388
return await super(MoveService, self).delete(id)
390
@permit('get_moves_by_barcode')
391
async def get_moves_by_barcode(self, barcode: str, order_id: uuid.UUID) -> List[ModelType]:
393
Если прикрепился пользователь, то значит, что необходимо создать саджесты для выполнения
395
query = select(self.model)
396
product_obj = await self.env['product'].adapter.product_by_barcode(barcode)
398
query = query.where(self.model.order_id == order_id)
399
query = query.where(self.model.product_id == product_obj.id)
400
executed_data = await self.session.execute(query)
401
move_entities = executed_data.scalars().all()