forked from erp-dev/erp
feat: red flush
This commit is contained in:
438
business/tests/test_red_flush_services.py
Normal file
438
business/tests/test_red_flush_services.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user