1
0
forked from erp-dev/erp

feat: red flush and cost

This commit is contained in:
2026-06-22 22:27:28 +08:00
parent 9a1c92febf
commit 70b3a0d246
25 changed files with 1375 additions and 20 deletions

View File

@@ -0,0 +1,31 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('business', '0031_red_flush_fields'),
]
operations = [
migrations.AddField(
model_name='purchaseorder',
name='balance_before_snapshot',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True, verbose_name='创建前供应商余额快照'),
),
migrations.AddField(
model_name='salesorder',
name='balance_before_snapshot',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True, verbose_name='创建前客户余额快照'),
),
migrations.AddField(
model_name='purchasereturnorder',
name='balance_before_snapshot',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True, verbose_name='创建前供应商余额快照'),
),
migrations.AddField(
model_name='salesreturnorder',
name='balance_before_snapshot',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True, verbose_name='创建前客户余额快照'),
),
]

View File

@@ -133,6 +133,10 @@ class PurchaseOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounte
is_red_flushed = models.BooleanField(default=False, db_index=True, verbose_name='已红冲')
red_flush_id = models.UUIDField(null=True, blank=True, db_index=True, verbose_name='红冲批次ID')
red_flushed_at = models.DateTimeField(null=True, blank=True, verbose_name='红冲时间')
balance_before_snapshot = models.DecimalField(
max_digits=15, decimal_places=2, null=True, blank=True,
verbose_name='创建前供应商余额快照',
)
from_pre_purchase_order_id = models.BigIntegerField(
null=True,
blank=True,
@@ -283,6 +287,10 @@ class SalesOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpa
is_red_flushed = models.BooleanField(default=False, db_index=True, verbose_name='已红冲')
red_flush_id = models.UUIDField(null=True, blank=True, db_index=True, verbose_name='红冲批次ID')
red_flushed_at = models.DateTimeField(null=True, blank=True, verbose_name='红冲时间')
balance_before_snapshot = models.DecimalField(
max_digits=15, decimal_places=2, null=True, blank=True,
verbose_name='创建前客户余额快照',
)
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
from_pre_sales_order_id = models.BigIntegerField(
blank=True,
@@ -795,6 +803,10 @@ class PurchaseReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, Order
is_red_flushed = models.BooleanField(default=False, db_index=True, verbose_name='已红冲')
red_flush_id = models.UUIDField(null=True, blank=True, db_index=True, verbose_name='红冲批次ID')
red_flushed_at = models.DateTimeField(null=True, blank=True, verbose_name='红冲时间')
balance_before_snapshot = models.DecimalField(
max_digits=15, decimal_places=2, null=True, blank=True,
verbose_name='创建前供应商余额快照',
)
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
class Meta:
@@ -923,6 +935,10 @@ class SalesReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCou
is_red_flushed = models.BooleanField(default=False, db_index=True, verbose_name='已红冲')
red_flush_id = models.UUIDField(null=True, blank=True, db_index=True, verbose_name='红冲批次ID')
red_flushed_at = models.DateTimeField(null=True, blank=True, verbose_name='红冲时间')
balance_before_snapshot = models.DecimalField(
max_digits=15, decimal_places=2, null=True, blank=True,
verbose_name='创建前客户余额快照',
)
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
class Meta:

View File

@@ -288,6 +288,9 @@ def create_purchase_order(
purchase_order = models.PurchaseOrder.objects.create(
merchant=merchant,
supplier=supplier,
balance_before_snapshot=BalanceService.get_supplier_balance(
merchant=merchant, supplier=supplier,
),
purchase_date=normalized_date,
operator=operator,
warehouse=warehouse,
@@ -425,6 +428,9 @@ def create_sales_order(
sales_order = models.SalesOrder.objects.create(
merchant=merchant,
customer=customer,
balance_before_snapshot=BalanceService.get_customer_balance(
merchant=merchant, customer=customer,
),
sales_date=normalized_date,
operator=operator,
warehouse=warehouse,
@@ -577,6 +583,9 @@ def create_purchase_return_order(
return_order = models.PurchaseReturnOrder.objects.create(
merchant=merchant,
supplier=supplier,
balance_before_snapshot=BalanceService.get_supplier_balance(
merchant=merchant, supplier=supplier,
),
purchase_order=resolved_purchase_order,
return_date=normalized_date,
operator=operator,
@@ -727,6 +736,9 @@ def create_sales_return_order(
return_order = models.SalesReturnOrder.objects.create(
merchant=merchant,
customer=customer,
balance_before_snapshot=BalanceService.get_customer_balance(
merchant=merchant, customer=customer,
),
sales_order=resolved_sales_order,
return_date=normalized_date,
operator=operator,
@@ -1164,6 +1176,7 @@ def red_flush_sales_order(
approved_status=models.SalesOrderStatusEnum.APPROVED,
balance_source_type=models.BalanceChangeSourceEnum.SALES_ORDER,
stock_source_type=stock_models.StockChangeSourceEnum.SALES,
require_stock_records=False,
counterparty_field='customer',
error_label='销售单',
red_flushed_by=red_flushed_by,
@@ -1279,6 +1292,7 @@ def _red_flush_order_impl(
stock_source_type: stock_models.StockChangeSourceEnum | None,
counterparty_field: str,
error_label: str,
require_stock_records: bool = True,
red_flushed_by=None,
reason: str | None = '',
):
@@ -1328,6 +1342,7 @@ def _red_flush_order_impl(
red_flushed_by=red_flushed_by,
reason=reason_text,
error_label=error_label,
require_stock_records=require_stock_records,
)
locked_order.is_red_flushed = True
@@ -1412,6 +1427,7 @@ def _red_flush_stock_records_for_order(
red_flushed_by,
reason: str,
error_label: str,
require_stock_records: bool = True,
) -> None:
stock_records = list(
stock_models.StockChangeRecord.objects.select_for_update().filter(
@@ -1421,7 +1437,9 @@ def _red_flush_stock_records_for_order(
).order_by('id')
)
if not stock_records:
raise ValueError(f'{error_label}缺少可红冲的库存记录')
if require_stock_records:
raise ValueError(f'{error_label}缺少可红冲的库存记录')
return
stock_service = StockFlowService(merchant=order.merchant, created_by=red_flushed_by)
for stock_record in stock_records:
@@ -2532,6 +2550,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
merchant=self.merchant,
customer=customer,
status=models.SalesOrderStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('customer', 'warehouse')
.prefetch_related('items__product')
@@ -2565,6 +2584,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
merchant=self.merchant,
customer=customer,
status=models.SalesReturnStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('customer', 'warehouse')
.prefetch_related('items__product')
@@ -2598,6 +2618,7 @@ class _CustomerStatementBuilder(_StatementBuilder):
merchant=self.merchant,
customer=customer,
status=models.ReceiptOrderStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('customer')
)
@@ -2685,6 +2706,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
merchant=self.merchant,
supplier=supplier,
status=models.PurchaseOrderStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('supplier', 'warehouse')
.prefetch_related('items__product')
@@ -2718,6 +2740,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
merchant=self.merchant,
supplier=supplier,
status=models.PurchaseReturnStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('supplier', 'warehouse')
.prefetch_related('items__product')
@@ -2751,6 +2774,7 @@ class _SupplierStatementBuilder(_StatementBuilder):
merchant=self.merchant,
supplier=supplier,
status=models.PaymentOrderStatusEnum.APPROVED,
is_red_flushed=False,
)
.select_related('supplier')
)

View File

@@ -0,0 +1,225 @@
from decimal import Decimal
from django.test import TestCase
from django.utils import timezone
from basic_info import models as basic_models
from business import models as business_models, services
from .fixtures import create_basic_fixtures, create_sales_fixtures
class OrderBalanceBeforeSnapshotTestCase(TestCase):
def setUp(self):
(
self.supplier_merchant,
self.supplier,
_supplier_strict_warehouse,
self.supplier_warehouse,
self.supplier_product,
self.supplier_operator,
) = create_basic_fixtures()
(
self.customer_merchant,
self.customer,
_customer_strict_warehouse,
self.customer_warehouse,
_customer_strict_out_warehouse,
self.customer_product,
self.customer_operator,
) = create_sales_fixtures()
def _supplier_items(self):
return [
{
'product_id': self.supplier_product.id,
'quantity': '3',
'num_of_rolls': 1,
'price': '12.50',
}
]
def _customer_items(self):
return [
{
'product_id': self.customer_product.id,
'quantity': '4',
'num_of_rolls': 1,
'price': '8.25',
}
]
def _create_supplier_orders(self):
purchase_order = services.create_purchase_order(
merchant=self.supplier_merchant,
supplier=self.supplier,
order_date=timezone.now().date(),
warehouse=self.supplier_warehouse,
operator=self.supplier_operator,
items=self._supplier_items(),
)
purchase_return_order = services.create_purchase_return_order(
merchant=self.supplier_merchant,
supplier=self.supplier,
return_date=timezone.now().date(),
warehouse=self.supplier_warehouse,
operator=self.supplier_operator,
items=self._supplier_items(),
purchase_order=purchase_order,
)
return purchase_order, purchase_return_order
def _create_customer_orders(self):
sales_order = services.create_sales_order(
merchant=self.customer_merchant,
customer=self.customer,
order_date=timezone.now().date(),
warehouse=self.customer_warehouse,
operator=self.customer_operator,
items=self._customer_items(),
)
sales_return_order = services.create_sales_return_order(
merchant=self.customer_merchant,
customer=self.customer,
return_date=timezone.now().date(),
warehouse=self.customer_warehouse,
operator=self.customer_operator,
items=self._customer_items(),
sales_order=sales_order,
)
return sales_order, sales_return_order
def test_all_four_creation_services_capture_current_balance(self):
business_models.SupplierBalance.objects.create(
merchant=self.supplier_merchant,
supplier=self.supplier,
balance=Decimal('123.45'),
)
business_models.CustomerBalance.objects.create(
merchant=self.customer_merchant,
customer=self.customer,
balance=Decimal('-67.89'),
)
purchase_order, purchase_return_order = self._create_supplier_orders()
sales_order, sales_return_order = self._create_customer_orders()
self.assertEqual(purchase_order.balance_before_snapshot, Decimal('123.45'))
self.assertEqual(purchase_return_order.balance_before_snapshot, Decimal('123.45'))
self.assertEqual(sales_order.balance_before_snapshot, Decimal('-67.89'))
self.assertEqual(sales_return_order.balance_before_snapshot, Decimal('-67.89'))
def test_missing_balance_rows_snapshot_zero_instead_of_null(self):
purchase_order, purchase_return_order = self._create_supplier_orders()
sales_order, sales_return_order = self._create_customer_orders()
for order in (
purchase_order,
purchase_return_order,
sales_order,
sales_return_order,
):
self.assertEqual(order.balance_before_snapshot, Decimal('0'))
self.assertIsNotNone(order.balance_before_snapshot)
def test_updates_and_later_balance_changes_do_not_recalculate_snapshot(self):
supplier_balance = business_models.SupplierBalance.objects.create(
merchant=self.supplier_merchant,
supplier=self.supplier,
balance=Decimal('100.00'),
)
customer_balance = business_models.CustomerBalance.objects.create(
merchant=self.customer_merchant,
customer=self.customer,
balance=Decimal('200.00'),
)
purchase_order, purchase_return_order = self._create_supplier_orders()
sales_order, sales_return_order = self._create_customer_orders()
other_supplier = basic_models.Supplier.objects.create(
merchant=self.supplier_merchant,
name='快照测试供应商B',
)
other_customer = basic_models.Customer.objects.create(
merchant=self.customer_merchant,
name='快照测试客户B',
)
business_models.SupplierBalance.objects.create(
merchant=self.supplier_merchant,
supplier=other_supplier,
balance=Decimal('999.00'),
)
business_models.CustomerBalance.objects.create(
merchant=self.customer_merchant,
customer=other_customer,
balance=Decimal('888.00'),
)
supplier_balance.balance = Decimal('300.00')
supplier_balance.save(update_fields=['balance', 'updated_at'])
customer_balance.balance = Decimal('400.00')
customer_balance.save(update_fields=['balance', 'updated_at'])
services.update_purchase_order(
purchase_order=purchase_order,
supplier=other_supplier,
items=self._supplier_items(),
)
services.update_purchase_return_order(
purchase_return_order=purchase_return_order,
supplier=other_supplier,
items=self._supplier_items(),
)
services.update_sales_order(
sales_order=sales_order,
customer=other_customer,
items=self._customer_items(),
)
services.update_sales_return_order(
sales_return_order=sales_return_order,
customer=other_customer,
items=self._customer_items(),
)
for order in (purchase_order, purchase_return_order):
order.refresh_from_db()
self.assertEqual(order.balance_before_snapshot, Decimal('100.00'))
for order in (sales_order, sales_return_order):
order.refresh_from_db()
self.assertEqual(order.balance_before_snapshot, Decimal('200.00'))
def test_api_serializers_expose_snapshot_as_read_only(self):
business_models.SupplierBalance.objects.create(
merchant=self.supplier_merchant,
supplier=self.supplier,
balance=Decimal('123.45'),
)
business_models.CustomerBalance.objects.create(
merchant=self.customer_merchant,
customer=self.customer,
balance=Decimal('67.89'),
)
purchase_order, purchase_return_order = self._create_supplier_orders()
sales_order, sales_return_order = self._create_customer_orders()
from api_v1.views.business.purchase.views import PurchaseOrderSerializer
from api_v1.views.business.purchase_return.views import PurchaseReturnOrderSerializer
from api_v1.views.business.sales.views import SalesOrderSerializer
from api_v1.views.business.sales_return.views import SalesReturnOrderSerializer
cases = (
(PurchaseOrderSerializer, purchase_order, '123.45'),
(PurchaseReturnOrderSerializer, purchase_return_order, '123.45'),
(SalesOrderSerializer, sales_order, '67.89'),
(SalesReturnOrderSerializer, sales_return_order, '67.89'),
)
for serializer_class, order, expected in cases:
with self.subTest(serializer=serializer_class.__name__):
self.assertEqual(serializer_class(order).data['balance_before_snapshot'], expected)
serializer = serializer_class(
order,
data={'balance_before_snapshot': '9999.99'},
partial=True,
)
self.assertTrue(serializer.is_valid(), serializer.errors)
self.assertNotIn('balance_before_snapshot', serializer.validated_data)

View File

@@ -146,6 +146,37 @@ class BusinessRedFlushServiceTestCase(TestCase):
red_flush_id=flushed.red_flush_id,
)
def test_red_flush_sales_order_without_stock_reverses_balance_only(self):
merchant, customer, _, warehouse, _, product, operator = create_sales_fixtures()
self._disable_auto_stock_tasks(merchant)
order = services.create_sales_order(
merchant=merchant,
customer=customer,
order_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 20, 'num_of_rolls': 1, 'price': '15'}],
)
services.review_sales_order(
sales_order=order,
target_status=business_models.SalesOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
flushed = services.red_flush_sales_order(merchant=merchant, sales_order=order, red_flushed_by=self.user)
self.assertTrue(flushed.is_red_flushed)
balance = business_models.CustomerBalance.objects.get(merchant=merchant, customer=customer)
self.assertEqual(balance.balance, Decimal('0.00'))
self._assert_balance_red_flushed(
source_type=business_models.BalanceChangeSourceEnum.SALES_ORDER,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
self.assertFalse(
stock_models.StockChangeRecord.objects.filter(red_flush_id=flushed.red_flush_id).exists()
)
def test_red_flush_purchase_return_order_reverses_balance_and_stock(self):
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)

View File

@@ -1,10 +1,13 @@
from decimal import Decimal
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
from basic_info import models as basic_models
from business import models as business_models, services
from stock import models as stock_models
from stock import services as stock_services
from .fixtures import create_basic_fixtures, create_sales_fixtures
@@ -163,3 +166,342 @@ class BusinessStatementServiceTestCase(TestCase):
self.assertEqual(purchase_record['items'][0]['num_of_rolls'], 2)
payment_record = next(record for record in payload['records'] if record['source_type'] == 'payment_order')
self.assertEqual(payment_record['negative_amount'], Decimal('15.00'))
class BusinessStatementRedFlushExclusionTestCase(TestCase):
"""对账单 builders 必须排除已红冲的正式单据。
Bug 复核:红冲服务在保留单据 status=APPROVED 的同时把 is_red_flushed 置为 True。
若 _build_*_records 仅过滤 status已红冲单据会以原金额继续出现在对账单 records 中,
导致 summary 的 positive_total/negative_total 把红冲金额多算一次。
本测试类对客户侧 3 类与供应商侧 3 类单据各红冲一次后,断言:
- 对应 source_type 不在 records 列表中
- summary 的金额合计中没有该单金额
"""
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username='statement-red-flush', password='pass123')
def _disable_auto_stock_tasks(self, merchant):
basic_models.MerchantSetting.objects.filter(
merchant=merchant,
key=basic_models.MerchantSettingKeyEnum.AUTO_CREATE_STOCK_CHANGE_TASKS,
).update(val_bool=False)
def _complete_stock_record(self, record_id):
record = stock_models.StockChangeRecord.objects.get(id=record_id)
stock_services.make_stock_change_completed(record)
return record
def _create_and_complete_purchase_order(self, *, merchant, supplier, warehouse, operator, product, quantity, price):
order = services.create_purchase_order(
merchant=merchant,
supplier=supplier,
order_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': quantity, 'num_of_rolls': 1, 'price': str(price)}],
remarks='采购对账',
)
services.review_purchase_order(
purchase_order=order,
target_status=business_models.PurchaseOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
payload = services.create_purchase_order_stock_entries_sync(
purchase_order_id=order.id,
warehouse_id=warehouse.id,
items=services._build_stock_flow_items_from_order(order),
created_by_id=self.user.id,
)
self._complete_stock_record(payload['stock_change_record_id'])
return order
# ============ 客户侧 ============
def test_red_flushed_sales_order_excluded_from_customer_statement(self):
merchant, customer, warehouse_strict, warehouse_relaxed, _, product, operator = create_sales_fixtures()
self._disable_auto_stock_tasks(merchant)
today = timezone.now().date()
# 一个保留的销售单 + 一个会被红冲的销售单
keep = services.create_sales_order(
merchant=merchant,
customer=customer,
order_date=today,
warehouse=warehouse_strict,
operator=operator,
items=[{'product_id': product.id, 'numbers': [6, 4], 'price': '10', 'unit': ''}],
remarks='保留销售单',
)
services.review_sales_order(
sales_order=keep,
target_status=business_models.SalesOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
to_flush = services.create_sales_order(
merchant=merchant,
customer=customer,
order_date=today,
warehouse=warehouse_strict,
operator=operator,
items=[{'product_id': product.id, 'numbers': [5, 5], 'price': '20', 'unit': ''}],
remarks='红冲销售单',
)
services.review_sales_order(
sales_order=to_flush,
target_status=business_models.SalesOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
services.red_flush_sales_order(
merchant=merchant,
sales_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲销售单',
)
payload = services.build_customer_statement(merchant=merchant, customer=customer)
sales_records = [r for r in payload['records'] if r['source_type'] == 'sales_order']
self.assertEqual(len(sales_records), 1)
self.assertEqual(sales_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留单金额 = 10 * 10 = 100若红冲单未排除会再多 200 = 300
self.assertEqual(summary['positive_total'], '100.00')
def test_red_flushed_sales_return_order_excluded_from_customer_statement(self):
merchant, customer, warehouse_strict, warehouse_relaxed, _, product, operator = create_sales_fixtures()
self._disable_auto_stock_tasks(merchant)
today = timezone.now().date()
def _make_sales_return(quantity, price, remark):
ret = services.create_sales_return_order(
merchant=merchant,
customer=customer,
return_date=today,
warehouse=warehouse_relaxed,
operator=operator,
items=[{'product_id': product.id, 'quantity': quantity, 'num_of_rolls': 1, 'price': str(price)}],
remarks=remark,
)
services.review_sales_return_order(
sales_return_order=ret,
target_status=business_models.SalesReturnStatusEnum.APPROVED,
reviewed_by=self.user,
)
sync_payload = services.create_sales_return_order_stock_entries_sync(
sales_return_order_id=ret.id,
warehouse_id=warehouse_relaxed.id,
items=services._build_stock_flow_items_from_order(ret),
created_by_id=self.user.id,
)
self._complete_stock_record(sync_payload['stock_change_record_id'])
return ret
keep = _make_sales_return(3, Decimal('10'), '保留销退')
to_flush = _make_sales_return(2, Decimal('20'), '红冲销退')
services.red_flush_sales_return_order(
merchant=merchant,
sales_return_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲销退单',
)
payload = services.build_customer_statement(merchant=merchant, customer=customer)
sr_records = [r for r in payload['records'] if r['source_type'] == 'sales_return_order']
self.assertEqual(len(sr_records), 1)
self.assertEqual(sr_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留销退 negative=3*10=30红冲若未排除会再多 2*20=40合计 70
self.assertEqual(summary['negative_total'], '30.00')
def test_red_flushed_receipt_order_excluded_from_customer_statement(self):
merchant, customer, _wh1, _wh2, _, _, operator = create_sales_fixtures()
self._disable_auto_stock_tasks(merchant)
today = timezone.now().date()
keep = services.create_receipt_order(
merchant=merchant,
customer=customer,
receipt_date=today,
amount='40',
operator=operator,
remarks='保留收款',
)
services.review_receipt_order(
receipt_order=keep,
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
to_flush = services.create_receipt_order(
merchant=merchant,
customer=customer,
receipt_date=today,
amount='25',
operator=operator,
remarks='红冲收款',
)
services.review_receipt_order(
receipt_order=to_flush,
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
services.red_flush_receipt_order(
merchant=merchant,
receipt_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲收款单',
)
payload = services.build_customer_statement(merchant=merchant, customer=customer)
receipt_records = [r for r in payload['records'] if r['source_type'] == 'receipt_order']
self.assertEqual(len(receipt_records), 1)
self.assertEqual(receipt_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留收款 negative=40红冲未排除会再 +25 = 65
self.assertEqual(summary['negative_total'], '40.00')
# ============ 供应商侧 ============
def test_red_flushed_purchase_order_excluded_from_supplier_statement(self):
merchant, supplier, warehouse_strict, warehouse_relaxed, product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)
keep = self._create_and_complete_purchase_order(
merchant=merchant, supplier=supplier, warehouse=warehouse_relaxed,
operator=operator, product=product, quantity=10, price=Decimal('5'),
)
to_flush = self._create_and_complete_purchase_order(
merchant=merchant, supplier=supplier, warehouse=warehouse_relaxed,
operator=operator, product=product, quantity=8, price=Decimal('7'),
)
services.red_flush_purchase_order(
merchant=merchant,
purchase_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲采购单',
)
payload = services.build_supplier_statement(merchant=merchant, supplier=supplier)
purchase_records = [r for r in payload['records'] if r['source_type'] == 'purchase_order']
self.assertEqual(len(purchase_records), 1)
self.assertEqual(purchase_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留单 10*5=50若红冲单未排除会再多 8*7=56合计 106
self.assertEqual(summary['positive_total'], '50.00')
def test_red_flushed_purchase_return_order_excluded_from_supplier_statement(self):
merchant, supplier, warehouse_strict, warehouse_relaxed, product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)
today = timezone.now().date()
# 先建一个采购单提供库存底盘,然后做两个退货单(一个保留 + 一个红冲)
purchase = self._create_and_complete_purchase_order(
merchant=merchant, supplier=supplier, warehouse=warehouse_relaxed,
operator=operator, product=product, quantity=50, price=Decimal('5'),
)
def _make_return(quantity, price, remark):
ret = services.create_purchase_return_order(
merchant=merchant,
supplier=supplier,
return_date=today,
warehouse=warehouse_relaxed,
operator=operator,
items=[{'product_id': product.id, 'quantity': quantity, 'num_of_rolls': 1, 'price': str(price)}],
remarks=remark,
purchase_order=purchase,
)
services.review_purchase_return_order(
purchase_return_order=ret,
target_status=business_models.PurchaseReturnStatusEnum.APPROVED,
reviewed_by=self.user,
)
sync_payload = services.create_purchase_return_order_stock_entries_sync(
purchase_return_order_id=ret.id,
warehouse_id=warehouse_relaxed.id,
items=services._build_stock_flow_items_from_order(ret),
created_by_id=self.user.id,
)
self._complete_stock_record(sync_payload['stock_change_record_id'])
return ret
keep = _make_return(3, Decimal('5'), '保留采退')
to_flush = _make_return(2, Decimal('7'), '红冲采退')
services.red_flush_purchase_return_order(
merchant=merchant,
purchase_return_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲采退单',
)
payload = services.build_supplier_statement(merchant=merchant, supplier=supplier)
pr_records = [r for r in payload['records'] if r['source_type'] == 'purchase_return_order']
self.assertEqual(len(pr_records), 1)
self.assertEqual(pr_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留采退 negative=3*5=15若红冲未排除会再多 2*7=14合计 29
self.assertEqual(summary['negative_total'], '15.00')
def test_red_flushed_payment_order_excluded_from_supplier_statement(self):
merchant, supplier, _wh1, _wh2, _product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)
today = timezone.now().date()
keep = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=today,
amount='30',
operator=operator,
remarks='保留付款',
)
services.review_payment_order(
payment_order=keep,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
to_flush = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=today,
amount='18',
operator=operator,
remarks='红冲付款',
)
services.review_payment_order(
payment_order=to_flush,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
services.red_flush_payment_order(
merchant=merchant,
payment_order=to_flush,
red_flushed_by=self.user,
reason='测试红冲付款单',
)
payload = services.build_supplier_statement(merchant=merchant, supplier=supplier)
payment_records = [r for r in payload['records'] if r['source_type'] == 'payment_order']
self.assertEqual(len(payment_records), 1)
self.assertEqual(payment_records[0]['source_id'], keep.id)
summary = services.build_statement_summary(payload)
# 保留付款 negative=30若红冲未排除会再多 18 合计 48
self.assertEqual(summary['negative_total'], '30.00')