flower_mr 1 bulan lalu
induk
melakukan
e9352a284f

TEMPAT SAMPAH
bound/__pycache__/models.cpython-38.pyc


+ 25 - 0
bound/migrations/0003_boundbatchmodel_relate_material_and_more.py

@@ -0,0 +1,25 @@
+# Generated by Django 4.1.2 on 2025-04-29 20:42
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('erp', '0005_alter_inboundbill_options'),
+        ('bound', '0002_boundbatchmodel_goods_in_location_qty_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='boundbatchmodel',
+            name='relate_material',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bound_batch', to='erp.materialdetail', verbose_name='关联物料'),
+        ),
+        migrations.AddField(
+            model_name='boundlistmodel',
+            name='relate_bill',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bound_list', to='erp.inboundbill', verbose_name='关联单据'),
+        ),
+    ]

TEMPAT SAMPAH
bound/migrations/__pycache__/0003_boundbatchmodel_relate_material_and_more.cpython-38.pyc


+ 5 - 1
bound/models.py

@@ -1,8 +1,10 @@
 from django.db import models
-
+from erp.models import InboundBill, MaterialDetail
 
 
 class BoundListModel(models.Model):
+    STATUS = ("100", '入库申请'), ("101", '入库同意'), ("102", '组盘中'), ("200", '出库申请'), ("201", '出库同意'), ("202", '出库中'), ("203", '部分出库'), ("204", '已出库'), ("300", '取消申请')
+
     bound_month = models.CharField(max_length=255, verbose_name="月份")
     bound_date = models.DateField(verbose_name="单据日期")
 
@@ -23,6 +25,7 @@ class BoundListModel(models.Model):
     is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
     create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
     update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
+    relate_bill = models.ForeignKey(InboundBill, on_delete=models.CASCADE, verbose_name="关联单据", related_name='bound_list',null=True, blank=True)
 
     class Meta:
         db_table = 'boundlist'
@@ -70,6 +73,7 @@ class BoundBatchModel(models.Model):
     is_delete = models.BooleanField(default=False, verbose_name='Delete Label')
     create_time = models.DateTimeField(auto_now_add=True, verbose_name="Create Time")
     update_time = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name="Update Time")
+    relate_material = models.ForeignKey(MaterialDetail, on_delete=models.CASCADE, verbose_name="关联物料", related_name='bound_batch',null=True, blank=True)
     
     class Meta:
         db_table = 'boundbatch'

TEMPAT SAMPAH
db.sqlite3


TEMPAT SAMPAH
erp/__pycache__/models.cpython-38.pyc


TEMPAT SAMPAH
erp/__pycache__/urls.cpython-38.pyc


TEMPAT SAMPAH
erp/__pycache__/views.cpython-38.pyc


+ 17 - 0
erp/migrations/0004_alter_inboundbill_options.py

@@ -0,0 +1,17 @@
+# Generated by Django 4.1.2 on 2025-04-29 20:25
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('erp', '0003_alter_inboundbill_options_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='inboundbill',
+            options={'ordering': ['-create_time', '-update_time'], 'verbose_name': '生产入库单', 'verbose_name_plural': '生产入库单'},
+        ),
+    ]

+ 17 - 0
erp/migrations/0005_alter_inboundbill_options.py

@@ -0,0 +1,17 @@
+# Generated by Django 4.1.2 on 2025-04-29 20:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('erp', '0004_alter_inboundbill_options'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='inboundbill',
+            options={'ordering': ['bound_status', '-create_time', '-update_time'], 'verbose_name': '生产入库单', 'verbose_name_plural': '生产入库单'},
+        ),
+    ]

TEMPAT SAMPAH
erp/migrations/__pycache__/0004_alter_inboundbill_options.cpython-38.pyc


TEMPAT SAMPAH
erp/migrations/__pycache__/0005_alter_inboundbill_options.cpython-38.pyc


+ 1 - 1
erp/models.py

@@ -28,7 +28,7 @@ class InboundBill(models.Model):
     class Meta:
         verbose_name = '生产入库单'
         verbose_name_plural = verbose_name
-        ordering = ['-update_time', '-create_time']
+        ordering = [ 'bound_status','-create_time','-update_time']
 
 
 class MaterialDetail(models.Model):

+ 1 - 0
erp/urls.py

@@ -7,6 +7,7 @@ urlpatterns = [
     path('createOutboundApply', views.OutboundApplyCreate.as_view()),
     path('updateBatchInfo', views.BatchUpdate.as_view()),
     path('productInfo', views.ProductInfo.as_view()),
+    path('generateinbound', views.GenerateInbound.as_view()),
 
     path('inboundBills/', views.InboundBills.as_view({"get": "list"}),name="inboundBills"),
     re_path(r'^inboundBills/(?P<pk>\d+)/$', views.InboundBills.as_view({

+ 196 - 10
erp/views.py

@@ -7,12 +7,16 @@ from django_filters.rest_framework import DjangoFilterBackend
 from rest_framework.filters import OrderingFilter
 from django.utils import timezone
 from django.db.models import Q, F, Case, When
-
+from datetime import datetime
+from collections import defaultdict
 from utils.page import MyPageNumberPagination
 from .models import *
 from .serializers import *
 from .filter import *
 import logging
+from django.db import transaction
+
+from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchLogModel
 
 logger = logging.getLogger('wms.boundBill')
 
@@ -63,15 +67,6 @@ class WMSResponse:
                 "fail_materials": fail_materials
             }
         }, status=status)
-# Create your views here.
-# urlpatterns = [
-#     path('createInboundApply', views.InboundApplyCreate.as_view()),
-#     path('createOutboundApply', views.OutboundApplyCreate.as_view()),
-#     path('updateBatchInfo', views.BatchUpdate.as_view()),
-#     path('productInfo', views.ProductInfo.as_view()),
-
-
-# ]
 
 class InboundApplyCreate(APIView):
     """
@@ -267,6 +262,197 @@ class InboundApplyCreate(APIView):
                 )
         return material_detail
 
+
+class GenerateInbound(APIView):
+    """
+    生产入库单生成接口
+    功能特性:
+    1. 防重复创建校验(状态校验+关联单据校验)
+    2. 事务级数据一致性保障
+    3. 批量操作优化
+    4. 完整日志追踪
+    """
+
+    def post(self, request):
+        try:
+            bill_id = request.data.get('billId')
+            if not bill_id:
+                return Response({"error": "缺少必要参数: billId"}, 
+                              status=status.HTTP_400_BAD_REQUEST)
+
+            # 开启原子事务
+            with transaction.atomic():
+                # 锁定原始单据并校验
+                bill_obj, bound_list = self.validate_and_lock(bill_id)
+
+                # 创建出入库主单
+                bound_list = self.create_bound_list(bill_obj)
+
+                logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}")
+                # 处理物料明细(批量操作优化)
+                self.process_materials(bill_obj, bound_list)
+
+                # 更新原始单据状态
+                bill_obj.bound_status = 1
+                bill_obj.save(update_fields=['bound_status'])
+
+                logger.info(f"入库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}")
+                return Response({
+                    "bound_code": bound_list.bound_code,
+                    "batch_count": bill_obj.totalCount
+                }, status=status.HTTP_201_CREATED)
+
+        except InboundBill.DoesNotExist:
+            logger.error(f"原始单据不存在 | billId: {bill_id}")
+            return Response({"error": "原始单据不存在"}, status=status.HTTP_404_NOT_FOUND)
+        except Exception as e:
+            logger.exception(f"入库单生成异常 | billId: {bill_id}")
+            return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+    def validate_and_lock(self, bill_id):
+        """验证并锁定相关资源"""
+        # 锁定原始单据
+        bill_obj = InboundBill.objects.select_for_update().get(
+            billId=bill_id,
+            is_delete=False
+        )
+
+        # 状态校验
+        if bill_obj.bound_status == 1:
+            logger.warning(f"单据已生成过入库单 | status: {bill_obj.bound_status}")
+            raise Exception("该单据已生成过入库单")
+
+        # 关联单据校验(双重校验机制)
+        existing_bound = BoundListModel.objects.filter(
+            Q(bound_desc__contains=f"生产入库单{bill_obj.number}") |
+            Q(relate_bill=bill_obj)
+        ).first()
+
+        if existing_bound:
+            logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}")
+            raise Exception(f"已存在关联入库单[{existing_bound.bound_code}]")
+
+        return bill_obj, None
+
+    def process_materials(self, bill_obj, bound_list):
+        """批量处理物料明细"""
+        materials = MaterialDetail.objects.filter(
+            bound_billId=bill_obj,
+            is_delete=False
+        ).select_related('bound_billId')
+
+        if not materials:
+            raise Exception("入库单没有有效物料明细")
+
+        # 批量创建对象列表
+        batch_list = []
+        detail_list = []
+        log_list = []
+        goods_counter = defaultdict(int)
+        order_day=str(timezone.now().strftime('-%Y%m'))
+        order_month=str(timezone.now().strftime('%Y%m'))
+        for idx, material in enumerate(materials, 1):
+            # 生成批次
+            data = {}
+            qs_set = BoundBatchModel.objects.filter( goods_code=material.goods_code, bound_month=order_month,  is_delete=False)
+            goods_code = material.goods_code
+            goods_counter[goods_code] += 1
+            len_qs_set = len(qs_set) + goods_counter[goods_code]
+            print("len_qs_set", len_qs_set)
+            data['bound_batch_order'] = int(order_day.split('-')[-1])*100 + len_qs_set
+            data['bound_number'] = material.goods_code + order_day + str(len_qs_set).zfill(2)
+  
+            batch = BoundBatchModel(
+                bound_number=data['bound_number'],
+                bound_month=bound_list.bound_month,
+                bound_batch_order=data['bound_batch_order'],
+                warehouse_code='W01',
+                warehouse_name='立体仓',
+                goods_code=material.goods_code,
+                goods_desc=material.goods_name,
+                goods_std=material.goods_std,
+                goods_unit=material.goods_unit,
+                goods_qty=material.plan_qty,
+                goods_weight=float(material.goods_weight),
+                goods_total_weight=float(material.goods_total_weight),
+                creater=bound_list.creater,
+                openid=bound_list.openid,
+                relate_material=material
+            )
+            batch_list.append(batch)
+
+            # 生成明细
+            detail_list.append(BoundDetailModel(
+                bound_list=bound_list,
+                bound_batch=batch,
+                detail_code=f"{batch.bound_number}-DET",
+                creater=bound_list.creater,
+                openid=bound_list.openid
+            ))
+
+            # 生成日志
+            log_list.append(BatchLogModel(
+                batch_id=batch,
+                log_type=0,
+                log_date=timezone.now(),
+                goods_code=batch.goods_code,
+                goods_desc=batch.goods_desc,
+                goods_qty=batch.goods_qty,
+                log_content=f"生产入库批次创建,来源单据:{bill_obj.number}",
+                creater=batch.creater,
+                openid=batch.openid
+            ))
+
+        # 批量写入数据库
+        BoundBatchModel.objects.bulk_create(batch_list)
+        BoundDetailModel.objects.bulk_create(detail_list)
+        BatchLogModel.objects.bulk_create(log_list)
+
+    def create_bound_list(self, bill_obj):
+     
+        """创建出入库主单(带来源标识)"""
+        if BoundListModel.objects.filter(relate_bill=bill_obj).exists():
+            return BoundListModel.objects.get(relate_bill=bill_obj)
+
+        return BoundListModel.objects.create(
+            bound_month=timezone.now().strftime("%Y%m"),
+            bound_date=timezone.now().strftime("%Y-%m-%d"),
+
+            bound_code=bill_obj.number,           
+            bound_code_type=bill_obj.type,
+            bound_bs_type='B01',
+            bound_type='in',
+            bound_desc=f"生产入库单{bill_obj.number}",
+            bound_department=(bill_obj.department if bill_obj.department else 'D99'),
+            base_type=0,
+            bound_status='101',
+            creater=bill_obj.creater,
+            openid='ERP',
+            relate_bill=bill_obj 
+        )
+
+    def generate_sequence_code(self, prefix):
+        """生成带顺序的编号(示例实现)"""
+        timestamp = datetime.now().strftime("%Y%m%d%H%M")
+        last_code = BoundListModel.objects.filter(
+            bound_code__startswith=prefix
+        ).order_by('-id').values_list('bound_code', flat=True).first()
+        
+        if last_code:
+            seq = int(last_code[-3:]) + 1
+        else:
+            seq = 1
+            
+        return f"{prefix}{timestamp}-{seq:03d}"
+
+    def handle_exception(self, exc):
+        """统一异常处理"""
+        if isinstance(exc, InboundBill.DoesNotExist):
+            return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
+        elif "重复" in str(exc):
+            return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
+        return super().handle_exception(exc)
+
 class OutboundApplyCreate(APIView):
     """
     生产出库申请

File diff ditekan karena terlalu besar
+ 256 - 0
logs/boundBill.log


+ 11 - 0
logs/error.log

@@ -2160,3 +2160,14 @@ Traceback (most recent call last):
   File "d:\language\python38\lib\site-packages\django\db\models\sql\query.py", line 1709, in names_to_path
     raise FieldError(
 django.core.exceptions.FieldError: Cannot resolve keyword 'id' into field. Choices are: billId, bill_id, bound_status, create_time, creater, date, department, is_delete, note, number, totalCount, type, update_time, warehouse
+[2025-04-29 21:38:17,642][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:39:45,043][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:41:34,823][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:43:56,693][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:45:18,837][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:46:16,685][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:49:50,874][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:53:49,583][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:12:02,509][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:13:06,549][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:25:54,858][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound

+ 32 - 0
logs/server.log

@@ -2308,3 +2308,35 @@ django.core.exceptions.FieldError: Cannot resolve keyword 'id' into field. Choic
 [2025-04-29 19:46:16,954][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
 [2025-04-29 19:46:21,959][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
 [2025-04-29 19:46:26,950][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:46:37,165][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:46:41,963][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:46:46,968][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:46:47,001][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:32,650][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:36,746][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:36,962][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:41,940][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:46,953][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:51,965][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:47:56,982][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:48:01,965][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:48:06,951][django.request.log_response():241] [WARNING] Not Found: /erp/tasks/
+[2025-04-29 19:49:22,621][django.request.log_response():241] [WARNING] Not Found: /cyclecount/qtyrecorviewset/
+[2025-04-29 20:17:35,859][django.request.log_response():241] [WARNING] Not Found: /cyclecount/qtyrecorviewset/
+[2025-04-29 20:22:04,670][django.request.log_response():241] [WARNING] Bad Request: /wms/createInboundApply
+[2025-04-29 20:22:26,624][django.request.log_response():241] [WARNING] Bad Request: /wms/createInboundApply
+[2025-04-29 21:36:59,104][django.request.log_response():241] [WARNING] Bad Request: /wms/generateinbound
+[2025-04-29 21:38:17,642][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:39:45,043][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:41:34,823][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:43:56,693][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:45:18,837][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:46:16,685][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:49:50,874][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 21:53:49,583][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:12:02,509][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:13:06,549][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:25:54,858][django.request.log_response():241] [ERROR] Internal Server Error: /wms/generateinbound
+[2025-04-29 22:29:41,857][django.request.log_response():241] [WARNING] Not Found: /cyclecount/qtyrecorviewset/
+[2025-04-29 22:29:49,712][django.request.log_response():241] [WARNING] Not Found: /cyclecount/qtyrecorviewset/
+[2025-04-29 22:30:16,481][django.request.log_response():241] [WARNING] Not Found: /cyclecount/qtyrecorviewset/

+ 1 - 1
templates/src/layouts/MainLayout.vue

@@ -668,7 +668,7 @@ export default {
 
     _this.timer = setInterval(() => {
       _this.handleTimer()
-    }, 30000)
+    }, 10000)
   },
   updated() {
   },