forked from erp-dev/erp
feat: red flush and cost
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
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
|
||||
|
||||
@@ -163,3 +166,342 @@ class BusinessStatementServiceTestCase(TestCase):
|
||||
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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user