forked from erp-dev/erp
fix: query param of api_man
This commit is contained in:
@@ -67,6 +67,12 @@ class ProductSerializer(BaseSerializer):
|
|||||||
- 如果有图片:返回完整 URL(七牛云 CDN 地址)
|
- 如果有图片:返回完整 URL(七牛云 CDN 地址)
|
||||||
- 如果没有图片:返回 None
|
- 如果没有图片:返回 None
|
||||||
"""
|
"""
|
||||||
|
primary_url_resolver = getattr(obj, 'get_primary_image_url', None)
|
||||||
|
if callable(primary_url_resolver):
|
||||||
|
resolved = primary_url_resolver()
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
|
||||||
if obj.image:
|
if obj.image:
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
if request:
|
if request:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as dj_filters
|
from django_filters import rest_framework as dj_filters
|
||||||
|
|
||||||
@@ -68,11 +70,18 @@ class BasicInfoFilterMixin:
|
|||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
field = self._detect_filter_field()
|
field = self._detect_filter_field()
|
||||||
model = getattr(getattr(self, 'queryset', None), 'model', None)
|
model = getattr(getattr(self, 'queryset', None), 'model', None)
|
||||||
if not field or not model:
|
if field and model:
|
||||||
return super().filter_queryset(queryset)
|
self.filterset_class = self._build_filterset_class(model, field)
|
||||||
|
|
||||||
self.filterset_class = self._build_filterset_class(model, field)
|
qs = super().filter_queryset(queryset)
|
||||||
return super().filter_queryset(queryset)
|
|
||||||
|
# 统一按创建时间倒序排序(如果模型存在 created_at 字段)
|
||||||
|
model = getattr(qs, 'model', model)
|
||||||
|
if model:
|
||||||
|
field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
|
||||||
|
if 'created_at' in field_names:
|
||||||
|
return qs.order_by('-created_at')
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||||
@@ -106,6 +115,11 @@ class QuickInputViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
|||||||
qs = qs.filter(group=group)
|
qs = qs.filter(group=group)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='groups')
|
||||||
|
def groups(self, request):
|
||||||
|
groups = serializers.basic_models.QuickInput.objects.values_list('group', flat=True).distinct()
|
||||||
|
return Response(groups)
|
||||||
|
|
||||||
|
|
||||||
class ProductViewSet(BaseViewSet):
|
class ProductViewSet(BaseViewSet):
|
||||||
queryset = serializers.basic_models.Product.objects
|
queryset = serializers.basic_models.Product.objects
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ def _map_unit(unit_label: str | None) -> int:
|
|||||||
'米': basic_models.ProductUnitEnum.METER,
|
'米': basic_models.ProductUnitEnum.METER,
|
||||||
'码': basic_models.ProductUnitEnum.YARD,
|
'码': basic_models.ProductUnitEnum.YARD,
|
||||||
'公斤': basic_models.ProductUnitEnum.KG,
|
'公斤': basic_models.ProductUnitEnum.KG,
|
||||||
|
'段': basic_models.ProductUnitEnum.SEGMENT,
|
||||||
}
|
}
|
||||||
return mapping.get(unit_label, basic_models.ProductUnitEnum.METER)
|
return mapping.get(unit_label, basic_models.ProductUnitEnum.METER)
|
||||||
|
|
||||||
@@ -152,6 +153,19 @@ def _ensure_decimal(value: str | None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_int(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError, InvalidOperation):
|
||||||
|
# 有些值可能是 Decimal 或字符串数字
|
||||||
|
try:
|
||||||
|
return int(Decimal(str(value)))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _run_fetch(page: int, page_size: int):
|
def _run_fetch(page: int, page_size: int):
|
||||||
return asyncio.run(fetch_products_from_mingdaoyun(page=page, page_size=page_size))
|
return asyncio.run(fetch_products_from_mingdaoyun(page=page, page_size=page_size))
|
||||||
|
|
||||||
@@ -175,6 +189,12 @@ def _upsert_product(product_data, merchant, category):
|
|||||||
width_decimal = _ensure_decimal(product_data.width)
|
width_decimal = _ensure_decimal(product_data.width)
|
||||||
if width_decimal is not None:
|
if width_decimal is not None:
|
||||||
defaults['width_size'] = width_decimal
|
defaults['width_size'] = width_decimal
|
||||||
|
pieces_int = _ensure_int(product_data.pieces)
|
||||||
|
if pieces_int is not None:
|
||||||
|
defaults['pieces'] = pieces_int
|
||||||
|
segment_int = _ensure_int(product_data.segment_size)
|
||||||
|
if segment_int is not None:
|
||||||
|
defaults['segment_size'] = segment_int
|
||||||
|
|
||||||
product_obj, created = basic_models.Product.objects.get_or_create(
|
product_obj, created = basic_models.Product.objects.get_or_create(
|
||||||
merchant=merchant,
|
merchant=merchant,
|
||||||
@@ -387,4 +407,3 @@ def sync_mdy_customers(self, page_size: int = 300, max_pages: int | None = None,
|
|||||||
}
|
}
|
||||||
logger.info('明道云客户同步完成: %s', payload)
|
logger.info('明道云客户同步完成: %s', payload)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|||||||
18
basic_info/migrations/0020_product_mdy_image_url.py
Normal file
18
basic_info/migrations/0020_product_mdy_image_url.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-09 06:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0019_customer_mdy_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='mdy_image_url',
|
||||||
|
field=models.URLField(blank=True, help_text='来自明道云的图片链接', max_length=500, null=True, verbose_name='明道云图片URL'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
basic_info/migrations/0021_alter_product_unit.py
Normal file
18
basic_info/migrations/0021_alter_product_unit.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-09 07:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0020_product_mdy_image_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='product',
|
||||||
|
name='unit',
|
||||||
|
field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤'), (4, '段')], default=1, verbose_name='单位'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-09 07:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0021_alter_product_unit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='pieces',
|
||||||
|
field=models.IntegerField(blank=True, null=True, verbose_name='件数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='segment_size',
|
||||||
|
field=models.IntegerField(blank=True, null=True, verbose_name='一段尺寸'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import json
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from flower.common import ModelBase
|
from flower.common import ModelBase
|
||||||
@@ -36,6 +37,7 @@ class ProductUnitEnum(models.IntegerChoices):
|
|||||||
METER = 1, '米'
|
METER = 1, '米'
|
||||||
YARD = 2, '码'
|
YARD = 2, '码'
|
||||||
KG = 3, '公斤'
|
KG = 3, '公斤'
|
||||||
|
SEGMENT = 4, '段'
|
||||||
|
|
||||||
|
|
||||||
class EmployeeTypeEnum(models.TextChoices):
|
class EmployeeTypeEnum(models.TextChoices):
|
||||||
@@ -226,6 +228,8 @@ class Product(ModelBase):
|
|||||||
human_id = models.CharField(max_length=100, verbose_name='产品编号', blank=True)
|
human_id = models.CharField(max_length=100, verbose_name='产品编号', blank=True)
|
||||||
color = models.CharField(max_length=50, blank=True, null=True, verbose_name='颜色')
|
color = models.CharField(max_length=50, blank=True, null=True, verbose_name='颜色')
|
||||||
width_size = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name='宽幅')
|
width_size = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name='宽幅')
|
||||||
|
pieces = models.IntegerField(blank=True, null=True, verbose_name='件数')
|
||||||
|
segment_size = models.IntegerField(blank=True, null=True, verbose_name='一段尺寸')
|
||||||
single_price_in = models.DecimalField(
|
single_price_in = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
@@ -253,6 +257,13 @@ class Product(ModelBase):
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name='产品图片',
|
verbose_name='产品图片',
|
||||||
)
|
)
|
||||||
|
mdy_image_url = models.URLField(
|
||||||
|
max_length=500,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='明道云图片URL',
|
||||||
|
help_text='来自明道云的图片链接',
|
||||||
|
)
|
||||||
minimum_quantity = models.IntegerField(null=True, blank=True, verbose_name='最低库存量')
|
minimum_quantity = models.IntegerField(null=True, blank=True, verbose_name='最低库存量')
|
||||||
transform_rate = models.DecimalField(
|
transform_rate = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
@@ -267,6 +278,36 @@ class Product(ModelBase):
|
|||||||
help_text='标记该产品是否通过明道云接口同步',
|
help_text='标记该产品是否通过明道云接口同步',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_primary_image_url(self):
|
||||||
|
"""
|
||||||
|
优先返回 mdy_image_url;若为空则尝试从 description JSON 中提取 original_file_full_path。
|
||||||
|
"""
|
||||||
|
if self.mdy_image_url:
|
||||||
|
return self.mdy_image_url
|
||||||
|
|
||||||
|
if not self.description:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(self.description) if isinstance(self.description, str) else self.description
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
items = [payload]
|
||||||
|
elif isinstance(payload, list):
|
||||||
|
items = payload
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
url = item.get('original_file_full_path') or item.get('DownloadUrl')
|
||||||
|
if url:
|
||||||
|
return url
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,8 @@ class Product(BaseModel):
|
|||||||
uid: str
|
uid: str
|
||||||
rowid: str
|
rowid: str
|
||||||
name: str
|
name: str
|
||||||
|
pieces: int | None
|
||||||
|
segment_size: int | None
|
||||||
unit: str | None
|
unit: str | None
|
||||||
color: str | None
|
color: str | None
|
||||||
width: str | None = None
|
width: str | None = None
|
||||||
@@ -173,9 +175,11 @@ class MingDaoYunClient:
|
|||||||
|
|
||||||
product_type_map = {
|
product_type_map = {
|
||||||
'name': '668caa5eb80969563ecaee7d',
|
'name': '668caa5eb80969563ecaee7d',
|
||||||
|
'pieces': '689c5aa37c175367b29fcb87',
|
||||||
'color': '66ed3b9ae01d5599bdb45f6d',
|
'color': '66ed3b9ae01d5599bdb45f6d',
|
||||||
'uid': '668caa5eb80969563ecaee7c',
|
'uid': '668caa5eb80969563ecaee7c',
|
||||||
'unit': '668e2ee2ca758c8cc5d0dc1d',
|
'unit': '668e2ee2ca758c8cc5d0dc1d',
|
||||||
|
'segment_size': '668e2194790c0e04058b4f3c',
|
||||||
'width': '668e2194790c0e04058b4f3c',
|
'width': '668e2194790c0e04058b4f3c',
|
||||||
'detail_str': '668caa5eb80969563ecaee7e',
|
'detail_str': '668caa5eb80969563ecaee7e',
|
||||||
'created_at': 'ctime',
|
'created_at': 'ctime',
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-09 07:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0008_alter_stockchangerecord_source_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockchangedetail',
|
||||||
|
name='unit',
|
||||||
|
field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤'), (4, '段')], verbose_name='单位'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockfreeze',
|
||||||
|
name='unit',
|
||||||
|
field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤'), (4, '段')], verbose_name='单位'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stocksnapshot',
|
||||||
|
name='unit',
|
||||||
|
field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤'), (4, '段')], verbose_name='单位'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transferorderitem',
|
||||||
|
name='unit',
|
||||||
|
field=models.IntegerField(choices=[(1, '米'), (2, '码'), (3, '公斤'), (4, '段')], verbose_name='单位'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user