forked from erp-dev/erp
feat: payment order and receipt order
This commit is contained in:
@@ -32,17 +32,18 @@
|
||||
### 2.3 OrderCounterpartyMixin
|
||||
对外暴露统一的 `get_counterparty()` 接口,通过 `get_counterparty_field_name()`(或 `counterparty_field_name` 属性)确定具体业务主体字段。这样采购单/销售单分别返回 `Supplier` 与 `Customer`,但调用方只需面对一个接口。
|
||||
|
||||
### 2.4 PurchaseOrder / SalesOrder
|
||||
两类订单模型均继承上述三个 Mixin:
|
||||
### 2.4 订单类型
|
||||
当前业务对象覆盖:
|
||||
|
||||
- 采购单:`get_direction()` 返回 `1`,`get_counterparty_field_name()` 返回 `supplier`
|
||||
- 销售单:`get_direction()` 返回 `-1`,`get_counterparty_field_name()` 返回 `customer`
|
||||
- 其余字段(`merchant/warehouse/operator/status/items`)保持一致,便于服务、序列化与统计逻辑复用
|
||||
- **PurchaseOrder**(采购单):`direction=1`,`counterparty=supplier`,含库存明细。
|
||||
- **SalesOrder**(销售单):`direction=-1`,`counterparty=customer`,含库存明细;严进严出模式需记录 `consume_detail_ids`。
|
||||
- **PaymentOrder**(付款单):`direction=-1`,`counterparty=supplier`,仅金额字段,不触发库存。
|
||||
- **ReceiptOrder**(收款单):`direction=-1`,`counterparty=customer`,仅金额字段,不触发库存。
|
||||
|
||||
通过 mixin,两个模型天然具备:
|
||||
- 金额聚合与带方向金额计算
|
||||
- 统一的业务主体读取接口
|
||||
- 与库存/财务交互时一致的 `StockFlowService` payload
|
||||
通过 mixin,所有单据都具备:
|
||||
- 统一的金额聚合与方向计算(资金/库存可共用 `get_signed_total_amount()`)
|
||||
- 一致的业务主体接口(供应商/客户)
|
||||
- 可扩展的服务与序列化模式(有无库存由具体模型决定)
|
||||
|
||||
## 3. 服务层约定
|
||||
|
||||
@@ -52,10 +53,10 @@
|
||||
|
||||
## 4. 未来演进建议
|
||||
|
||||
1. **新增单据**:若未来出现调拨单、退货单等,优先继承 `OrderItemsAggregationMixin + OrderDirectionMixin + OrderCounterpartyMixin`,仅通过 `get_direction()` / `get_counterparty_field_name()` 区别方向与主体,减少重复实现。
|
||||
1. **新增单据**:若未来出现调拨单、退货单或更多资金类单据,优先复用 `OrderDirectionMixin + OrderCounterpartyMixin`(如有明细再叠加 `OrderItemsAggregationMixin`),仅通过 `get_direction()` / `get_counterparty_field_name()` 区别方向与主体,减少重复实现。
|
||||
2. **审批/状态机**:采购与销售如需共享状态流转,可提炼状态机或 service 层 mixin,而无需在模型层合并。
|
||||
3. **统计与报表**:财务/库存统计应依赖 `get_signed_total_amount()` / `get_direction()`,确保采购/销售、退货/正向都能通过统一接口处理。
|
||||
4. **文档同步**:新增单据或服务时必须更新本文件,描述新增模型如何复用 mixin、如何影响下游模块,保持设计透明。
|
||||
|
||||
以上约定的目标是:**保持采购单与未来销售单等业务对象的独立性,同时通过 mixin/服务层抽象复用绝大多数公共逻辑**。如需变更此架构(例如重新合并模型或修改核心 mixin 行为),请在评估后更新本文件,明确变化原因与迁移方案。
|
||||
以上约定的目标是:**保持各类业务单据的独立性,同时通过 mixin/服务层抽象复用绝大多数公共逻辑**。如需变更此架构,请在评估后更新本文件,说明原因与迁移方案。
|
||||
|
||||
|
||||
56
business/migrations/0011_paymentorder_receiptorder.py
Normal file
56
business/migrations/0011_paymentorder_receiptorder.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-30 13:20
|
||||
|
||||
import business.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('basic_info', '0016_merchantsetting_type'),
|
||||
('business', '0010_salesorder_salesorderitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PaymentOrder',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('payment_date', models.DateField(verbose_name='付款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='金额')),
|
||||
('status', models.IntegerField(choices=[(1, '审批中'), (2, '审批通过'), (3, '作废')], default=1, verbose_name='状态')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_orders', to='basic_info.merchant', verbose_name='所属商户')),
|
||||
('operator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_orders', to='basic_info.employee', verbose_name='经办人')),
|
||||
('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='payment_orders', to='basic_info.supplier', verbose_name='供应商')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '付款单',
|
||||
'verbose_name_plural': '付款单',
|
||||
},
|
||||
bases=(business.models.OrderDirectionMixin, business.models.OrderCounterpartyMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReceiptOrder',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('receipt_date', models.DateField(verbose_name='收款日期')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=15, verbose_name='金额')),
|
||||
('status', models.IntegerField(choices=[(1, '审批中'), (2, '审批通过'), (3, '作废')], default=1, verbose_name='状态')),
|
||||
('remarks', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipt_orders', to='basic_info.customer', verbose_name='客户')),
|
||||
('merchant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipt_orders', to='basic_info.merchant', verbose_name='所属商户')),
|
||||
('operator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='receipt_orders', to='basic_info.employee', verbose_name='经办人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '收款单',
|
||||
'verbose_name_plural': '收款单',
|
||||
},
|
||||
bases=(business.models.OrderDirectionMixin, business.models.OrderCounterpartyMixin, models.Model),
|
||||
),
|
||||
]
|
||||
@@ -316,3 +316,107 @@ class SalesOrderItem(ModelBase):
|
||||
|
||||
def total_amount(self):
|
||||
return round(self.price * self.real_quantity(), 2)
|
||||
|
||||
|
||||
class PaymentOrderStatusEnum(models.IntegerChoices):
|
||||
PENDING = 1, '审批中'
|
||||
APPROVED = 2, '审批通过'
|
||||
CANCELLED = 3, '作废'
|
||||
|
||||
|
||||
class PaymentOrder(OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
merchant = models.ForeignKey(
|
||||
basic_info_models.Merchant,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='payment_orders',
|
||||
verbose_name='所属商户',
|
||||
)
|
||||
supplier = models.ForeignKey(
|
||||
basic_info_models.Supplier,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='payment_orders',
|
||||
verbose_name='供应商',
|
||||
)
|
||||
payment_date = models.DateField(verbose_name='付款日期')
|
||||
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='金额')
|
||||
operator = models.ForeignKey(
|
||||
basic_info_models.Employee,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='payment_orders',
|
||||
verbose_name='经办人',
|
||||
)
|
||||
status = models.IntegerField(
|
||||
choices=PaymentOrderStatusEnum.choices,
|
||||
default=PaymentOrderStatusEnum.PENDING,
|
||||
verbose_name='状态',
|
||||
)
|
||||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '付款单'
|
||||
verbose_name_plural = '付款单'
|
||||
|
||||
def __str__(self):
|
||||
return f'付款单 {self.id} - {self.supplier.name}'
|
||||
|
||||
def get_total_amount(self) -> Decimal:
|
||||
return self.amount
|
||||
|
||||
def get_direction(self) -> int:
|
||||
return -1
|
||||
|
||||
def get_counterparty_field_name(self) -> str:
|
||||
return 'supplier'
|
||||
|
||||
|
||||
class ReceiptOrderStatusEnum(models.IntegerChoices):
|
||||
PENDING = 1, '审批中'
|
||||
APPROVED = 2, '审批通过'
|
||||
CANCELLED = 3, '作废'
|
||||
|
||||
|
||||
class ReceiptOrder(OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
merchant = models.ForeignKey(
|
||||
basic_info_models.Merchant,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='receipt_orders',
|
||||
verbose_name='所属商户',
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
basic_info_models.Customer,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='receipt_orders',
|
||||
verbose_name='客户',
|
||||
)
|
||||
receipt_date = models.DateField(verbose_name='收款日期')
|
||||
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='金额')
|
||||
operator = models.ForeignKey(
|
||||
basic_info_models.Employee,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='receipt_orders',
|
||||
verbose_name='经办人',
|
||||
)
|
||||
status = models.IntegerField(
|
||||
choices=ReceiptOrderStatusEnum.choices,
|
||||
default=ReceiptOrderStatusEnum.PENDING,
|
||||
verbose_name='状态',
|
||||
)
|
||||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '收款单'
|
||||
verbose_name_plural = '收款单'
|
||||
|
||||
def __str__(self):
|
||||
return f'收款单 {self.id} - {self.customer.name}'
|
||||
|
||||
def get_total_amount(self) -> Decimal:
|
||||
return self.amount
|
||||
|
||||
def get_direction(self) -> int:
|
||||
return -1
|
||||
|
||||
def get_counterparty_field_name(self) -> str:
|
||||
return 'customer'
|
||||
|
||||
@@ -161,6 +161,62 @@ def create_sales_order(
|
||||
return sales_order
|
||||
|
||||
|
||||
def create_payment_order(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
supplier: basic_info_models.Supplier,
|
||||
payment_date,
|
||||
amount,
|
||||
operator: basic_info_models.Employee,
|
||||
remarks: str | None = '',
|
||||
) -> models.PaymentOrder:
|
||||
"""
|
||||
创建付款单(资金流出)。
|
||||
"""
|
||||
normalized_date = _normalize_order_date(payment_date)
|
||||
normalized_amount = _ensure_positive_amount(amount, 'amount')
|
||||
|
||||
with transaction.atomic():
|
||||
payment_order = models.PaymentOrder.objects.create(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
payment_date=normalized_date,
|
||||
amount=normalized_amount,
|
||||
operator=operator,
|
||||
remarks=remarks,
|
||||
)
|
||||
payment_order.refresh_from_db()
|
||||
return payment_order
|
||||
|
||||
|
||||
def create_receipt_order(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
customer: basic_info_models.Customer,
|
||||
receipt_date,
|
||||
amount,
|
||||
operator: basic_info_models.Employee,
|
||||
remarks: str | None = '',
|
||||
) -> models.ReceiptOrder:
|
||||
"""
|
||||
创建收款单(资金流入)。
|
||||
"""
|
||||
normalized_date = _normalize_order_date(receipt_date)
|
||||
normalized_amount = _ensure_positive_amount(amount, 'amount')
|
||||
|
||||
with transaction.atomic():
|
||||
receipt_order = models.ReceiptOrder.objects.create(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
receipt_date=normalized_date,
|
||||
amount=normalized_amount,
|
||||
operator=operator,
|
||||
remarks=remarks,
|
||||
)
|
||||
receipt_order.refresh_from_db()
|
||||
return receipt_order
|
||||
|
||||
|
||||
def review_purchase_order(
|
||||
*,
|
||||
purchase_order: models.PurchaseOrder | None = None,
|
||||
@@ -222,6 +278,58 @@ def review_sales_order(
|
||||
return _cancel_sales_order(order)
|
||||
|
||||
|
||||
def review_payment_order(
|
||||
*,
|
||||
payment_order: models.PaymentOrder | None = None,
|
||||
payment_order_id: int | None = None,
|
||||
target_status: models.PaymentOrderStatusEnum,
|
||||
reviewed_by=None,
|
||||
) -> models.PaymentOrder:
|
||||
order = _resolve_payment_order_instance(payment_order, payment_order_id)
|
||||
|
||||
if target_status not in {
|
||||
models.PaymentOrderStatusEnum.APPROVED,
|
||||
models.PaymentOrderStatusEnum.CANCELLED,
|
||||
}:
|
||||
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
|
||||
|
||||
if order.status == target_status:
|
||||
return order
|
||||
|
||||
with transaction.atomic():
|
||||
order.status = target_status
|
||||
order.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
order.refresh_from_db(fields=['status', 'updated_at'])
|
||||
return order
|
||||
|
||||
|
||||
def review_receipt_order(
|
||||
*,
|
||||
receipt_order: models.ReceiptOrder | None = None,
|
||||
receipt_order_id: int | None = None,
|
||||
target_status: models.ReceiptOrderStatusEnum,
|
||||
reviewed_by=None,
|
||||
) -> models.ReceiptOrder:
|
||||
order = _resolve_receipt_order_instance(receipt_order, receipt_order_id)
|
||||
|
||||
if target_status not in {
|
||||
models.ReceiptOrderStatusEnum.APPROVED,
|
||||
models.ReceiptOrderStatusEnum.CANCELLED,
|
||||
}:
|
||||
raise ValueError('target_status 只能是 APPROVED 或 CANCELLED')
|
||||
|
||||
if order.status == target_status:
|
||||
return order
|
||||
|
||||
with transaction.atomic():
|
||||
order.status = target_status
|
||||
order.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
order.refresh_from_db(fields=['status', 'updated_at'])
|
||||
return order
|
||||
|
||||
|
||||
def _normalize_order_items(
|
||||
*,
|
||||
merchant: basic_info_models.Merchant,
|
||||
@@ -596,6 +704,28 @@ def _resolve_sales_order_instance(
|
||||
)
|
||||
|
||||
|
||||
def _resolve_payment_order_instance(
|
||||
payment_order: models.PaymentOrder | None,
|
||||
payment_order_id: int | None,
|
||||
) -> models.PaymentOrder:
|
||||
if payment_order is None and payment_order_id is None:
|
||||
raise ValueError('必须提供 payment_order 或 payment_order_id')
|
||||
if payment_order is not None:
|
||||
payment_order_id = payment_order.id
|
||||
return models.PaymentOrder.objects.select_related('merchant', 'supplier').get(id=payment_order_id)
|
||||
|
||||
|
||||
def _resolve_receipt_order_instance(
|
||||
receipt_order: models.ReceiptOrder | None,
|
||||
receipt_order_id: int | None,
|
||||
) -> models.ReceiptOrder:
|
||||
if receipt_order is None and receipt_order_id is None:
|
||||
raise ValueError('必须提供 receipt_order 或 receipt_order_id')
|
||||
if receipt_order is not None:
|
||||
receipt_order_id = receipt_order.id
|
||||
return models.ReceiptOrder.objects.select_related('merchant', 'customer').get(id=receipt_order_id)
|
||||
|
||||
|
||||
def _to_decimal(value, field_name: str) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(value))
|
||||
@@ -603,6 +733,13 @@ def _to_decimal(value, field_name: str) -> Decimal:
|
||||
raise ValueError(f'{field_name} 必须是合法数值') from exc
|
||||
|
||||
|
||||
def _ensure_positive_amount(value, field_name: str) -> Decimal:
|
||||
amount = _to_decimal(value, field_name)
|
||||
if amount <= 0:
|
||||
raise ValueError(f'{field_name} 必须大于 0')
|
||||
return amount
|
||||
|
||||
|
||||
def _to_positive_int(value, field_name: str) -> int:
|
||||
if value is None:
|
||||
raise ValueError(f'{field_name} 不能为空')
|
||||
|
||||
@@ -374,6 +374,105 @@ class SalesOrderServiceTestCase(TestCase):
|
||||
item = sales_order.items.first()
|
||||
self.assertEqual(item.consume_detail_ids, '1,2')
|
||||
|
||||
def test_sales_order_outgoing_missing_consume_ids_raises(self):
|
||||
payload = [{
|
||||
'product_id': self.product.id,
|
||||
'quantity': 30,
|
||||
'price': '25',
|
||||
'unit': '米',
|
||||
}]
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=self.warehouse_strict_out,
|
||||
operator=self.operator,
|
||||
items=payload,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
|
||||
class PaymentReceiptServiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
(
|
||||
self.merchant,
|
||||
self.supplier,
|
||||
self.warehouse_strict,
|
||||
self.warehouse_relaxed,
|
||||
self.product,
|
||||
self.operator,
|
||||
) = create_basic_fixtures()
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户B',
|
||||
mobile='13888888888',
|
||||
created_by=None,
|
||||
)
|
||||
|
||||
def test_create_payment_order_and_approve(self):
|
||||
order = services.create_payment_order(
|
||||
merchant=self.merchant,
|
||||
supplier=self.supplier,
|
||||
payment_date=timezone.now().date(),
|
||||
amount='120.50',
|
||||
operator=self.operator,
|
||||
remarks='付款测试',
|
||||
)
|
||||
self.assertEqual(order.status, business_models.PaymentOrderStatusEnum.PENDING)
|
||||
reviewed = services.review_payment_order(
|
||||
payment_order=order,
|
||||
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
|
||||
reviewed_by=self.operator,
|
||||
)
|
||||
self.assertEqual(reviewed.status, business_models.PaymentOrderStatusEnum.APPROVED)
|
||||
|
||||
def test_create_receipt_order_and_cancel(self):
|
||||
order = services.create_receipt_order(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
receipt_date=timezone.now().date(),
|
||||
amount='80.00',
|
||||
operator=self.operator,
|
||||
remarks='收款测试',
|
||||
)
|
||||
cancelled = services.review_receipt_order(
|
||||
receipt_order=order,
|
||||
target_status=business_models.ReceiptOrderStatusEnum.CANCELLED,
|
||||
reviewed_by=self.operator,
|
||||
)
|
||||
self.assertEqual(cancelled.status, business_models.ReceiptOrderStatusEnum.CANCELLED)
|
||||
|
||||
def test_payment_amount_must_be_positive(self):
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_payment_order(
|
||||
merchant=self.merchant,
|
||||
supplier=self.supplier,
|
||||
payment_date=timezone.now().date(),
|
||||
amount='-1',
|
||||
operator=self.operator,
|
||||
)
|
||||
|
||||
def test_receipt_approval_is_idempotent(self):
|
||||
order = services.create_receipt_order(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
receipt_date=timezone.now().date(),
|
||||
amount='10',
|
||||
operator=self.operator,
|
||||
)
|
||||
services.review_receipt_order(
|
||||
receipt_order=order,
|
||||
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
|
||||
reviewed_by=self.operator,
|
||||
)
|
||||
reviewed_again = services.review_receipt_order(
|
||||
receipt_order=order,
|
||||
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
|
||||
reviewed_by=self.operator,
|
||||
)
|
||||
self.assertEqual(reviewed_again.status, business_models.ReceiptOrderStatusEnum.APPROVED)
|
||||
|
||||
|
||||
class PurchaseOrderStockServiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
@@ -463,6 +562,36 @@ class SalesOrderStockServiceTestCase(TestCase):
|
||||
self.assertEqual(payload['sales_order_id'], self.sales_order.id)
|
||||
self.assertEqual(payload['stock_change_record_id'], 654)
|
||||
|
||||
def test_service_calls_stock_out_strict_mode(self):
|
||||
strict_order = business_models.SalesOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
sales_date=timezone.now().date(),
|
||||
operator=self.operator,
|
||||
warehouse=self.warehouse_strict_out,
|
||||
)
|
||||
consume_items = [{'product_id': self.product.id, 'consume_detail_ids': [11, 12], 'quantity': 50}]
|
||||
with patch('business.services.StockFlowService') as mock_flow_cls:
|
||||
mock_instance = mock_flow_cls.return_value
|
||||
mock_instance.stock_out.return_value = (MagicMock(id=987), [], 2)
|
||||
|
||||
payload = services.create_sales_order_stock_entries_sync(
|
||||
sales_order_id=strict_order.id,
|
||||
warehouse_id=self.warehouse_strict_out.id,
|
||||
items=consume_items,
|
||||
created_by_id=self.user.id,
|
||||
)
|
||||
|
||||
mock_flow_cls.assert_called_once_with(merchant=self.merchant, created_by=self.user)
|
||||
mock_instance.stock_out.assert_called_once_with(
|
||||
warehouse_id=self.warehouse_strict_out.id,
|
||||
source_type=stock_models.StockChangeSourceEnum.SALES,
|
||||
source_id=strict_order.id,
|
||||
items=[{'product_id': self.product.id, 'consume_detail_ids': [11, 12], 'quantity': 50}],
|
||||
)
|
||||
self.assertEqual(payload['sales_order_id'], strict_order.id)
|
||||
self.assertEqual(payload['stock_change_record_id'], 987)
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_TASK_ALWAYS_EAGER=True,
|
||||
|
||||
Reference in New Issue
Block a user