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,
|
||||
)
|
||||
343
business/tests/test_service_edge_cases.py
Normal file
343
business/tests/test_service_edge_cases.py
Normal file
@@ -0,0 +1,343 @@
|
||||
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 .fixtures import create_basic_fixtures, create_sales_fixtures
|
||||
|
||||
|
||||
class BusinessServiceEdgeCaseTestCase(TestCase):
|
||||
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 test_sales_order_kind_and_date_validation_errors(self):
|
||||
merchant, customer, _, warehouse, _, product, operator = create_sales_fixtures()
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
kind='invalid',
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
kind=999,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
order_date='not-a-date',
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
order_date=object(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
|
||||
def test_return_order_source_validation_errors(self):
|
||||
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
|
||||
other_supplier = basic_models.Supplier.objects.create(merchant=merchant, name='其他供应商')
|
||||
purchase_order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_purchase_return_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
purchase_order_id=999999,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_purchase_return_order(
|
||||
merchant=merchant,
|
||||
supplier=other_supplier,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
purchase_order=purchase_order,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
|
||||
sales_merchant, customer, _, sales_warehouse, _, sales_product, sales_operator = create_sales_fixtures()
|
||||
other_customer = basic_models.Customer.objects.create(merchant=sales_merchant, name='其他客户')
|
||||
sales_order = services.create_sales_order(
|
||||
merchant=sales_merchant,
|
||||
customer=customer,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=sales_warehouse,
|
||||
operator=sales_operator,
|
||||
items=[{'product_id': sales_product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_return_order(
|
||||
merchant=sales_merchant,
|
||||
customer=customer,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=sales_warehouse,
|
||||
operator=sales_operator,
|
||||
sales_order_id=999999,
|
||||
items=[{'product_id': sales_product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_return_order(
|
||||
merchant=sales_merchant,
|
||||
customer=other_customer,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=sales_warehouse,
|
||||
operator=sales_operator,
|
||||
sales_order=sales_order,
|
||||
items=[{'product_id': sales_product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
|
||||
def test_empty_items_and_update_source_validation_errors(self):
|
||||
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
|
||||
purchase_order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.update_purchase_order(purchase_order=purchase_order, items=[])
|
||||
updated = services.update_purchase_order(
|
||||
purchase_order=purchase_order,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
from_pre_purchase_order_id=123,
|
||||
)
|
||||
self.assertEqual(updated.from_pre_purchase_order_id, 123)
|
||||
sales_merchant, customer, _, sales_warehouse, _, sales_product, sales_operator = create_sales_fixtures()
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_order(
|
||||
merchant=sales_merchant,
|
||||
customer=customer,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=sales_warehouse,
|
||||
operator=sales_operator,
|
||||
items=[],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_purchase_return_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[],
|
||||
)
|
||||
|
||||
return_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': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
return_order.purchase_order = None
|
||||
return_order.save(update_fields=['purchase_order'])
|
||||
with self.assertRaises(ValueError):
|
||||
services.update_purchase_return_order(
|
||||
purchase_return_order=return_order,
|
||||
purchase_order_id=999999,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
services.create_sales_return_order(
|
||||
merchant=sales_merchant,
|
||||
customer=customer,
|
||||
return_date=timezone.now().date(),
|
||||
warehouse=sales_warehouse,
|
||||
operator=sales_operator,
|
||||
items=[],
|
||||
)
|
||||
|
||||
def test_review_invalid_targets_and_cancelled_reapprove_errors(self):
|
||||
merchant, supplier, _, warehouse, product, operator = create_basic_fixtures()
|
||||
purchase_order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 1, 'num_of_rolls': 1, 'price': '1'}],
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.review_purchase_order(purchase_order=purchase_order, target_status=999)
|
||||
services.review_purchase_order(
|
||||
purchase_order=purchase_order,
|
||||
target_status=business_models.PurchaseOrderStatusEnum.CANCELLED,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.review_purchase_order(
|
||||
purchase_order=purchase_order,
|
||||
target_status=business_models.PurchaseOrderStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
payment = services.create_payment_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
payment_date=timezone.now().date(),
|
||||
amount='1',
|
||||
operator=operator,
|
||||
)
|
||||
services.review_payment_order(payment_order=payment, target_status=business_models.PaymentOrderStatusEnum.CANCELLED)
|
||||
with self.assertRaises(ValueError):
|
||||
services.review_payment_order(payment_order=payment, target_status=business_models.PaymentOrderStatusEnum.APPROVED)
|
||||
|
||||
def test_resolve_helpers_require_instance_or_id(self):
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_purchase_order_instance(None, None)
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_sales_order_instance(None, None)
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_purchase_return_order_instance(None, None)
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_sales_return_order_instance(None, None)
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_payment_order_instance(None, None)
|
||||
with self.assertRaises(ValueError):
|
||||
services._resolve_receipt_order_instance(None, None)
|
||||
|
||||
def test_stock_entry_sync_not_found_payloads(self):
|
||||
self.assertEqual(
|
||||
services.create_purchase_order_stock_entries_sync(
|
||||
purchase_order_id=999999,
|
||||
warehouse_id=1,
|
||||
items=[],
|
||||
)['error'],
|
||||
'purchase_order_not_found',
|
||||
)
|
||||
self.assertEqual(
|
||||
services.create_sales_order_stock_entries_sync(
|
||||
sales_order_id=999999,
|
||||
warehouse_id=1,
|
||||
items=[],
|
||||
)['error'],
|
||||
'sales_order_not_found',
|
||||
)
|
||||
self.assertEqual(
|
||||
services.create_purchase_return_order_stock_entries_sync(
|
||||
purchase_return_order_id=999999,
|
||||
warehouse_id=1,
|
||||
items=[],
|
||||
)['error'],
|
||||
'purchase_return_order_not_found',
|
||||
)
|
||||
self.assertEqual(
|
||||
services.create_sales_return_order_stock_entries_sync(
|
||||
sales_return_order_id=999999,
|
||||
warehouse_id=1,
|
||||
items=[],
|
||||
)['error'],
|
||||
'sales_return_order_not_found',
|
||||
)
|
||||
|
||||
def test_build_stock_flow_items_error_branches(self):
|
||||
merchant, supplier, warehouse_strict, _, product, operator = create_basic_fixtures()
|
||||
order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse_strict,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'numbers': [1], 'price': '1'}],
|
||||
)
|
||||
order.items.all().delete()
|
||||
with self.assertRaises(ValueError):
|
||||
services._build_stock_flow_items_from_order(order)
|
||||
|
||||
order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=timezone.now().date(),
|
||||
warehouse=warehouse_strict,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'numbers': [1], 'price': '1'}],
|
||||
)
|
||||
item = order.items.first()
|
||||
item.quantity_of_rolls = ''
|
||||
item.save(update_fields=['quantity_of_rolls'])
|
||||
with self.assertRaises(ValueError):
|
||||
services._build_stock_flow_items_from_order(order)
|
||||
|
||||
def test_bind_purchase_order_stock_change_record_error_branches(self):
|
||||
merchant, supplier, warehouse_strict, warehouse_relaxed, 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_strict,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'numbers': [1], 'price': '1'}],
|
||||
)
|
||||
|
||||
bound_record = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=warehouse_strict,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
source_id=123,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.bind_purchase_order_stock_change_record(
|
||||
purchase_order_id=order.id,
|
||||
stock_change_record_id=bound_record.id,
|
||||
)
|
||||
|
||||
wrong_warehouse_record = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=warehouse_relaxed,
|
||||
source_type=stock_models.StockChangeSourceEnum.PURCHASE,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.bind_purchase_order_stock_change_record(
|
||||
purchase_order_id=order.id,
|
||||
stock_change_record_id=wrong_warehouse_record.id,
|
||||
)
|
||||
|
||||
sales_source_record = stock_models.StockChangeRecord.objects.create(
|
||||
merchant=merchant,
|
||||
type=stock_models.StockChangeTypeEnum.ADD,
|
||||
warehouse=warehouse_strict,
|
||||
source_type=stock_models.StockChangeSourceEnum.SALES_RETURN,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
services.bind_purchase_order_stock_change_record(
|
||||
purchase_order_id=order.id,
|
||||
stock_change_record_id=sales_source_record.id,
|
||||
)
|
||||
165
business/tests/test_statement_services.py
Normal file
165
business/tests/test_statement_services.py
Normal file
@@ -0,0 +1,165 @@
|
||||
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 BusinessStatementServiceTestCase(TestCase):
|
||||
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 test_build_customer_statement_collects_all_customer_records(self):
|
||||
merchant, customer, warehouse_strict, warehouse_relaxed, _, product, operator = create_sales_fixtures()
|
||||
self._disable_auto_stock_tasks(merchant)
|
||||
today = timezone.now().date()
|
||||
|
||||
sales_order = 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=sales_order,
|
||||
target_status=business_models.SalesOrderStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
return_order = services.create_sales_return_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
return_date=today,
|
||||
warehouse=warehouse_relaxed,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 3, 'num_of_rolls': 1, 'price': '10'}],
|
||||
remarks='销退对账',
|
||||
)
|
||||
services.review_sales_return_order(
|
||||
sales_return_order=return_order,
|
||||
target_status=business_models.SalesReturnStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
receipt_order = services.create_receipt_order(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
receipt_date=today,
|
||||
amount='20',
|
||||
discount_amount='2',
|
||||
operator=operator,
|
||||
remarks='收款对账',
|
||||
)
|
||||
services.review_receipt_order(
|
||||
receipt_order=receipt_order,
|
||||
target_status=business_models.ReceiptOrderStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
business_models.ExternalCustomerStatementOrder.objects.create(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
category=business_models.ExternalCustomerStatementCategoryEnum.SALE,
|
||||
external_source_id='EXT-SALE-1',
|
||||
occurred_at=today,
|
||||
total_amount=Decimal('50'),
|
||||
sf_amount=Decimal('5'),
|
||||
zk_amount=Decimal('3'),
|
||||
items_payload=[{'product_name': '外部销售'}],
|
||||
)
|
||||
business_models.ExternalCustomerStatementOrder.objects.create(
|
||||
merchant=merchant,
|
||||
customer=customer,
|
||||
category=business_models.ExternalCustomerStatementCategoryEnum.SALE_RETURN,
|
||||
external_source_id='EXT-RETURN-1',
|
||||
occurred_at=today,
|
||||
total_amount=Decimal('7'),
|
||||
zk_amount=Decimal('1'),
|
||||
items_payload=[{'product_name': '外部退货'}],
|
||||
)
|
||||
|
||||
payload = services.build_customer_statement(merchant=merchant, customer=customer)
|
||||
|
||||
source_types = {record['source_type'] for record in payload['records']}
|
||||
self.assertEqual(
|
||||
source_types,
|
||||
{
|
||||
'sales_order',
|
||||
'sales_return_order',
|
||||
'receipt_order',
|
||||
'external_sales_order',
|
||||
'external_sales_return_order',
|
||||
},
|
||||
)
|
||||
sales_record = next(record for record in payload['records'] if record['source_type'] == 'sales_order')
|
||||
self.assertEqual(sales_record['items'][0]['quantity'], Decimal('10'))
|
||||
self.assertEqual(sales_record['items'][0]['quantity_of_rolls'], [6, 4])
|
||||
receipt_record = next(record for record in payload['records'] if record['source_type'] == 'receipt_order')
|
||||
self.assertEqual(receipt_record['positive_amount'], Decimal('-2.00'))
|
||||
self.assertEqual(receipt_record['negative_amount'], Decimal('20.00'))
|
||||
summary = services.build_statement_summary(payload)
|
||||
self.assertIn('positive_total', summary)
|
||||
self.assertIn('negative_total', summary)
|
||||
|
||||
def test_build_supplier_statement_collects_all_supplier_records(self):
|
||||
merchant, supplier, warehouse_strict, warehouse_relaxed, product, operator = create_basic_fixtures()
|
||||
self._disable_auto_stock_tasks(merchant)
|
||||
today = timezone.now().date()
|
||||
|
||||
purchase_order = services.create_purchase_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
order_date=today,
|
||||
warehouse=warehouse_strict,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'numbers': [8, 2], 'price': '9', 'unit': '米'}],
|
||||
remarks='采购对账',
|
||||
)
|
||||
services.review_purchase_order(
|
||||
purchase_order=purchase_order,
|
||||
target_status=business_models.PurchaseOrderStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
return_order = services.create_purchase_return_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
return_date=today,
|
||||
warehouse=warehouse_relaxed,
|
||||
operator=operator,
|
||||
items=[{'product_id': product.id, 'quantity': 4, 'num_of_rolls': 1, 'price': '9'}],
|
||||
remarks='采退对账',
|
||||
)
|
||||
services.review_purchase_return_order(
|
||||
purchase_return_order=return_order,
|
||||
target_status=business_models.PurchaseReturnStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
payment_order = services.create_payment_order(
|
||||
merchant=merchant,
|
||||
supplier=supplier,
|
||||
payment_date=today,
|
||||
amount='15',
|
||||
operator=operator,
|
||||
remarks='付款对账',
|
||||
)
|
||||
services.review_payment_order(
|
||||
payment_order=payment_order,
|
||||
target_status=business_models.PaymentOrderStatusEnum.APPROVED,
|
||||
)
|
||||
|
||||
payload = services.build_supplier_statement(merchant=merchant, supplier=supplier)
|
||||
|
||||
source_types = {record['source_type'] for record in payload['records']}
|
||||
self.assertEqual(source_types, {'purchase_order', 'purchase_return_order', 'payment_order'})
|
||||
purchase_record = next(record for record in payload['records'] if record['source_type'] == 'purchase_order')
|
||||
self.assertEqual(purchase_record['items'][0]['quantity'], Decimal('10'))
|
||||
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'))
|
||||
Reference in New Issue
Block a user