1
0
forked from erp-dev/erp
Files
erpnew/business/tests/test_statement_services.py

520 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
)
external_sale_order = 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': '外部销售'}],
)
external_return_order = 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['human_id'], sales_order.human_id)
self.assertEqual(sales_record['items'][0]['quantity'], Decimal('10'))
self.assertEqual(sales_record['items'][0]['quantity_of_rolls'], [6, 4])
return_record = next(record for record in payload['records'] if record['source_type'] == 'sales_return_order')
self.assertEqual(return_record['human_id'], return_order.human_id)
receipt_record = next(record for record in payload['records'] if record['source_type'] == 'receipt_order')
self.assertEqual(receipt_record['human_id'], receipt_order.human_id)
self.assertEqual(receipt_record['positive_amount'], Decimal('-2.00'))
self.assertEqual(receipt_record['negative_amount'], Decimal('20.00'))
external_sale_record = next(record for record in payload['records'] if record['source_type'] == 'external_sales_order')
self.assertEqual(external_sale_record['human_id'], str(external_sale_order.id))
external_return_record = next(record for record in payload['records'] if record['source_type'] == 'external_sales_return_order')
self.assertEqual(external_return_record['human_id'], str(external_return_order.id))
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['human_id'], purchase_order.human_id)
self.assertEqual(purchase_record['items'][0]['quantity'], Decimal('10'))
self.assertEqual(purchase_record['items'][0]['num_of_rolls'], 2)
return_record = next(record for record in payload['records'] if record['source_type'] == 'purchase_return_order')
self.assertEqual(return_record['human_id'], return_order.human_id)
payment_record = next(record for record in payload['records'] if record['source_type'] == 'payment_order')
self.assertEqual(payment_record['human_id'], payment_order.human_id)
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')