1023 lines
37 KiB
Python
1023 lines
37 KiB
Python
from datetime import datetime, timedelta
|
|
from appPublic.qr import gen_qr_withlogo
|
|
from appPublic.uniqueID import getID
|
|
from appPublic.jsonConfig import getConfig
|
|
from appPublic.dictObject import DictObject
|
|
from sqlor.dbpools import DBPools
|
|
from ahserver.serverenv import ServerEnv
|
|
from ahserver.filestorage import FileStorage
|
|
|
|
|
|
async def discount_qrcode(request, params_kw):
|
|
"""
|
|
Generate a promotional QR code for discount
|
|
discount less then 1 and greate then 0
|
|
valid_term is digit + one of ["D", "M", "Y"]
|
|
expired_date is a date after it the promote qrcode invalidable
|
|
"""
|
|
discount = params_kw.discount
|
|
valid_term = params_kw.valid_term
|
|
expired_date = params_kw.expired_date
|
|
env = request._run_ns
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
resellerid = await env.get_userorgid()
|
|
qr_id = getID()
|
|
url = env.entire_url('./promote') + f'?id={qr_id}'
|
|
fs = FileStorage()
|
|
p = fs._name2path(f'{getID()}.png')
|
|
gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc')
|
|
webp = fs.webpath(p)
|
|
async with db.sqlorContext(dbname) as sor:
|
|
biz_date = await env.get_business_date(sor)
|
|
if biz_date >= expired_date:
|
|
raise Exception('Promote QRCODE is out of time')
|
|
|
|
ret = {
|
|
'id': qr_id,
|
|
'resellerid': resellerid,
|
|
'discount': discount,
|
|
'valid_term': valid_term,
|
|
'expired_date': expired_date,
|
|
'qr_webpath': webp
|
|
}
|
|
await sor.C('discount_qr', ret.copy())
|
|
return DictObject(**ret)
|
|
return None
|
|
|
|
|
|
async def set_promote_discount(request, params_kw):
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
id = params_kw.id
|
|
customerid = await env.get_userorgid()
|
|
async with db.sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_qr', {'id': id})
|
|
if not recs:
|
|
raise Exception(f'promote id({id}) not exists')
|
|
biz_date = await env.get_business_date(sor)
|
|
if recs[0].expired_date <= biz_date:
|
|
raise Exception('Promote QRCODE is out of time')
|
|
cnt = int(recs[0].valid_term[:-1])
|
|
unit = recs[0].valid_term[-1]
|
|
enabled_date = biz_date
|
|
expired_date = ''
|
|
if unit == 'D':
|
|
expired_date = env.strdate_add(enabled_date, days=cnt)
|
|
elif unit == 'M':
|
|
expired_date = env.strdate_add(enabled_date, months=cnt)
|
|
elif unit == 'Y':
|
|
expired_date = env.strdate_add(enabled_date, years=cnt)
|
|
else:
|
|
raise Exception(f'Invalid valid_term({recs[0].valid_term})')
|
|
|
|
# Disable old active discount for this customer
|
|
await disable_old_discount(sor, recs[0].resellerid, customerid, biz_date)
|
|
|
|
# Create new discount record (no longer stores discount value directly)
|
|
discountid = getID()
|
|
ret = {
|
|
'id': discountid,
|
|
'name': f'促销折扣-{biz_date}',
|
|
'resellerid': recs[0].resellerid,
|
|
'customerid': customerid,
|
|
'enabled_date': enabled_date,
|
|
'expired_date': expired_date
|
|
}
|
|
await sor.C('discount', ret.copy())
|
|
|
|
# Create discount_detail record with the discount value from QR code
|
|
# prodtypeid=None, productid=None means applies to all products
|
|
detail_ret = {
|
|
'id': getID(),
|
|
'discountid': discountid,
|
|
'resellerid': recs[0].resellerid,
|
|
'prodtypeid': None,
|
|
'productid': None,
|
|
'discount': recs[0].discount,
|
|
}
|
|
await sor.C('discount_detail', detail_ret.copy())
|
|
|
|
return recs[0].discount
|
|
return None
|
|
|
|
|
|
async def disable_old_discount(sor, resellerid, customerid, biz_date):
|
|
"""Disable any active discount record for the given reseller+customer pair."""
|
|
# Use sor.R with sort instead of raw SQL + FOR UPDATE (DB-agnostic)
|
|
recs = await sor.R('discount', {
|
|
'resellerid': resellerid,
|
|
'customerid': customerid,
|
|
'sort': 'enabled_date desc'
|
|
})
|
|
if not recs:
|
|
return
|
|
# Find the active one in Python (DB-agnostic date comparison)
|
|
for rec in recs:
|
|
if rec.get('enabled_date', '') <= biz_date and rec.get('expired_date', '') > biz_date:
|
|
await sor.U('discount', {'id': rec['id'], 'expired_date': biz_date})
|
|
return
|
|
|
|
|
|
async def _discount_detail_exists(sor):
|
|
"""Check if discount_detail table exists."""
|
|
try:
|
|
await sor.sqlExe("SELECT 1 FROM discount_detail LIMIT 0", {})
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def sor_get_star_discount(sor, resellerid, biz_date):
|
|
"""Get default discount for a reseller (no specific customer)."""
|
|
sql = """select d.id, d.discount from discount d
|
|
where d.resellerid = ${resellerid}$
|
|
and d.customerid is NULL
|
|
and d.enabled_date <= ${biz_date}$
|
|
and d.expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return 1
|
|
|
|
discountid = recs[0].id
|
|
# If discount_detail table exists, look up default detail
|
|
if await _discount_detail_exists(sor):
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs2 = await sor.sqlExe(sql2, {'discountid': discountid})
|
|
if not recs2:
|
|
return 1
|
|
return recs2[0].discount
|
|
# Fallback: read discount value directly from discount table
|
|
return recs[0].discount if recs[0].discount is not None else 1
|
|
|
|
|
|
async def sor_get_customer_discount(sor, resellerid, customerid):
|
|
"""Get discount record for a customer (legacy, returns record not value).
|
|
Use sor_get_product_discount for product-specific discount."""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
sql = """select * from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid = ${customerid}$
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"customerid": customerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return await sor_get_star_discount(sor, resellerid, biz_date)
|
|
|
|
# Return default discount for this customer (prodtypeid=None, productid=None)
|
|
discountid = recs[0].id
|
|
if await _discount_detail_exists(sor):
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs2 = await sor.sqlExe(sql2, {'discountid': discountid})
|
|
if not recs2:
|
|
return 1
|
|
return recs2[0].discount
|
|
# Fallback: read discount value directly from discount table
|
|
return recs[0].discount if recs[0].discount is not None else 1
|
|
|
|
|
|
async def sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid):
|
|
"""
|
|
Get product-specific discount.
|
|
Lookup priority:
|
|
1. Exact match: discountid -> (prodtypeid, productid)
|
|
2. Type-level match: discountid -> (prodtypeid, NULL)
|
|
3. Default match: discountid -> (NULL, NULL)
|
|
|
|
Returns discount value (float), default 1.0 (no discount).
|
|
"""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
|
|
# Step 1: Find active discount record for reseller+customer
|
|
sql = """select id from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid = ${customerid}$
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
ns = {
|
|
"resellerid": resellerid,
|
|
"customerid": customerid,
|
|
"biz_date": biz_date
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
# Try reseller-level default (customerid is NULL)
|
|
sql = """select id from discount
|
|
where resellerid = ${resellerid}$
|
|
and customerid is NULL
|
|
and enabled_date <= ${biz_date}$
|
|
and expired_date > ${biz_date}$"""
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if not recs:
|
|
return 1.0
|
|
|
|
discountid = recs[0].id
|
|
|
|
# If discount_detail table doesn't exist, fall back to discount.discount
|
|
if not await _discount_detail_exists(sor):
|
|
return recs[0].discount if recs[0].discount is not None else 1.0
|
|
|
|
# Step 2: Try exact product match
|
|
sql2 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid = ${prodtypeid}$
|
|
and productid = ${productid}$"""
|
|
recs2 = await sor.sqlExe(sql2, {
|
|
'discountid': discountid,
|
|
'prodtypeid': prodtypeid,
|
|
'productid': productid
|
|
})
|
|
if recs2:
|
|
return recs2[0].discount
|
|
|
|
# Step 3: Try product type level match (productid is NULL)
|
|
sql3 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid = ${prodtypeid}$
|
|
and productid is NULL"""
|
|
recs3 = await sor.sqlExe(sql3, {
|
|
'discountid': discountid,
|
|
'prodtypeid': prodtypeid,
|
|
})
|
|
if recs3:
|
|
return recs3[0].discount
|
|
|
|
# Step 4: Try default match (both NULL)
|
|
sql4 = """select discount from discount_detail
|
|
where discountid = ${discountid}$
|
|
and prodtypeid is NULL
|
|
and productid is NULL"""
|
|
recs4 = await sor.sqlExe(sql4, {'discountid': discountid})
|
|
if recs4:
|
|
return recs4[0].discount
|
|
|
|
return 1.0
|
|
|
|
|
|
async def sor_get_min_product_discount(sor, product_id, resellerid, customerid, exact_match=False):
|
|
"""Find the minimum discount for a specific product from active discount records.
|
|
|
|
Args:
|
|
sor: sqlor context
|
|
product_id: product.id
|
|
resellerid: discount.resellerid
|
|
customerid: discount.customerid (user/supplier/distributor org ID)
|
|
exact_match: if True, only match exact customerid (no NULL/'*' fallback).
|
|
Use True for suppliers/distributors, False (default) for customers.
|
|
"""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
|
|
if exact_match:
|
|
customer_cond = "d.customerid = ${customerid}$"
|
|
else:
|
|
customer_cond = "(d.customerid = ${customerid}$ OR d.customerid IS NULL OR d.customerid = '*')"
|
|
|
|
sql = f"""SELECT dd.discount
|
|
FROM discount_detail dd
|
|
JOIN discount d ON dd.discountid = d.id
|
|
WHERE d.resellerid = ${{resellerid}}$
|
|
AND {customer_cond}
|
|
AND d.enabled_date <= ${{biz_date}}$
|
|
AND d.expired_date > ${{biz_date}}$
|
|
AND dd.productid = ${{product_id}}$"""
|
|
ns = {
|
|
'resellerid': resellerid,
|
|
'customerid': customerid,
|
|
'biz_date': biz_date,
|
|
'product_id': product_id,
|
|
}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
if recs:
|
|
discounts = [r.discount for r in recs if r.discount is not None]
|
|
if discounts:
|
|
return min(discounts)
|
|
return 1.0
|
|
|
|
|
|
async def get_min_product_discount(product_id, resellerid, customerid):
|
|
"""Get the minimum discount for a specific product (convenience wrapper)."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_min_product_discount(sor, product_id, resellerid, customerid)
|
|
return 1.0
|
|
|
|
|
|
async def get_min_supplier_discount(product_id, resellerid, supplierid):
|
|
"""Get the minimum discount for a supplier on a specific product.
|
|
|
|
No wildcard/NULL fallback — exact supplier match only.
|
|
"""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_min_product_discount(sor, product_id, resellerid, supplierid, exact_match=True)
|
|
return 1.0
|
|
|
|
|
|
async def get_min_distributor_discount(product_id, resellerid, distributorid):
|
|
"""Get the minimum discount for a distributor on a specific product.
|
|
|
|
No wildcard/NULL fallback — exact distributor match only.
|
|
"""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_min_product_discount(sor, product_id, resellerid, distributorid, exact_match=True)
|
|
return 1.0
|
|
|
|
|
|
async def get_customer_discount(resellerid, customerid):
|
|
"""Legacy: get default discount for a customer (all products)."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_customer_discount(sor, resellerid, customerid)
|
|
return 1
|
|
|
|
|
|
async def get_product_discount(resellerid, customerid, prodtypeid, productid):
|
|
"""
|
|
Get product-specific discount.
|
|
Parameters:
|
|
resellerid: merchant ID
|
|
customerid: customer ID
|
|
prodtypeid: product type ID
|
|
productid: product ID
|
|
Returns: discount value (float), 1.0 means no discount.
|
|
"""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
async with db.sqlorContext(dbname) as sor:
|
|
return await sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid)
|
|
return 1.0
|
|
|
|
|
|
async def get_discount_details(sor, discountid):
|
|
"""Get all product detail records for a given discount."""
|
|
sql = """select * from discount_detail
|
|
where discountid = ${discountid}$
|
|
order by prodtypeid, productid"""
|
|
recs = await sor.sqlExe(sql, {'discountid': discountid})
|
|
return recs
|
|
|
|
|
|
async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount):
|
|
"""Add a product-specific discount detail."""
|
|
if discount <= 0 or discount > 1:
|
|
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)')
|
|
|
|
ret = {
|
|
'id': getID(),
|
|
'discountid': discountid,
|
|
'resellerid': resellerid,
|
|
'prodtypeid': prodtypeid if prodtypeid else None,
|
|
'productid': productid if productid else None,
|
|
'discount': discount,
|
|
}
|
|
await sor.C('discount_detail', ret.copy())
|
|
return ret
|
|
|
|
|
|
async def update_discount_detail(sor, detail_id, discount):
|
|
"""Update a discount detail record."""
|
|
if discount <= 0 or discount > 1:
|
|
raise Exception(f'discount({discount}) invalid, must be between 0 and 1 (inclusive)')
|
|
|
|
await sor.U('discount_detail', {'id': detail_id, 'discount': discount})
|
|
|
|
|
|
async def delete_discount_detail(sor, detail_id):
|
|
"""Delete a discount detail record."""
|
|
await sor.D('discount_detail', {'id': detail_id})
|
|
|
|
|
|
# ─── Marketing & Promo Code & Customer Bind ───
|
|
|
|
async def create_marketing(request, params_kw):
|
|
"""Create a marketing plan (reseller operator only)."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
resellerid = await env.get_userorgid()
|
|
user_id = await env.get_user()
|
|
name = params_kw.get('name')
|
|
enabled_date = params_kw.get('enabled_date')
|
|
expired_date = params_kw.get('expired_date')
|
|
|
|
if not name:
|
|
return {'status': 'error', 'message': 'Missing name'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
marketing_id = getID()
|
|
await sor.C('discount_marketing', {
|
|
'id': marketing_id,
|
|
'resellerid': resellerid,
|
|
'name': name,
|
|
'enabled_date': enabled_date,
|
|
'expired_date': expired_date,
|
|
'status': '1',
|
|
'created_by': user_id,
|
|
})
|
|
|
|
# Also create a discount record linked to this marketing
|
|
discount_id = getID()
|
|
await sor.C('discount', {
|
|
'id': discount_id,
|
|
'name': f'营销方案-{name}',
|
|
'resellerid': resellerid,
|
|
'customerid': None, # applies to all customers of this reseller
|
|
'enabled_date': enabled_date,
|
|
'expired_date': expired_date,
|
|
})
|
|
|
|
return {'status': 'success', 'id': marketing_id, 'discount_id': discount_id}
|
|
|
|
|
|
async def update_marketing(request, params_kw):
|
|
"""Update a marketing plan."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
marketing_id = params_kw.get('id')
|
|
resellerid = await env.get_userorgid()
|
|
|
|
if not marketing_id:
|
|
return {'status': 'error', 'message': 'Missing id'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
# Verify ownership
|
|
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
|
|
if not recs:
|
|
return {'status': 'error', 'message': 'Marketing not found or no access'}
|
|
|
|
update_data = {'id': marketing_id}
|
|
for field in ['name', 'enabled_date', 'expired_date', 'status']:
|
|
if field in params_kw:
|
|
update_data[field] = params_kw[field]
|
|
|
|
await sor.U('discount_marketing', update_data)
|
|
return {'status': 'success', 'id': marketing_id}
|
|
|
|
|
|
async def delete_marketing(request, params_kw):
|
|
"""Delete a marketing plan and its promo codes."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
marketing_id = params_kw.get('id')
|
|
resellerid = await env.get_userorgid()
|
|
|
|
if not marketing_id:
|
|
return {'status': 'error', 'message': 'Missing id'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
|
|
if not recs:
|
|
return {'status': 'error', 'message': 'Marketing not found or no access'}
|
|
|
|
# Delete promo codes first
|
|
promo_codes = await sor.R('discount_promo_code', {'marketing_id': marketing_id})
|
|
for pc in (promo_codes or []):
|
|
await sor.D('discount_promo_code', {'id': pc.id})
|
|
|
|
await sor.D('discount_marketing', {'id': marketing_id})
|
|
return {'status': 'success'}
|
|
|
|
|
|
async def get_marketing(request, params_kw):
|
|
"""Get marketing plan detail with its discount details."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
marketing_id = params_kw.get('id')
|
|
resellerid = await env.get_userorgid()
|
|
|
|
if not marketing_id:
|
|
return {'status': 'error', 'message': 'Missing id'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
|
|
if not recs:
|
|
return {'status': 'error', 'message': 'Marketing not found'}
|
|
|
|
marketing = recs[0]
|
|
# Get associated discount record
|
|
discount_recs = await sor.sqlExe(
|
|
"SELECT * FROM discount WHERE name = ${name}$ AND resellerid = ${resellerid}$ AND customerid IS NULL",
|
|
{'name': f'营销方案-{marketing.name}', 'resellerid': resellerid}
|
|
)
|
|
|
|
discount_details = []
|
|
if discount_recs:
|
|
discount_id = discount_recs[0].id
|
|
discount_details = await sor.R('discount_detail', {'discountid': discount_id, 'resellerid': resellerid})
|
|
|
|
return {
|
|
'status': 'success',
|
|
'data': {
|
|
'marketing': marketing,
|
|
'discount_details': discount_details or []
|
|
}
|
|
}
|
|
|
|
|
|
async def list_marketings(request, params_kw):
|
|
"""List all marketing plans for current reseller."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
resellerid = await env.get_userorgid()
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_marketing', {'resellerid': resellerid, 'sort': 'created_at desc'})
|
|
return {'status': 'success', 'data': recs or [], 'total': len(recs or [])}
|
|
|
|
|
|
async def generate_promo_code(request, params_kw):
|
|
"""Generate a promo code (sale role).
|
|
marketing_id can be empty (pure invitation code)."""
|
|
env = request._run_ns
|
|
dbname = env.get_module_dbname('discount')
|
|
resellerid = await env.get_userorgid()
|
|
sale_id = await env.get_user()
|
|
marketing_id = params_kw.get('marketing_id') # can be empty
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
# If marketing_id provided, verify it exists and belongs to this reseller
|
|
if marketing_id:
|
|
mkt = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
|
|
if not mkt:
|
|
return {'status': 'error', 'message': 'Marketing not found or no access'}
|
|
code_type = '1' # promo code
|
|
else:
|
|
code_type = '2' # invitation code
|
|
|
|
code_id = getID()
|
|
await sor.C('discount_promo_code', {
|
|
'id': code_id, # id IS the promo code
|
|
'marketing_id': marketing_id if marketing_id else None,
|
|
'resellerid': resellerid,
|
|
'sale_id': sale_id,
|
|
'code_type': code_type,
|
|
'status': '1',
|
|
})
|
|
|
|
return {'status': 'success', 'code': code_id}
|
|
|
|
|
|
async def disable_promo_code(request, params_kw):
|
|
"""Disable a promo code."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
code_id = params_kw.get('id')
|
|
resellerid = await env.get_userorgid()
|
|
|
|
if not code_id:
|
|
return {'status': 'error', 'message': 'Missing id'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_promo_code', {'id': code_id, 'resellerid': resellerid})
|
|
if not recs:
|
|
return {'status': 'error', 'message': 'Promo code not found'}
|
|
|
|
await sor.U('discount_promo_code', {'id': code_id, 'status': '0'})
|
|
return {'status': 'success'}
|
|
|
|
|
|
async def generate_promo_qr(request, params_kw):
|
|
"""Generate QR code for a promo code."""
|
|
env = request._run_ns
|
|
dbname = env.get_module_dbname('discount')
|
|
promo_id = params_kw.get('id', '')
|
|
|
|
if not promo_id:
|
|
return {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Missing promo code id', 'type': 'error'}}
|
|
|
|
from appPublic.qr import gen_qr_withlogo
|
|
from appPublic.jsonConfig import getConfig
|
|
from ahserver.filestorage import FileStorage
|
|
from appPublic.uniqueID import getID as _getID
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_promo_code', {'id': promo_id})
|
|
if not recs:
|
|
return {'widgettype': 'Message', 'options': {'title': 'Error', 'message': f'Promo code {promo_id} not found', 'type': 'error'}}
|
|
|
|
config = getConfig()
|
|
qr_url = env.entire_url('./promote/index.ui') + f'?code={promo_id}'
|
|
fs = FileStorage()
|
|
p = fs._name2path(f'{_getID()}.png')
|
|
gen_qr_withlogo(qr_url, p, logopath=config.logopath, logoloc='cc')
|
|
webp = fs.webpath(p)
|
|
qr_file_url = env.entire_url('/idfile') + f'?path={webp}'
|
|
|
|
return {
|
|
"widgettype": "VBox",
|
|
"options": {"width": "100%", "height": "100%", "spacing": 10},
|
|
"subwidgets": [
|
|
{"widgettype": "Image", "options": {"width": "340px", "height": "340px", "url": qr_file_url}},
|
|
{"widgettype": "HBox", "options": {"width": "100%", "cheight": 2},
|
|
"subwidgets": [
|
|
{"widgettype": "Text", "options": {"otext": "促销码:", "i18n": True, "cwidth": 5}},
|
|
{"widgettype": "Text", "options": {"text": promo_id}}
|
|
]},
|
|
{"widgettype": "HBox", "options": {"width": "100%", "cheight": 2},
|
|
"subwidgets": [
|
|
{"widgettype": "Text", "options": {"otext": "链接:", "i18n": True, "cwidth": 5}},
|
|
{"widgettype": "Text", "options": {"text": qr_url, "css": "text-muted"}}
|
|
]}
|
|
]
|
|
}
|
|
|
|
|
|
async def bind_customer(request, params_kw):
|
|
"""Bind customer to reseller+sale via promo code (called during registration/login).
|
|
First-time binding only. Update discounts if new ones are lower.
|
|
params_kw may contain 'customerid' to override current user's orgid (for registration flow)."""
|
|
env = request._run_ns
|
|
dbname = env.get_module_dbname('discount')
|
|
config = getConfig()
|
|
db = DBPools()
|
|
db.databases = config.databases
|
|
code_id = params_kw.get('promo_code_id')
|
|
customerid = params_kw.get('customerid') or await env.get_userorgid()
|
|
|
|
if not code_id:
|
|
return {'status': 'error', 'message': 'Missing promo_code_id'}
|
|
|
|
async with db.sqlorContext(dbname) as sor:
|
|
# Check if customer already bound
|
|
existing = await sor.R('discount_customer_bind', {'customerid': customerid})
|
|
if existing:
|
|
return {'status': 'error', 'message': 'Customer already bound'}
|
|
|
|
# Verify promo code
|
|
code_recs = await sor.R('discount_promo_code', {'id': code_id, 'status': '1'})
|
|
if not code_recs:
|
|
return {'status': 'error', 'message': 'Invalid or disabled promo code'}
|
|
|
|
code_rec = code_recs[0]
|
|
resellerid = code_rec.resellerid
|
|
sale_id = code_rec.sale_id
|
|
|
|
# Create binding
|
|
bind_id = getID()
|
|
await sor.C('discount_customer_bind', {
|
|
'id': bind_id,
|
|
'customerid': customerid,
|
|
'resellerid': resellerid,
|
|
'sale_id': sale_id,
|
|
'promo_code_id': code_id,
|
|
'bind_type': '1',
|
|
})
|
|
|
|
# If promo code has marketing, copy discount details to customer
|
|
if code_rec.marketing_id:
|
|
await _apply_marketing_discounts_to_customer(sor, resellerid, customerid, code_rec.marketing_id)
|
|
|
|
return {'status': 'success', 'resellerid': resellerid, 'sale_id': sale_id}
|
|
|
|
|
|
async def _apply_marketing_discounts_to_customer(sor, resellerid, customerid, marketing_id):
|
|
"""Copy marketing's discount details to customer, only if new discount is lower."""
|
|
env = ServerEnv()
|
|
biz_date = await env.get_business_date(sor)
|
|
|
|
# Find marketing's discount record
|
|
mkt_recs = await sor.R('discount_marketing', {'id': marketing_id, 'resellerid': resellerid})
|
|
if not mkt_recs:
|
|
return
|
|
|
|
mkt_name = mkt_recs[0].name
|
|
mkt_discount_recs = await sor.sqlExe(
|
|
"SELECT * FROM discount WHERE name = ${name}$ AND resellerid = ${resellerid}$ AND customerid IS NULL",
|
|
{'name': f'营销方案-{mkt_name}', 'resellerid': resellerid}
|
|
)
|
|
if not mkt_discount_recs:
|
|
return
|
|
|
|
mkt_discount_id = mkt_discount_recs[0].id
|
|
mkt_details = await sor.R('discount_detail', {'discountid': mkt_discount_id})
|
|
|
|
if not mkt_details:
|
|
return
|
|
|
|
# Find or create customer's discount record
|
|
cust_discount = await sor_get_customer_discount(sor, resellerid, customerid)
|
|
|
|
# Get customer's discount id
|
|
cust_sql = """SELECT id FROM discount
|
|
WHERE resellerid = ${resellerid}$ AND customerid = ${customerid}$
|
|
AND enabled_date <= ${biz_date}$ AND expired_date > ${biz_date}$"""
|
|
cust_recs = await sor.sqlExe(cust_sql, {'resellerid': resellerid, 'customerid': customerid, 'biz_date': biz_date})
|
|
|
|
if not cust_recs:
|
|
# Create new discount record for customer
|
|
new_discount_id = getID()
|
|
await sor.C('discount', {
|
|
'id': new_discount_id,
|
|
'name': f'客户折扣-{customerid}',
|
|
'resellerid': resellerid,
|
|
'customerid': customerid,
|
|
'enabled_date': biz_date,
|
|
'expired_date': '2099-12-31',
|
|
})
|
|
cust_discount_id = new_discount_id
|
|
else:
|
|
cust_discount_id = cust_recs[0].id
|
|
|
|
# Copy details, only if new discount is lower than existing
|
|
for mkt_detail in mkt_details:
|
|
# Check if customer already has this product discount
|
|
existing_detail = await sor.sqlExe(
|
|
"""SELECT * FROM discount_detail
|
|
WHERE discountid = ${discountid}$ AND prodtypeid = ${prodtypeid}$ AND productid = ${productid}$""",
|
|
{
|
|
'discountid': cust_discount_id,
|
|
'prodtypeid': mkt_detail.prodtypeid,
|
|
'productid': mkt_detail.productid
|
|
}
|
|
)
|
|
|
|
if existing_detail:
|
|
# Only update if new discount is lower (better for customer)
|
|
if mkt_detail.discount < existing_detail[0].discount:
|
|
await sor.U('discount_detail', {'id': existing_detail[0].id, 'discount': mkt_detail.discount})
|
|
else:
|
|
# Add new detail
|
|
await sor.C('discount_detail', {
|
|
'id': getID(),
|
|
'discountid': cust_discount_id,
|
|
'resellerid': resellerid,
|
|
'prodtypeid': mkt_detail.prodtypeid,
|
|
'productid': mkt_detail.productid,
|
|
'discount': mkt_detail.discount,
|
|
})
|
|
|
|
|
|
async def get_customer_bind(request, params_kw):
|
|
"""Get customer's binding info."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
customerid = params_kw.get('customerid') or await env.get_userorgid()
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
recs = await sor.R('discount_customer_bind', {'customerid': customerid})
|
|
if not recs:
|
|
return {'status': 'success', 'data': None}
|
|
return {'status': 'success', 'data': recs[0]}
|
|
|
|
|
|
async def assign_customer_to_sale(request, params_kw):
|
|
"""Owner operator manually assigns unbound customer to a sale."""
|
|
env = ServerEnv()
|
|
dbname = env.get_module_dbname('discount')
|
|
customerid = params_kw.get('customerid')
|
|
sale_id = params_kw.get('sale_id')
|
|
|
|
if not customerid or not sale_id:
|
|
return {'status': 'error', 'message': 'Missing customerid or sale_id'}
|
|
|
|
# Verify caller is owner org (orgid='0')
|
|
caller_orgid = await env.get_userorgid()
|
|
if caller_orgid != '0':
|
|
return {'status': 'error', 'message': 'Only owner can assign customers'}
|
|
|
|
async with DBPools().sqlorContext(dbname) as sor:
|
|
# Check if already bound
|
|
existing = await sor.R('discount_customer_bind', {'customerid': customerid})
|
|
if existing:
|
|
return {'status': 'error', 'message': 'Customer already bound'}
|
|
|
|
bind_id = getID()
|
|
await sor.C('discount_customer_bind', {
|
|
'id': bind_id,
|
|
'customerid': customerid,
|
|
'resellerid': '0', # owner org
|
|
'sale_id': sale_id,
|
|
'promo_code_id': None,
|
|
'bind_type': '2', # manual assignment
|
|
})
|
|
|
|
return {'status': 'success', 'id': bind_id}
|
|
|
|
|
|
async def sor_before_insert_discount_promo_code(env, sor, ns):
|
|
"""CRUD hook: auto-set sale_id from logged-in user on insert."""
|
|
from appPublic.uniqueID import getID as _getID
|
|
ns['id'] = ns.get('id') or _getID()
|
|
ns['sale_id'] = ns.get('sale_id') or await env.get_userid()
|
|
ns['resellerid'] = ns.get('resellerid') or await env.get_userorgid()
|
|
|
|
|
|
def load_discount():
|
|
env = ServerEnv()
|
|
# before_insert hooks
|
|
env.sor_before_insert_discount_promo_code = sor_before_insert_discount_promo_code
|
|
# existing registrations...
|
|
env.get_customer_discount = get_customer_discount
|
|
env.sor_get_customer_discount = sor_get_customer_discount
|
|
env.get_product_discount = get_product_discount
|
|
env.sor_get_product_discount = sor_get_product_discount
|
|
env.sor_get_min_product_discount = sor_get_min_product_discount
|
|
env.get_min_product_discount = get_min_product_discount
|
|
env.get_min_supplier_discount = get_min_supplier_discount
|
|
env.get_min_distributor_discount = get_min_distributor_discount
|
|
env.discount_qrcode = discount_qrcode
|
|
env.set_promote_discount = set_promote_discount
|
|
env.get_discount_details = get_discount_details
|
|
env.add_discount_detail = add_discount_detail
|
|
env.update_discount_detail = update_discount_detail
|
|
env.delete_discount_detail = delete_discount_detail
|
|
# Marketing & Promo Code
|
|
env.create_marketing = create_marketing
|
|
env.update_marketing = update_marketing
|
|
env.delete_marketing = delete_marketing
|
|
env.get_marketing = get_marketing
|
|
env.list_marketings = list_marketings
|
|
env.generate_promo_code = generate_promo_code
|
|
env.disable_promo_code = disable_promo_code
|
|
env.generate_promo_qr = generate_promo_qr
|
|
env.bind_customer = bind_customer
|
|
env.get_customer_bind = get_customer_bind
|
|
env.assign_customer_to_sale = assign_customer_to_sale
|
|
# Discount setting products list
|
|
env.get_discount_setting_products = get_discount_setting_products
|
|
env.get_discount_info = get_discount_info
|
|
# Marketing plan discount setting
|
|
env.get_marketing_discount_products = get_marketing_discount_products
|
|
env.get_marketing_info = get_marketing_info
|
|
|
|
|
|
async def get_discount_setting_products(request):
|
|
"""Get products with their discount details for discount_setting page.
|
|
Used in Jinja2 template: {% set products = get_discount_setting_products(request) %}
|
|
"""
|
|
env = request._run_ns
|
|
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
|
|
user_orgid = await env.get_userorgid()
|
|
|
|
if not discountid or not user_orgid:
|
|
return []
|
|
|
|
dbname = env.get_module_dbname('discount')
|
|
db = DBPools()
|
|
config = getConfig()
|
|
db.databases = config.databases
|
|
|
|
# 查询基准折扣方案ID (customerid='*', 全部用户适用)
|
|
base_sql = "SELECT id FROM discount WHERE customerid='*' AND resellerid=${org_id}$ LIMIT 1"
|
|
base_ns = {'org_id': user_orgid}
|
|
|
|
sql = """
|
|
SELECT
|
|
p.id as productid,
|
|
p.product_code,
|
|
p.product_name,
|
|
p.category_id,
|
|
pc.name as category_name,
|
|
dd.id as detail_id,
|
|
COALESCE(dd.discount, base_dd.discount) as discount,
|
|
dd.discountid,
|
|
CASE WHEN dd.id IS NOT NULL THEN 1 ELSE 0 END as in_plan
|
|
FROM product p
|
|
LEFT JOIN product_category pc ON p.category_id = pc.id
|
|
LEFT JOIN discount_detail dd ON dd.productid = p.id
|
|
AND dd.discountid = ${discountid}$
|
|
LEFT JOIN discount_detail base_dd ON base_dd.productid = p.id
|
|
AND base_dd.discountid = ${base_discountid}$
|
|
WHERE p.org_id = ${org_id}$ AND p.status = '1'
|
|
"""
|
|
ns = {'discountid': discountid, 'org_id': user_orgid}
|
|
|
|
async with db.sqlorContext(dbname) as sor:
|
|
# 查询基准折扣方案ID, 若不存在则降级(没有基准折扣)
|
|
base_recs = await sor.sqlExe(base_sql, base_ns)
|
|
base_discountid = base_recs[0].id if base_recs else None
|
|
if base_discountid:
|
|
ns['base_discountid'] = base_discountid
|
|
else:
|
|
# 无基准方案时, 用discountid自身代替, 保证COALESCE正常(只会取dd.discount, base_dd为NULL)
|
|
ns['base_discountid'] = discountid
|
|
|
|
recs = await sor.sqlExe(sql, ns)
|
|
# 为每个产品附加 in_plan 标记
|
|
return recs if recs else []
|
|
|
|
|
|
async def get_discount_info(request):
|
|
"""Get discount name and customer name for display header."""
|
|
env = request._run_ns
|
|
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
|
|
if not discountid:
|
|
return {}
|
|
dbname = env.get_module_dbname('discount')
|
|
db = DBPools()
|
|
config = getConfig()
|
|
db.databases = config.databases
|
|
sql = """
|
|
SELECT dm.name, o.orgname
|
|
FROM discount_marketing dm
|
|
LEFT JOIN organization o ON dm.resellerid = o.id
|
|
WHERE dm.id = ${discountid}$
|
|
"""
|
|
async with db.sqlorContext(dbname) as sor:
|
|
recs = await sor.sqlExe(sql, {'discountid': discountid})
|
|
if recs:
|
|
return {'name': recs[0].name or '', 'customer': recs[0].orgname or ''}
|
|
return {}
|
|
|
|
|
|
async def get_marketing_discount_products(request):
|
|
"""Get products with base (all-user) discount and marketing-specific discount.
|
|
Used in Jinja2 template: {% set products = get_marketing_discount_products(request) %}
|
|
"""
|
|
env = request._run_ns
|
|
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
|
|
user_orgid = await env.get_userorgid()
|
|
if not discountid or not user_orgid:
|
|
return []
|
|
dbname = env.get_module_dbname('discount')
|
|
db = DBPools()
|
|
config = getConfig()
|
|
db.databases = config.databases
|
|
sql = """
|
|
SELECT
|
|
p.id as productid,
|
|
p.product_code,
|
|
p.product_name,
|
|
p.category_id,
|
|
pc.name as category_name,
|
|
base.discount as base_discount,
|
|
mkt.id as detail_id,
|
|
mkt.discount as marketing_discount,
|
|
CASE WHEN mkt.id IS NOT NULL THEN '1' ELSE '0' END as in_marketing
|
|
FROM product p
|
|
LEFT JOIN product_category pc ON p.category_id = pc.id
|
|
LEFT JOIN (
|
|
SELECT dd.productid, dd.discount
|
|
FROM discount_detail dd
|
|
JOIN discount d ON dd.discountid = d.id
|
|
WHERE d.resellerid = ${org_id}$ AND (d.customerid IS NULL OR d.customerid = '*')
|
|
) base ON base.productid = p.id
|
|
LEFT JOIN discount_detail mkt ON mkt.productid = p.id AND mkt.discountid = ${discountid}$
|
|
WHERE p.org_id = ${org_id}$ AND p.status = '1'
|
|
ORDER BY pc.name, p.product_name
|
|
"""
|
|
ns = {'discountid': discountid, 'org_id': user_orgid}
|
|
async with db.sqlorContext(dbname) as sor:
|
|
recs = await sor.sqlExe(sql, ns)
|
|
return recs if recs else []
|
|
|
|
|
|
async def get_marketing_info(request):
|
|
"""Get marketing plan name for display header."""
|
|
env = request._run_ns
|
|
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
|
|
if not discountid:
|
|
return {}
|
|
dbname = env.get_module_dbname('discount')
|
|
db = DBPools()
|
|
config = getConfig()
|
|
db.databases = config.databases
|
|
sql = "SELECT name FROM discount_marketing WHERE id = ${discountid}$"
|
|
async with db.sqlorContext(dbname) as sor:
|
|
recs = await sor.sqlExe(sql, {'discountid': discountid})
|
|
if recs:
|
|
return {'name': recs[0].name or ''}
|
|
return {}
|