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