django-oscar 2.0 如何定义店内现金支付方式
明确结论
在 django-oscar 2.0 中,支付方式不需要通过 payment/methods.py 里的 Base、CashOnPickup 等类注册。仅仅为了支持现金支付,也不需要继承 SourceType、Source 或 Transaction。
可以按下面的方式实现:
- 在结账流程中提供“到店现金支付”“货到付款”等选项,并将用户的选择保存在 checkout session 或提交表单中。
- 为不同的支付渠道创建对应的
SourceType数据。 - 下单时,在
PaymentDetailsView.handle_payment()中为订单添加Source。 - 此时尚未收到现金,因此不要将订单记为已付款,也不要伪造
pre-auth。 - 实际收款时调用
Source.debit(),生成一笔debit类型的Transaction,并记录相应的 payment event。
SourceType、Source 和 Transaction 是支付账务模型,不能代替支付方式的选择界面。支付选项如何展示、用户如何选择,以及哪些订单可以使用某种支付方式,仍由项目自己的 checkout 业务逻辑决定。
三个模型分别表示什么
SourceType
SourceType 表示资金来源的类别,例如:
Cash on pickupCash on deliveryCredit cardBank transfer
到店付款和货到付款最好分别创建两个 SourceType,不要统一使用一个名为 Cash 的类型,再通过 Source.reference 区分。
两者虽然都使用现金,但流程并不相同:
- 到店付款涉及门店和提货状态。
- 货到付款涉及配送、签收和配送员结算。
- 两者的取消、失败、对账及订单状态转换规则也可能不同。
SourceType 通常应通过 data migration 或初始化数据创建,不要在每次请求中调用 get_or_create()。这种写法可以用于示例或原型,但在生产环境中容易掩盖配置错误。
from django.db import migrations
def create_cash_source_types(apps, schema_editor):
SourceType = apps.get_model("payment", "SourceType")
SourceType.objects.get_or_create(name="Cash on pickup")
SourceType.objects.get_or_create(name="Cash on delivery")
class Migration(migrations.Migration):
dependencies = [
# 填写项目实际依赖
]
operations = [
migrations.RunPython(
create_cash_source_types,
migrations.RunPython.noop,
),
]
Source
Source 表示某张订单上的具体资金来源。例如:
- 订单 10001 的到店现金付款。
- 订单 10002 的货到付款。
- 某张订单中的一笔信用卡付款。
同一个 SourceType 可以对应许多订单上的 Source。
Source.reference 更适合保存这笔资金来源的业务引用,例如:
- 门店收据号
- POS 流水号
- 配送收款单号
- 第三方支付流水号
不要在每条记录的 reference 中填写固定字符串 CashOnPickup 或 CashOnDelivery。支付模式应由 SourceType 表示,reference 用来标识具体的收款记录。如果下单时还没有收据号,可以暂时留空,也可以保存一个确有业务意义的内部引用。
Transaction
Transaction 表示某个 Source 上发生的一次资金操作。它不描述订单中的哪些商品行被支付,而是记录资金来源层面的操作,例如:
Allocate:授权或预留金额。Debit:实际扣款或收款。Refund:退款。
因此,信用卡预授权即使没有立即完成资金结算,仍然可以记录为一笔 transaction。这里的 transaction 更接近“支付源上的账务操作”,不等于“现金已经转移”。
下单时如何处理现金付款
到店付款和货到付款都属于延期收款。创建订单时,客户只是承诺稍后付款,商家并未收到现金。因此,不应执行以下操作:
- 将
amount_debited设置为订单金额。 - 创建
debittransaction。 - 记录
paidpayment event。 - 将其记录为信用卡意义上的
pre-auth。 - 为了让账面余额看起来完整而伪造 allocation。
下单时可以创建一个各项金额均为零的 Source,用来说明这张订单计划使用哪种支付渠道:
from oscar.apps.checkout.views import (
PaymentDetailsView as OscarPaymentDetailsView,
)
from oscar.apps.payment.models import Source, SourceType
class PaymentDetailsView(OscarPaymentDetailsView):
def handle_payment(self, order_number, total, **kwargs):
payment_method = self.checkout_session.get_stripe_payment_method()
# 上面只是示意。实际项目应从自己的表单或 checkout session 读取。
if payment_method != "cash_on_pickup":
return
source_type = SourceType.objects.get(name="Cash on pickup")
source = Source(
source_type=source_type,
currency=total.currency,
amount_allocated=0,
amount_debited=0,
amount_refunded=0,
reference="",
)
self.add_payment_source(source)
这里使用的 checkout session 方法需要由项目自行实现。Oscar 提供了结账流程和支付记录的扩展点,但不会自动生成现金支付方式选择器。
如果订单创建后不需要立即在订单详情中显示计划使用的支付渠道,也可以暂时不创建 Source,等收到现金时再创建。不过,提前创建一个零金额 Source 往往更方便,因为后台能直接看到订单正在等待哪一种付款。
是否应该设置 amount_allocated
通常不应该。
amount_allocated 表示已经授权、保留或确认可用于该订单的金额。信用卡预授权是典型的 allocation,但客户选择“到店付现金”并不代表资金已经获得授权或被预留。
更符合账务含义的记录方式是:
下单时:
amount_allocated = 0
amount_debited = 0
amount_refunded = 0
实际收到现金后:
amount_debited = 实收金额
如果某个现有插件把待收现金写入 amount_allocated,那只是该插件采用的业务约定,不能视为 Oscar 对现金支付的统一要求。
订单的应收金额可以直接根据订单总额和已付款金额计算,不需要借用 allocation 表示。
实际收款时如何记录
顾客到店付款,或者配送员确认已经收到货款时,再对相应的 Source 执行 debit:
from decimal import Decimal
from django.db import transaction
from oscar.apps.payment.models import Source
@transaction.atomic
def record_cash_received(source_id, amount, receipt_number):
source = (
Source.objects
.select_for_update()
.select_related("order", "source_type")
.get(id=source_id)
)
amount = Decimal(amount)
if amount <= 0:
raise ValueError("Payment amount must be greater than zero")
source.debit(
amount,
reference=receipt_number,
status="Cash received",
)
return source
在 Oscar 的抽象支付模型中,Source.debit() 会更新 source 的 debit 汇总,并创建相应的 Transaction。应优先调用模型提供的方法,不要分别手工修改 amount_debited 和创建 Transaction,否则汇总金额与交易明细可能不一致。
实际项目还要处理以下情况:
- 重复点击或重复回调造成的重复收款。
- 部分付款。
- 一张订单由多个
Source共同支付。 - 实收金额超过订单未付金额。
- 订单已经取消或付清。
- 收据号、POS 流水号的唯一性。
- 操作人、门店、配送员和收款时间等审计信息。
Oscar 的标准 Transaction 未必包含这些业务字段。门店、收银员、配送员等信息通常应保存在项目自己的收款记录模型中,再通过外键关联 Source 或 Transaction。
Transaction 与 payment event 的区别
两者记录的内容不同。
Transaction 关联支付源,回答的是:
这个资金来源发生了什么账务操作?
payment event 关联订单和订单行,回答的是:
订单中的哪些商品、以多少金额发生了付款、退款或其他支付事件?
例如,顾客用现金支付整张订单时,可以记录为:
Source:
类型:Cash on pickup
Transaction:
类型:Debit
金额:100.00
引用:门店收据 POS-20260831-001
Payment event:
类型:Cash received
金额:100.00
关联订单行及对应数量
如果之后只退其中一件商品,则可以创建:
Transaction:
类型:Refund
金额:20.00
Payment event:
类型:Refunded
金额:20.00
关联被退商品行及数量
这就是“transactions have nothing to do with the lines of the order while payment events do”的含义。transaction 记录支付源上的金额操作,payment event 负责建立订单行级别的业务映射。
payment event 应在什么时候创建
下单时尚未收到现金,通常不要创建表示付款成功的 event。此时可以用订单状态表示:
Pending cash paymentAwaiting pickupAwaiting cash on delivery
收到现金后,再创建 Cash received 或 Paid 类型的 payment event,并把相关订单行的数量关联到该事件。
如果项目只支持整单付款,暂时只创建订单级 event 可能已经足够。如果需要部分发货、部分退款或按商品行对账,则应通过 Oscar 的订单事件处理逻辑记录相应的 line quantities。不同的 fork 或后台履约流程可能使用不同的事件处理服务,因此不应绕过现有 event handler,在各处直接操作 PaymentEventQuantity。
还要注意,self.add_payment_event() 是 checkout 阶段使用的辅助方法。订单创建以后,应通过项目的履约服务、事件处理器或订单 payment-event 关联来记录事件,不要再次调用 checkout view 的临时方法。
推荐的完整状态流程
到店现金付款
客户选择 Cash on pickup
↓
创建订单和零金额 Source
↓
订单状态变为 Awaiting pickup / Awaiting cash payment
↓
顾客到店,收银员确认收到现金
↓
Source.debit(amount, reference=receipt_number)
↓
创建 Cash received payment event
↓
订单状态变为 Paid / Collected
货到付款
客户选择 Cash on delivery
↓
创建订单和零金额 Source
↓
订单进入待配送状态
↓
配送员收款并确认
↓
Source.debit(amount, reference=collection_number)
↓
创建 Cash received payment event
↓
订单进入 Paid / Delivered 状态
“已送达”和“已收款”最好作为两个独立事件处理。实际业务中,可能出现货物已经交付但收款登记延迟、顾客拒收、金额短缺,或者配送员尚未交账等情况。
是否必须创建 Transaction
从数据模型的使用方式看,Oscar 不会为现金支付自动连接银行或现金系统,因此技术上可以只修改订单状态,甚至完全不使用支付模型。不过,如果项目需要统一的支付审计、退款记录和已付金额统计,就应在实际收到现金时记录 debit transaction。
可以采用以下规则:
- 仅选择现金支付:不创建资金 transaction。
- 实际收到现金:创建
debittransaction。 - 实际退还现金:创建
refundtransaction。 - 现金没有真正的预授权:不要创建虚假的
allocatetransaction。 - 收款失败、顾客拒收或订单取消:通常不创建 debit,通过订单状态或业务事件记录原因。
因此,Transaction 不是用来定义 Cash on pickup 这类支付方式的。支付方式由 checkout 选择逻辑和 SourceType 表示,Transaction 只记录实际发生的资金操作。
备注:内容仅供参考。
