lavkach3

Форк
0
402 строки · 21.3 Кб
1
import logging
2
import logging
3
import uuid
4
from typing import Any, Optional, List
5

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, \
12
    SuggestType
13
from app.inventory.order.schemas.move_schemas import MoveCreateScheme, MoveUpdateScheme, MoveFilter
14
from app.inventory.quant import Quant
15
#from app.inventory.order.services.move_tkq import move_set_done
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
20

21
logging.basicConfig(level=logging.INFO)
22
logger = logging.getLogger(__name__)
23

24

25
class MoveService(BaseService[Move, MoveCreateScheme, MoveUpdateScheme, MoveFilter]):
26
    """
27
    Сервис для работы с Мувами
28
    CRUD+
29
    Мув нельзя создать, если запаса недостаточно (это прям правило)
30
    Путь мува такоф:
31
        CREATED:    мув создан, но уже зарезервированы кванты что бы их никто не забрал
32
        WAITING:    мув ждет распределение в Order - этот шаг может быть пропущен, если мув исполняют без ордера
33
        CONFIRMED:  мув получил Order - этот шаг может быть пропущен, если мув исполняют без ордера
34
        ASSIGNED:   мув взял в работу сотрудник
35
        DONE:       мув завершен (terminal)
36
        CANCELED:   мув отменен (terminal)
37
    """
38

39
    def __init__(self, request: Request):
40
        super(MoveService, self).__init__(request, Move, MoveCreateScheme, MoveUpdateScheme)
41

42
    @permit('move_edit')
43
    async def update(self, id: Any, obj: UpdateSchemaType, commit: bool = True) -> Optional[ModelType]:
44
        return await super(MoveService, self).update(id, obj, commit)
45

46
    @permit('move_list')
47
    async def list(self, _filter: FilterSchemaType, size: int):
48
        return await super(MoveService, self).list(_filter, size)
49

50
    async def create_suggests(self, move: uuid.UUID | Move):
51
        """
52
            Создаются саджесты в зависимости от OrderType и Product
53
        """
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={
64
                    "move_id": move.id,
65
                    "priority": 1,
66
                    "type": SuggestType.IN_LOCATION,
67
                    "value": f'{location_src.id}',
68
                    # "user_id": self.user.user_id
69
                }, commit=False)
70
            """Ввод Партии"""  # TODO:  пока хз как праильное
71
            """Далее саджест на идентификацию товара"""
72
            await suggest_service.create(obj={
73
                "move_id": move.id,
74
                "priority": 2,
75
                "type": SuggestType.IN_PRODUCT,
76
                "value": f'{move.product_id}',
77
                # "user_id": self.user.user_id
78
            }, commit=False)
79
            """Далее саджест ввода количества"""
80
            await suggest_service.create(obj={
81
                "move_id": move.id,
82
                "priority": 3,
83
                "type": SuggestType.IN_QUANTITY,
84
                "value": f'{move.quantity}',
85
                # "user_id": self.user.user_id
86
            }, commit=False)
87
            """Далее саджест на ввод срока годности"""  # TODO:  пока хз как праильное
88
            """Далее саджест идентификации локации назначения"""
89
            await suggest_service.create(obj={
90
                "move_id": move.id,
91
                "priority": 4,
92
                "type": SuggestType.IN_LOCATION,
93
                "value": f'{location_dest.id}',
94
                # "user_id": self.user.user_id
95
            }, commit=False)
96
            # Итого простой кейс, отсканировал локацию-источник, отсканировал товар, отсканировал локацию-назначения
97

98
    @permit('move_user_assign')
99
    async def user_assign(self, move_id: uuid.UUID, user_id: uuid.UUID):
100
        """
101
            Если прикрепился пользователь, то значит, что необходимо создать саджесты для выполнения
102
        """
103
        move = await self.get(move_id)
104
        await move.create_suggests()
105
        move.user_id = user_id
106
        await self.session.commit()
107
        return move
108

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
112
        for m in moves:
113
            move = await self._confirm(m, parent, user_id)
114

115
        try:
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)}")
123
        return {
124
            "status": "OK",
125
            "detail": "Move is confirmed"
126
        }
127

128
    async def _confirm(self, move: uuid.UUID | Move, parent: Order | Move | None = None, user_id: uuid.UUID = None):
129
        """
130
        Входом для конфирма Мува может быть
131
         - Order - когда человек или система целенаправлено создают некий Ордер ( Ордер на примеку или Ордер на перемещение)
132
         - OrderType - когда Выбирается некий тип(правила) применяемые к определенному обьекту, Товарру/Упаковке
133
         - Move - Когда идет двуэтапное движение, тогда правила беруться из Родительского Мува
134
         - None - Тогда система подберет автоматически OrderType согласно подходящем
135

136
         Стратегия такова
137
         Если указана локация в move, то проверяется допустимость этой локации согласно правилам OrderType
138
         Если указан Quant, то сразу идем в Quant и фильтруем только его
139
         Если локация не указана, то поиск доступных квантов будет происходить в допустимых в OrderType
140
        """
141
        if isinstance(move, uuid.UUID):
142
            move = await self.get(move)
143
        if move.status == MoveStatus.CONFIRMED:
144
            return move
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:
151

152
            """ ПОДБОР ИСХОДЯЩЕГО КВАНТА/ЛОКАЦИИ"""
153

154
            quant_src_entity: Quant | None = None
155
            quant_dest_entity: Quant | None = None
156

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())  # TODO: его надо реализовать
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 """
171

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)
175
            )
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)
179
            )
180
            location_src_ids = list(
181
                set(order_type_entity.allowed_location_src_ids) -
182
                set(order_type_entity.exclude_location_src_ids)
183
            )
184
            if move.location_src_id:
185
                """Если мы указали локацию, то нас уже не интересуют правила из OrderType"""
186
                location_class_src_ids, location_type_src_ids, location_src_ids = [], [], [move.location_src_id, ]
187

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
197
            )
198
            # TODO: здесь нужно вставить метод FEFO, FIFO, LIFO, LEFO
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,
205
                    "is_active": True,
206
                    "is_can_negative": True
207
                }
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,
218
                        "quantity": 0.0,
219
                        "reserved_quantity": 0.0,
220
                        "incoming_quantity": 0.0,
221
                        "uom_id": move.uom_id,
222
                    }, commit=False)
223
                    available_src_quants = [quant_src_entity, ]
224
                    break
225

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:
232
                            pass
233
                        elif remainder <= src_quant.available_quantity:
234
                            src_quant.reserved_quantity += remainder
235
                            remainder = 0.0
236
                            quant_src_entity = src_quant
237
                            self.session.add(quant_src_entity)
238
                            break
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)
244
                            break
245
                    else:
246
                        pass  # TODO: единицы измерения
247
                if remainder:
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
256
                                    remainder = 0.0
257
                                    quant_src_entity = src_quant
258
                                    self.session.add(quant_src_entity)
259
                                    break
260
                            else:
261
                                ...  # TODO: единицы измерения
262
                    else:
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)
268

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
273

274
            """ ПОИСК КВАНТА/ЛОКАЦИИ НАЗНАЧЕНИЯ """
275

276
            """ Если у муве насильно указали location_dest_id"""
277
            """ Нужно проверить, что данная локация подходит под правила OrderType и правила самой выбранной локации"""
278

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)
282
            )
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)
286
            )
287
            location_dest_ids = list(
288
                set(order_type_entity.allowed_location_dest_ids) -
289
                set(order_type_entity.exclude_location_dest_ids)
290
            )
291

292
            if move.location_dest_id:
293
                """Если мы указали локацию, то нас уже не интересуют правила из OrderType"""
294
                location_class_dest_ids, location_type_dest_ids, location_dest_ids = [], [], [move.location_dest_id, ]
295

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
306
            )
307
            # TODO: здесь нужно вставить метод Putaway
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,
314
                    "is_active": True,
315
                }
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,
326
                        "quantity": 0.0,
327
                        "reserved_quantity": 0.0,
328
                        "incoming_quantity": 0.0,
329
                        "uom_id": move.uom_id,
330
                    }, commit=False)
331
                    available_dest_quants = [quant_dest_entity, ]
332
                    break
333

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)
338
                break
339
            if not quant_dest_entity:
340
                raise ModuleException(status_code=406, enum=MoveErrors.DEST_QUANT_ERROR)
341

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)
347

348
            self.session.add(move)
349
            if not quant_src_entity.move_ids:
350
                quant_src_entity.move_ids = [move.id]
351
            else:
352
                quant_src_entity.move_ids.append(move.id)
353

354
            if not quant_dest_entity.move_ids:
355
                quant_dest_entity.move_ids = [move.id]
356
            else:
357
                quant_dest_entity.move_ids.append(move.id)
358
        # TODO: это надо убрать в отдельный вызов
359
        await self.create_suggests(move)
360
        return move
361

362
    async def set_done(self, move_id: uuid.UUID, sync=False):
363
        """
364
            Метод пытается завершить мув, если все саджесты закончены
365
        """
366
        if not sync:
367
            await move_set_done.kiq(move_id)
368

369
        return True
370
    list_brocker.register_task(set_done)
371

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)
377

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)
385

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)
389

390
    @permit('get_moves_by_barcode')
391
    async def get_moves_by_barcode(self, barcode: str, order_id: uuid.UUID) -> List[ModelType]:
392
        """
393
            Если прикрепился пользователь, то значит, что необходимо создать саджесты для выполнения
394
        """
395
        query = select(self.model)
396
        product_obj = await self.env['product'].adapter.product_by_barcode(barcode)
397
        if order_id:
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()
402
        return move_entities
403

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.