产品调价继承工单币种(港币不再默成美元);陆运集拼空分档只填询价重量那一档,跨境整车按已选线路写起讫。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,14 +11,17 @@ import logging
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.schema.field_validate import QUOTE_DATE_SYNONYMS
|
||||
try:
|
||||
from agent.schema.field_validate import QUOTE_DATE_SYNONYMS
|
||||
except ImportError: # 测服旧 release 未导出同义词时仍可抽产品价
|
||||
QUOTE_DATE_SYNONYMS = ()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 报价日期同义词。长的在前,避免短词截断。货好时间、提货地址不在这里。
|
||||
_QUOTE_DATE_LABEL_RE = "|".join(
|
||||
re.escape(name)
|
||||
for name in sorted(("报价日期",) + QUOTE_DATE_SYNONYMS, key=len, reverse=True)
|
||||
for name in sorted(("报价日期",) + tuple(QUOTE_DATE_SYNONYMS or ()), key=len, reverse=True)
|
||||
)
|
||||
|
||||
_MODE_WORDS = (
|
||||
@@ -829,9 +832,11 @@ def extract_air_collab_fields(
|
||||
return {k: got[k] for k in keys if str(got.get(k) or "").strip()}
|
||||
|
||||
|
||||
# 金额旁币种词:含港币;禁止只认 USD/CNY 把港币单漏成美元。
|
||||
_CCY_TOKEN = r"USD|CNY|RMB|HKD|EUR|JPY|GBP|美金|美元|人民币|港币|港元|欧元|日元|英镑"
|
||||
_RE_MONEY = re.compile(
|
||||
r"(?:USD|CNY|RMB|美金|美元|人民币)?\s*(\d+(?:\.\d+)?)\s*(?:USD|CNY|RMB|美金|美元|人民币|元)?"
|
||||
r"|(?:USD|CNY|RMB|美金|美元)\s*(\d+(?:\.\d+)?)",
|
||||
rf"(?:{_CCY_TOKEN}|元)?\s*(\d+(?:\.\d+)?)\s*(?:{_CCY_TOKEN}|元)?"
|
||||
rf"|(?:{_CCY_TOKEN})\s*(\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
# 陆运集拼:「2-500KG 单价CNY990」是分档单价,不是「燃油附加费=2」。
|
||||
@@ -839,14 +844,14 @@ _RE_WEIGHT_BAND_PRICE = re.compile(
|
||||
r"(?P<min>\d+)\s*[-~~—–到至]\s*(?P<max>\d+)\s*K\s*G"
|
||||
r"(?:\s*以上)?"
|
||||
r"\s*(?:单价)?"
|
||||
r"\s*(?:(?P<ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"\s*(?:(?P<ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<price>\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
_RE_WEIGHT_BAND_OPEN = re.compile(
|
||||
r"(?P<min>\d+)\s*K\s*G\s*以上"
|
||||
r"\s*(?:单价)?"
|
||||
r"\s*(?:(?P<ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"\s*(?:(?P<ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<price>\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
@@ -862,9 +867,9 @@ _RE_FEE_LINE = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费|[A-Za-z]{2,10})"
|
||||
r"\s*(?:改为|改成|调整为|调整成)?"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?:(?P<ccy1>USD|CNY|RMB|美金|美元|人民币)\s+)?"
|
||||
rf"\s*(?:(?P<ccy1>{_CCY_TOKEN})\s+)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy2>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
rf"\s*(?P<ccy2>{_CCY_TOKEN}|元)?",
|
||||
re.I,
|
||||
)
|
||||
# 「文件发改为:500」漏了「费」、中间还有冒号,不能丢掉。
|
||||
@@ -873,19 +878,21 @@ _RE_FEE_CHANGE = re.compile(
|
||||
r"\s*(?:改为|改成|调整为|调整成)"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
rf"\s*(?P<ccy>{_CCY_TOKEN}|元)?",
|
||||
re.I,
|
||||
)
|
||||
_SKIP_FEE_NAMES = frozenset({"usd", "cny", "rmb", "美金", "美元", "人民币", "元"})
|
||||
_SKIP_FEE_NAMES = frozenset(
|
||||
{"usd", "cny", "rmb", "hkd", "eur", "jpy", "gbp", "美金", "美元", "人民币", "港币", "港元", "元"}
|
||||
)
|
||||
# 航线常写「空运费 单价CNY99,数量100,金额CNY199」。单价和金额不是同一个数,必须认金额。
|
||||
_RE_FEE_PRICED = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费)"
|
||||
r"\s*单价\s*"
|
||||
r"(?:(?P<unit_ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"(?:(?P<unit_ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<unit>\d+(?:\.\d+)?)"
|
||||
r"(?:\s*[,,、]\s*数量\s*(?P<qty>\d+(?:\.\d+)?))?"
|
||||
r"(?:\s*[,,、]\s*金额\s*"
|
||||
r"(?:(?P<amt_ccy>USD|CNY|RMB|美金|美元|人民币)\s*)?"
|
||||
rf"(?:(?P<amt_ccy>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?))?",
|
||||
re.I,
|
||||
)
|
||||
@@ -898,21 +905,40 @@ def _ascii_amount(raw: str) -> str:
|
||||
|
||||
|
||||
def _currency_of(text: str, hint: str = "") -> str:
|
||||
"""
|
||||
从正文/提示抽三字码。正文没写币种时只回传 hint,禁止默认编造 USD。
|
||||
|
||||
港币必须先于「元」判断,避免「港元」被误判成 CNY。
|
||||
WO202610090107:TMS 是 HKD,产品只写「运输费调整为1100」时必须继承 HKD。
|
||||
"""
|
||||
blob = f"{hint} {text}".upper()
|
||||
if any(x in blob for x in ("CNY", "RMB", "人民币", "元")):
|
||||
raw = f"{hint} {text}"
|
||||
if any(x in blob for x in ("HKD", "港币", "港元", "HK$")) or "港币" in raw or "港元" in raw:
|
||||
return "HKD"
|
||||
if any(x in blob for x in ("CNY", "RMB", "人民币")) or (
|
||||
"元" in raw and "港元" not in raw and "港币" not in raw
|
||||
):
|
||||
return "CNY"
|
||||
if any(x in blob for x in ("USD", "美金", "美元")):
|
||||
return "USD"
|
||||
return hint or "USD"
|
||||
if any(x in blob for x in ("EUR", "欧元")):
|
||||
return "EUR"
|
||||
if any(x in blob for x in ("JPY", "日元")):
|
||||
return "JPY"
|
||||
if any(x in blob for x in ("GBP", "英镑")):
|
||||
return "GBP"
|
||||
return str(hint or "").strip().upper()
|
||||
|
||||
|
||||
def _parse_fee_rows(text: str) -> list[dict[str, str]]:
|
||||
def _parse_fee_rows(text: str, baseline_currency: str = "") -> list[dict[str, str]]:
|
||||
"""
|
||||
按行抽费用:码头操作费100CNY、报关费改为300CNY。
|
||||
|
||||
只认带「费」的中文名或短英文费用码,避免把工单号当金额。
|
||||
句里无币种时继承 baseline_currency(通常是当前 TMS/主账币种),禁止默认 USD。
|
||||
"""
|
||||
raw = _ascii_amount(text or "")
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
rows: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
@@ -925,10 +951,11 @@ def _parse_fee_rows(text: str) -> list[dict[str, str]]:
|
||||
if name in seen:
|
||||
return
|
||||
seen.add(name)
|
||||
line_ccy = _currency_of(raw, ccy or base_ccy) or base_ccy
|
||||
row = {
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"currency": _currency_of(raw, ccy),
|
||||
"currency": line_ccy,
|
||||
}
|
||||
for key, val in extra.items():
|
||||
if val:
|
||||
@@ -986,6 +1013,7 @@ def extract_product_quote(
|
||||
injected: Optional[dict[str, Any]] = None,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
baseline_currency: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
产品文字报价:先按费用行抽,再退回合计/币别。注入优先。
|
||||
@@ -996,7 +1024,9 @@ def extract_product_quote(
|
||||
陆运集拼:「2-500KG 单价CNY990,燃油附加费率1%,税率6%」走分档单价+费率,
|
||||
禁止把公斤档下限「2」当成燃油附加费金额(WO021)。
|
||||
无费用名时按 business_line/transport_mode 默认陆运费/空运费/海运费,禁止陆运硬套海运费。
|
||||
正文未写币种时继承 baseline_currency(当前 TMS/主账币种),禁止默认 USD(WO107)。
|
||||
"""
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
if isinstance(injected, dict) and (injected.get("total") or injected.get("fee_rows")):
|
||||
out = dict(injected)
|
||||
out.setdefault("source_label", "产品报价")
|
||||
@@ -1008,14 +1038,14 @@ def extract_product_quote(
|
||||
compact = stripped.replace(" ", "").replace(" ", "")
|
||||
if re.fullmatch(r"WO\d{12}", compact, re.I):
|
||||
return {}
|
||||
band = _parse_weight_band_quote(raw)
|
||||
band = _parse_weight_band_quote(raw, baseline_currency=base_ccy)
|
||||
if band:
|
||||
return band
|
||||
# 费率句若仍落到普通费用行,会把「燃油附加费」配上句首公斤数。先剥掉再抽。
|
||||
scrubbed = _strip_rate_and_band_noise(raw)
|
||||
rows = _parse_fee_rows(scrubbed) if scrubbed.strip() else []
|
||||
rows = _parse_fee_rows(scrubbed, baseline_currency=base_ccy) if scrubbed.strip() else []
|
||||
if rows:
|
||||
ccy = rows[0].get("currency") or _currency_of(raw)
|
||||
ccy = rows[0].get("currency") or _currency_of(raw, base_ccy) or base_ccy
|
||||
money_rows = [x for x in rows if "%" not in str(x.get("amount") or "")]
|
||||
total = (money_rows[0] if money_rows else rows[0])["amount"]
|
||||
if money_rows and len({x.get("currency") for x in money_rows}) == 1:
|
||||
@@ -1040,7 +1070,7 @@ def extract_product_quote(
|
||||
amount = _ascii_amount(hit.group(1) or hit.group(2) or "")
|
||||
if not amount:
|
||||
return {}
|
||||
ccy = _currency_of(raw)
|
||||
ccy = _currency_of(raw, base_ccy) or base_ccy
|
||||
default_fee = _default_product_fee_name(business_line, transport_mode)
|
||||
# 句里已经点了费用名(空运费)时,不能退回默认海运费。
|
||||
named = re.search(
|
||||
@@ -1066,6 +1096,7 @@ def extract_product_quote(
|
||||
return out
|
||||
|
||||
|
||||
|
||||
def _format_percent_rate(raw: str) -> str:
|
||||
"""费率展示统一带 %;已有 % 保留。"""
|
||||
text = (raw or "").strip()
|
||||
@@ -1100,13 +1131,15 @@ def _strip_rate_and_band_noise(text: str) -> str:
|
||||
return cleaned
|
||||
|
||||
|
||||
def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
def _parse_weight_band_quote(text: str, baseline_currency: str = "") -> dict[str, Any]:
|
||||
"""
|
||||
陆运集拼分档单价:2-500KG 单价CNY990 + 可选燃油附加费率/税率。
|
||||
|
||||
产出 weightPrices,供主账出单按「2-500KG」表头对档;费率进顶层字段填 Excel。
|
||||
无币种时优先继承 baseline,再退回 CNY(集拼常见人民币报价)。
|
||||
"""
|
||||
raw = _ascii_amount(text or "")
|
||||
base_ccy = str(baseline_currency or "").strip().upper()
|
||||
tiers: list[dict[str, Any]] = []
|
||||
rows: list[dict[str, str]] = []
|
||||
ccy = ""
|
||||
@@ -1116,7 +1149,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
price = _ascii_amount(hit.group("price") or "")
|
||||
if not price:
|
||||
continue
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or "")
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or base_ccy) or base_ccy
|
||||
ccy = ccy or line_ccy
|
||||
label = f"{lo}-{hi}KG"
|
||||
tiers.append({"min": lo, "max": hi, "price": price})
|
||||
@@ -1133,7 +1166,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
price = _ascii_amount(hit.group("price") or "")
|
||||
if not price:
|
||||
continue
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or "")
|
||||
line_ccy = _currency_of(raw, hit.group("ccy") or base_ccy) or base_ccy
|
||||
ccy = ccy or line_ccy
|
||||
label = f"{lo}KG以上"
|
||||
tiers.append({"min": lo, "max": None, "price": price})
|
||||
@@ -1162,7 +1195,7 @@ def _parse_weight_band_quote(text: str) -> dict[str, Any]:
|
||||
# 分档是单价,不能把多档相加冒充费用合计。
|
||||
out: dict[str, Any] = {
|
||||
"total": tiers[0]["price"],
|
||||
"currency": ccy or "CNY",
|
||||
"currency": ccy or base_ccy or "CNY",
|
||||
"source_label": "产品报价",
|
||||
"raw_text": (text or "").strip(),
|
||||
"fee_rows": rows,
|
||||
|
||||
@@ -629,14 +629,19 @@ def apply_air_quote(
|
||||
fields = extract_air_collab_fields(text)
|
||||
if fields:
|
||||
flow.ledger.patch_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
|
||||
view = ticket_view(ticket)
|
||||
current = dict(view.get("quote") or {})
|
||||
from agent.policy.sea_group_ops import _quote_currency_hint
|
||||
|
||||
quote = injected_quote or extract_product_quote(
|
||||
text, business_line="AIR", transport_mode="空运"
|
||||
text,
|
||||
business_line="AIR",
|
||||
transport_mode="空运",
|
||||
baseline_currency=_quote_currency_hint(current),
|
||||
) or {}
|
||||
if not quote:
|
||||
send_group_text(client, chat_id, copy.air_quote_not_recognized())
|
||||
return "quote_empty"
|
||||
view = ticket_view(ticket)
|
||||
current = dict(view.get("quote") or {})
|
||||
if _has_tms_quote(current) and not _is_product_quote(current):
|
||||
_park_air_pending(flow, no, quote, sender_id)
|
||||
send_group_text(client, chat_id, copy.group_ask_tms_or_product())
|
||||
|
||||
@@ -18,6 +18,7 @@ from agent.llm.extract_text import extract_collab_fields, extract_product_quote
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.multi_segments import Segment, group_brief, includes_air, segments_from_payload
|
||||
from agent.policy.sea_group_ops import (
|
||||
_quote_currency_hint,
|
||||
apply_group_deal,
|
||||
group_client_of,
|
||||
is_sales,
|
||||
@@ -152,6 +153,12 @@ def handle_multi_inbound(
|
||||
ticket = _reload(flow, no) or ticket
|
||||
segments = load_segments(ticket)
|
||||
target = next((seg for seg in segments if seg.mode == mode), target)
|
||||
# 段报价币种优先;否则用工单顶层 quote(产品未写币种时继承 TMS)
|
||||
baseline = ""
|
||||
if target is not None:
|
||||
baseline = _quote_currency_hint((segment_quotes(ticket) or {}).get(target.mode) or {})
|
||||
if not baseline:
|
||||
baseline = _quote_currency_hint(dict(ticket_view(ticket).get("quote") or {}))
|
||||
quote = extract_product_quote(
|
||||
text or "",
|
||||
injected=injected_quote,
|
||||
@@ -160,6 +167,7 @@ def handle_multi_inbound(
|
||||
"陆运" if str(mode or "").upper() == "LAND"
|
||||
else ("空运" if str(mode or "").upper() == "AIR" else "海运")
|
||||
),
|
||||
baseline_currency=baseline,
|
||||
)
|
||||
if not quote:
|
||||
if fields:
|
||||
|
||||
@@ -930,18 +930,19 @@ def apply_product_quote(
|
||||
merged.update(fields)
|
||||
sess.collab_facts = merged
|
||||
line = str(view.get("business_line") or view.get("businessLine") or "").strip().upper()
|
||||
base = _current_quote(view, sess)
|
||||
quote = extract_product_quote(
|
||||
text,
|
||||
injected=injected_quote,
|
||||
business_line=line,
|
||||
transport_mode="陆运" if line == "LAND" else ("海运" if line == "SEA" else ""),
|
||||
baseline_currency=_quote_currency_hint(base),
|
||||
)
|
||||
if not quote:
|
||||
logger.info("产品报价未识别 wo=%s text=%s", no, (text or "")[:200])
|
||||
send_group_text(client, chat_id, copy.group_quote_not_recognized())
|
||||
return "quote_not_recognized"
|
||||
quote = _attach_land_weight_band_segment(quote, view)
|
||||
base = _current_quote(view, sess)
|
||||
if _has_tms_quote(base):
|
||||
_park_pending_quote(no, quote, sess, flow)
|
||||
send_group_text(client, chat_id, copy.group_ask_tms_or_product())
|
||||
@@ -1405,6 +1406,53 @@ def _current_quote(view: dict[str, Any], sess: Any) -> dict[str, Any]:
|
||||
return base
|
||||
|
||||
|
||||
def _quote_currency_hint(quote: dict[str, Any] | None) -> str:
|
||||
"""
|
||||
当前报价币种提示:顶层 currency → 金额前缀 → 费用行/线路段。
|
||||
|
||||
产品改价未写币种时继承此值,禁止默认 USD(WO202610090107 中港 HKD)。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
for key in ("currency", "currencyCode"):
|
||||
code = str(src.get(key) or "").strip().upper()
|
||||
if code:
|
||||
return code
|
||||
for key in ("cost", "total", "freight", "land_freight"):
|
||||
raw = str(src.get(key) or "").strip().upper()
|
||||
for prefix in ("HKD", "CNY", "USD", "EUR", "JPY", "GBP", "RMB"):
|
||||
if raw.startswith(prefix):
|
||||
return "CNY" if prefix == "RMB" else prefix
|
||||
for row in _quote_fee_rows(src):
|
||||
code = str(row.get("currency") or row.get("currencyCode") or "").strip().upper()
|
||||
if code:
|
||||
return code
|
||||
for key in ("landSegments", "route_rows", "selected_land_route"):
|
||||
raw = src.get(key)
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
try:
|
||||
import json
|
||||
|
||||
raw = json.loads(raw)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(raw, dict):
|
||||
legs = raw.get("legs") if isinstance(raw.get("legs"), list) else [raw]
|
||||
for leg in legs:
|
||||
if not isinstance(leg, dict):
|
||||
continue
|
||||
code = str(leg.get("currency") or "").strip().upper()
|
||||
if code:
|
||||
return code
|
||||
elif isinstance(raw, list):
|
||||
for leg in raw:
|
||||
if not isinstance(leg, dict):
|
||||
continue
|
||||
code = str(leg.get("currency") or "").strip().upper()
|
||||
if code:
|
||||
return code
|
||||
return ""
|
||||
|
||||
|
||||
def _has_tms_quote(quote: dict[str, Any] | None) -> bool:
|
||||
"""当前价来自 TMS 才要问;产品价、销售调价不再替用户选。"""
|
||||
src = dict(quote or {})
|
||||
|
||||
@@ -374,6 +374,48 @@ class LandGroupCollabTests(unittest.TestCase):
|
||||
air = extract_product_quote("价格为1RMB/KG", business_line="AIR")
|
||||
self.assertEqual(str((air.get("fee_rows") or [{}])[0].get("name") or ""), "空运费")
|
||||
|
||||
def test_land_adjust_without_currency_inherits_tms_hkd(self) -> None:
|
||||
"""WO202610090107:TMS 港币,产品只写「运输费调整为1100」时继承 HKD,禁止默认 USD。"""
|
||||
from agent.llm.extract_text import extract_product_quote
|
||||
from agent.policy.sea_group_ops import _quote_currency_hint, merge_quote_fees
|
||||
|
||||
tms = {
|
||||
"currency": "HKD",
|
||||
"cost": "HKD 1000",
|
||||
"total": "1000",
|
||||
"source": "TMS",
|
||||
"fee_lines": [
|
||||
{
|
||||
"name": "运输费",
|
||||
"amount": "1000",
|
||||
"currency": "HKD",
|
||||
}
|
||||
],
|
||||
"landSegments": [
|
||||
{
|
||||
"origin": "广东省-深圳市-福田区",
|
||||
"destination": "香港特别行政区",
|
||||
"currency": "HKD",
|
||||
"costPrice": "1000",
|
||||
}
|
||||
],
|
||||
}
|
||||
self.assertEqual(_quote_currency_hint(tms), "HKD")
|
||||
# 旧行为:无 baseline 会落到 USD
|
||||
legacy = extract_product_quote("运输费调整为1100", business_line="LAND")
|
||||
self.assertNotEqual(str(legacy.get("currency") or ""), "USD")
|
||||
quote = extract_product_quote(
|
||||
"运输费调整为1100",
|
||||
business_line="LAND",
|
||||
baseline_currency=_quote_currency_hint(tms),
|
||||
)
|
||||
self.assertEqual(str(quote.get("currency") or ""), "HKD")
|
||||
self.assertEqual(str((quote.get("fee_lines") or [{}])[0].get("currency") or ""), "HKD")
|
||||
self.assertEqual(str((quote.get("fee_lines") or [{}])[0].get("amount") or ""), "1100")
|
||||
merged = merge_quote_fees(tms, quote)
|
||||
self.assertEqual(str((merged.get("fee_lines") or [{}])[0].get("currency") or ""), "HKD")
|
||||
self.assertEqual(str(merged.get("currency") or ""), "HKD")
|
||||
|
||||
def test_land_requote_replaces_wrong_sea_fee_name(self) -> None:
|
||||
"""旧陆运产品价误落海运费时,重报陆运费应盖掉而不是并排两行。"""
|
||||
from agent.policy.sea_group_ops import _align_wrong_default_freight, merge_quote_fees
|
||||
|
||||
Reference in New Issue
Block a user