PAYATHON 2026

django-oscar 2.0 如何定义店内现金支付方式

支付老李

明确结论

在 django-oscar 2.0 中,支付方式不需要通过 payment/methods.py 里的 Base、CashOnPickup 等类注册。仅仅为了支持现金支付,也不需要继承 SourceType、Source 或 Transaction。

可以按下面的方式实现:

  1. 在结账流程中提供“到店现金支付”“货到付款”等选项,并将用户的选择保存在 checkout session 或提交表单中。
  2. 为不同的支付渠道创建对应的 SourceType 数据。
  3. 下单时,在 PaymentDetailsView.handle_payment() 中为订单添加 Source。
  4. 此时尚未收到现金,因此不要将订单记为已付款,也不要伪造 pre-auth。
  5. 实际收款时调用 Source.debit(),生成一笔 debit 类型的 Transaction,并记录相应的 payment event。

SourceType、Source 和 Transaction 是支付账务模型,不能代替支付方式的选择界面。支付选项如何展示、用户如何选择,以及哪些订单可以使用某种支付方式,仍由项目自己的 checkout 业务逻辑决定。

三个模型分别表示什么

SourceType

SourceType 表示资金来源的类别,例如:

  • Cash on pickup
  • Cash on delivery
  • Credit card
  • Bank 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 设置为订单金额。
  • 创建 debit transaction。
  • 记录 paid payment 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 payment
  • Awaiting pickup
  • Awaiting 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。
  • 实际收到现金:创建 debit transaction。
  • 实际退还现金:创建 refund transaction。
  • 现金没有真正的预授权:不要创建虚假的 allocate transaction。
  • 收款失败、顾客拒收或订单取消:通常不创建 debit,通过订单状态或业务事件记录原因。

因此,Transaction 不是用来定义 Cash on pickup 这类支付方式的。支付方式由 checkout 选择逻辑和 SourceType 表示,Transaction 只记录实际发生的资金操作。

备注:内容仅供参考。