1
0
forked from erp-dev/erp

feat: red flush

This commit is contained in:
2026-06-12 13:57:21 +08:00
parent 80db7867f4
commit 0bcaa2db44
33 changed files with 2997 additions and 17 deletions

View File

@@ -0,0 +1,438 @@
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
class BusinessRedFlushServiceTestCase(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username='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 _assert_balance_red_flushed(self, *, source_type, source_id, red_flush_id):
records = business_models.BalanceChangeRecord.objects.filter(
source_type=source_type,
source_id=source_id,
).order_by('id')
self.assertEqual(records.count(), 2)
original = records[0]
reverse = records[1]
self.assertTrue(original.cancelled)
self.assertEqual(original.offset_id, reverse.id)
self.assertEqual(original.red_flush_id, red_flush_id)
self.assertEqual(reverse.offset_to, original.id)
self.assertEqual(reverse.red_flush_id, red_flush_id)
self.assertEqual(reverse.delta, -original.delta)
return original, reverse
def _assert_stock_red_flushed(self, *, source_type, source_id, red_flush_id):
original = stock_models.StockChangeRecord.objects.get(
source_type=source_type,
source_id=source_id,
)
reverse = stock_models.StockChangeRecord.objects.get(
source_type=stock_models.StockChangeSourceEnum.OFFSET,
source_id=original.id,
)
self.assertEqual(original.red_flush_id, red_flush_id)
self.assertEqual(reverse.red_flush_id, red_flush_id)
self.assertTrue(original.is_reversed)
self.assertTrue(reverse.is_offset)
return original, reverse
def test_red_flush_purchase_order_reverses_balance_and_stock(self):
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)
order = services.create_purchase_order(
merchant=merchant,
supplier=supplier,
order_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 40, 'num_of_rolls': 2, 'price': '10'}],
)
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'])
flushed = services.red_flush_purchase_order(
merchant=merchant,
purchase_order=order,
red_flushed_by=self.user,
reason='采购单测试红冲',
)
self.assertEqual(flushed.status, business_models.PurchaseOrderStatusEnum.APPROVED)
self.assertTrue(flushed.is_red_flushed)
self.assertIsNotNone(flushed.red_flush_id)
balance = business_models.SupplierBalance.objects.get(merchant=merchant, supplier=supplier)
self.assertEqual(balance.balance, Decimal('0.00'))
self._assert_balance_red_flushed(
source_type=business_models.BalanceChangeSourceEnum.PURCHASE_ORDER,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
self._assert_stock_red_flushed(
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
def test_red_flush_sales_order_reverses_balance_and_stock(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,
)
payload = services.create_sales_order_stock_entries_sync(
sales_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'])
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._assert_stock_red_flushed(
source_type=stock_models.StockChangeSourceEnum.SALES,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
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)
order = services.create_purchase_return_order(
merchant=merchant,
supplier=supplier,
return_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 12, 'num_of_rolls': 1, 'price': '8'}],
)
services.review_purchase_return_order(
purchase_return_order=order,
target_status=business_models.PurchaseReturnStatusEnum.APPROVED,
reviewed_by=self.user,
)
payload = services.create_purchase_return_order_stock_entries_sync(
purchase_return_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'])
flushed = services.red_flush_purchase_return_order(
merchant=merchant,
purchase_return_order=order,
red_flushed_by=self.user,
)
self.assertTrue(flushed.is_red_flushed)
balance = business_models.SupplierBalance.objects.get(merchant=merchant, supplier=supplier)
self.assertEqual(balance.balance, Decimal('0.00'))
self._assert_balance_red_flushed(
source_type=business_models.BalanceChangeSourceEnum.PURCHASE_RETURN_ORDER,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
self._assert_stock_red_flushed(
source_type=stock_models.StockChangeSourceEnum.PURCHASE_RETURN,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
def test_red_flush_sales_return_order_reverses_balance_and_stock(self):
merchant, customer, _, warehouse, _, product, operator = create_sales_fixtures()
self._disable_auto_stock_tasks(merchant)
order = services.create_sales_return_order(
merchant=merchant,
customer=customer,
return_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 12, 'num_of_rolls': 1, 'price': '8'}],
)
services.review_sales_return_order(
sales_return_order=order,
target_status=business_models.SalesReturnStatusEnum.APPROVED,
reviewed_by=self.user,
)
payload = services.create_sales_return_order_stock_entries_sync(
sales_return_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'])
flushed = services.red_flush_sales_return_order(
merchant=merchant,
sales_return_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_RETURN_ORDER,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
self._assert_stock_red_flushed(
source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
source_id=order.id,
red_flush_id=flushed.red_flush_id,
)
def test_red_flush_payment_order_reverses_balance_only(self):
merchant, supplier, _, _, _, operator = create_basic_fixtures()
order = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=timezone.now().date(),
amount='30',
operator=operator,
)
services.review_payment_order(
payment_order=order,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
flushed = services.red_flush_payment_order(merchant=merchant, payment_order=order, red_flushed_by=self.user)
self.assertTrue(flushed.is_red_flushed)
balance = business_models.SupplierBalance.objects.get(merchant=merchant, supplier=supplier)
self.assertEqual(balance.balance, Decimal('0.00'))
self._assert_balance_red_flushed(
source_type=business_models.BalanceChangeSourceEnum.PAYMENT_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_receipt_order_reverses_balance_only(self):
merchant, _, _, _, _, operator = create_basic_fixtures()
customer = basic_models.Customer.objects.create(merchant=merchant, name='红冲客户')
order = services.create_receipt_order(
merchant=merchant,
customer=customer,
receipt_date=timezone.now().date(),
amount='45',
operator=operator,
)
services.review_receipt_order(
receipt_order=order,
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
flushed = services.red_flush_receipt_order(merchant=merchant, receipt_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.RECEIPT_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_rejects_external_payment_order(self):
merchant, supplier, _, _, _, operator = create_basic_fixtures()
order = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=timezone.now().date(),
amount='30',
operator=operator,
)
order.is_external_source = True
order.external_source_id = 'XT-EXTERNAL-001'
order.save(update_fields=['is_external_source', 'external_source_id', 'updated_at'])
services.review_payment_order(
payment_order=order,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
with self.assertRaisesMessage(ValueError, '外部来源付款单不允许红冲'):
services.red_flush_payment_order(merchant=merchant, payment_order=order, red_flushed_by=self.user)
order.refresh_from_db()
self.assertFalse(order.is_red_flushed)
def test_red_flush_rejects_external_receipt_order(self):
merchant, _, _, _, _, operator = create_basic_fixtures()
customer = basic_models.Customer.objects.create(merchant=merchant, name='外部红冲客户')
order = services.create_receipt_order(
merchant=merchant,
customer=customer,
receipt_date=timezone.now().date(),
amount='45',
operator=operator,
)
order.external_source_id = 'SK-EXTERNAL-001'
order.save(update_fields=['external_source_id', 'updated_at'])
services.review_receipt_order(
receipt_order=order,
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
reviewed_by=self.user,
)
with self.assertRaisesMessage(ValueError, '外部来源收款单不允许红冲'):
services.red_flush_receipt_order(merchant=merchant, receipt_order=order, red_flushed_by=self.user)
order.refresh_from_db()
self.assertFalse(order.is_red_flushed)
def test_red_flush_rejects_non_approved_order(self):
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
order = services.create_purchase_order(
merchant=merchant,
supplier=supplier,
order_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 10, 'num_of_rolls': 1, 'price': '5'}],
)
with self.assertRaises(ValueError):
services.red_flush_purchase_order(merchant=merchant, purchase_order=order, red_flushed_by=self.user)
def test_red_flush_rejects_repeated_order(self):
merchant, supplier, _, _, _, operator = create_basic_fixtures()
order = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=timezone.now().date(),
amount='10',
operator=operator,
)
services.review_payment_order(
payment_order=order,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
)
services.red_flush_payment_order(merchant=merchant, payment_order=order, red_flushed_by=self.user)
with self.assertRaises(ValueError):
services.red_flush_payment_order(merchant=merchant, payment_order=order, red_flushed_by=self.user)
def test_red_flush_rejects_missing_balance_record(self):
merchant, supplier, _, _, _, operator = create_basic_fixtures()
order = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=timezone.now().date(),
amount='10',
operator=operator,
)
order.status = business_models.PaymentOrderStatusEnum.APPROVED
order.save(update_fields=['status'])
with self.assertRaises(ValueError):
services.red_flush_payment_order(merchant=merchant, payment_order=order, red_flushed_by=self.user)
def test_red_flush_rolls_back_balance_when_stock_missing(self):
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
self._disable_auto_stock_tasks(merchant)
order = services.create_purchase_order(
merchant=merchant,
supplier=supplier,
order_date=timezone.now().date(),
warehouse=warehouse,
operator=operator,
items=[{'product_id': product.id, 'quantity': 10, 'num_of_rolls': 1, 'price': '5'}],
)
services.review_purchase_order(
purchase_order=order,
target_status=business_models.PurchaseOrderStatusEnum.APPROVED,
)
with self.assertRaises(ValueError):
services.red_flush_purchase_order(merchant=merchant, purchase_order=order, red_flushed_by=self.user)
order.refresh_from_db()
self.assertFalse(order.is_red_flushed)
balance_records = business_models.BalanceChangeRecord.objects.filter(
source_type=business_models.BalanceChangeSourceEnum.PURCHASE_ORDER,
source_id=order.id,
)
self.assertEqual(balance_records.count(), 1)
self.assertFalse(balance_records.first().cancelled)
def test_red_flush_rejects_cross_merchant_order(self):
merchant, supplier, _, _, _, operator = create_basic_fixtures()
other_merchant = basic_models.Merchant.objects.create(
name='其他商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
order = services.create_payment_order(
merchant=merchant,
supplier=supplier,
payment_date=timezone.now().date(),
amount='10',
operator=operator,
)
services.review_payment_order(
payment_order=order,
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
)
with self.assertRaises(ValueError):
services.red_flush_payment_order(
merchant=other_merchant,
payment_order_id=order.id,
red_flushed_by=self.user,
)