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, )