|
@@ -36,11 +36,14 @@ class OutputPresenter:
|
|
|
DESTINATION_TRAILER_TOOLS = frozenset(('query_destination_trailer_list',))
|
|
DESTINATION_TRAILER_TOOLS = frozenset(('query_destination_trailer_list',))
|
|
|
ORDER_ABNORMAL_TOOLS = frozenset(('query_order_abnormal_list',))
|
|
ORDER_ABNORMAL_TOOLS = frozenset(('query_order_abnormal_list',))
|
|
|
RECEIVE_VOLUME_TOOLS = frozenset(('query_receive_volume_list',))
|
|
RECEIVE_VOLUME_TOOLS = frozenset(('query_receive_volume_list',))
|
|
|
|
|
+ GOODS_INFO_TOOLS = frozenset(('query_goods_info',))
|
|
|
|
|
+ GOODS_TAX_TOOLS = frozenset(('trial_goods_tax',))
|
|
|
CONTAINER_TIMELINESS_TOOLS = frozenset(('query_container_timeliness_list',))
|
|
CONTAINER_TIMELINESS_TOOLS = frozenset(('query_container_timeliness_list',))
|
|
|
HEADHAUL_DOCUMENT_TOOLS = frozenset(('query_headhaul_document_list',))
|
|
HEADHAUL_DOCUMENT_TOOLS = frozenset(('query_headhaul_document_list',))
|
|
|
HEADHAUL_PREPARE_TOOLS = frozenset(('prepare_headhaul_document_upload',))
|
|
HEADHAUL_PREPARE_TOOLS = frozenset(('prepare_headhaul_document_upload',))
|
|
|
HEADHAUL_SAVE_TOOLS = frozenset(('save_headhaul_document',))
|
|
HEADHAUL_SAVE_TOOLS = frozenset(('save_headhaul_document',))
|
|
|
HEADHAUL_DELETE_TOOLS = frozenset(('delete_headhaul_document',))
|
|
HEADHAUL_DELETE_TOOLS = frozenset(('delete_headhaul_document',))
|
|
|
|
|
+ FREIGHTOWER_TRACK_TOOLS = frozenset(('query_freightower_track',))
|
|
|
DETAIL_TOOLS = frozenset(('query_outbound_detail',))
|
|
DETAIL_TOOLS = frozenset(('query_outbound_detail',))
|
|
|
ORDER_DETAIL_TOOLS = frozenset(('query_order_detail',))
|
|
ORDER_DETAIL_TOOLS = frozenset(('query_order_detail',))
|
|
|
OPTION_TOOLS = frozenset((
|
|
OPTION_TOOLS = frozenset((
|
|
@@ -69,11 +72,14 @@ class OutputPresenter:
|
|
|
| DESTINATION_TRAILER_TOOLS
|
|
| DESTINATION_TRAILER_TOOLS
|
|
|
| ORDER_ABNORMAL_TOOLS
|
|
| ORDER_ABNORMAL_TOOLS
|
|
|
| RECEIVE_VOLUME_TOOLS
|
|
| RECEIVE_VOLUME_TOOLS
|
|
|
|
|
+ | GOODS_INFO_TOOLS
|
|
|
|
|
+ | GOODS_TAX_TOOLS
|
|
|
| CONTAINER_TIMELINESS_TOOLS
|
|
| CONTAINER_TIMELINESS_TOOLS
|
|
|
| HEADHAUL_DOCUMENT_TOOLS
|
|
| HEADHAUL_DOCUMENT_TOOLS
|
|
|
| HEADHAUL_PREPARE_TOOLS
|
|
| HEADHAUL_PREPARE_TOOLS
|
|
|
| HEADHAUL_SAVE_TOOLS
|
|
| HEADHAUL_SAVE_TOOLS
|
|
|
| HEADHAUL_DELETE_TOOLS
|
|
| HEADHAUL_DELETE_TOOLS
|
|
|
|
|
+ | FREIGHTOWER_TRACK_TOOLS
|
|
|
| OPTION_TOOLS | EXPORT_TOOLS | TASK_TOOLS
|
|
| OPTION_TOOLS | EXPORT_TOOLS | TASK_TOOLS
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -575,6 +581,20 @@ class OutputPresenter:
|
|
|
ORDER_ABNORMAL_STATUSES = frozenset((
|
|
ORDER_ABNORMAL_STATUSES = frozenset((
|
|
|
'待处理', '放行待审核', '包裹放行', '退件待审核', '退件驳回', '已退件',
|
|
'待处理', '放行待审核', '包裹放行', '退件待审核', '退件驳回', '已退件',
|
|
|
))
|
|
))
|
|
|
|
|
+ GOODS_INFO_COLUMNS = (
|
|
|
|
|
+ ('sku', 'SKU'), ('departure_country', '起运国'),
|
|
|
|
|
+ ('destination_country', '目的国'), ('goods_attr', '商品属性'),
|
|
|
|
|
+ ('cn_name', '中文名称'), ('en_name', '英文名称'),
|
|
|
|
|
+ ('declaration_hscode', '报关编码'), ('clearance_hscode', '清关编码'),
|
|
|
|
|
+ ('tax_rate', '税率'), ('is_inspection_fee', '木制/竹制品商检费'),
|
|
|
|
|
+ ('is_fda_declaration', 'FDA申报'), ('product_surcharge', '产品附加费'),
|
|
|
|
|
+ ('tax_surcharge', '关税附加费'), ('is_epa_declaration', 'EPA申报'),
|
|
|
|
|
+ ('is_act_declaration', 'LACY ACT申报'), ('is_dot_declaration', 'DOT申报'),
|
|
|
|
|
+ ('is_cpsc_declaration', 'CPSC申报'), ('brand_type', '品牌类型'),
|
|
|
|
|
+ ('brand', '品牌'), ('goods_type', '型号'),
|
|
|
|
|
+ ('material_cn', '材质(CN)'), ('material_en', '材质(EN)'),
|
|
|
|
|
+ ('purpose_cn', '用途(CN)'), ('purpose_en', '用途(EN)'),
|
|
|
|
|
+ )
|
|
|
RECEIVE_VOLUME_COLUMNS = (
|
|
RECEIVE_VOLUME_COLUMNS = (
|
|
|
('order_number', '订单号'), ('customer_name', '客户名称'),
|
|
('order_number', '订单号'), ('customer_name', '客户名称'),
|
|
|
('packing_type', '货物类型'), ('inbound_volume', '入库体积'),
|
|
('packing_type', '货物类型'), ('inbound_volume', '入库体积'),
|
|
@@ -852,6 +872,10 @@ class OutputPresenter:
|
|
|
'query_export_task': {
|
|
'query_export_task': {
|
|
|
'task_ref': '导出任务引用',
|
|
'task_ref': '导出任务引用',
|
|
|
},
|
|
},
|
|
|
|
|
+ 'query_freightower_track': {
|
|
|
|
|
+ 'bl_number': '提单号',
|
|
|
|
|
+ 'container_code': '柜号',
|
|
|
|
|
+ },
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ERROR_MESSAGES = {
|
|
ERROR_MESSAGES = {
|
|
@@ -867,6 +891,12 @@ class OutputPresenter:
|
|
|
'MCP_1601': '没有可导出的数据',
|
|
'MCP_1601': '没有可导出的数据',
|
|
|
'MCP_9001': '工具调用失败,请稍后重试',
|
|
'MCP_9001': '工具调用失败,请稍后重试',
|
|
|
}
|
|
}
|
|
|
|
|
+ TAX_TRIAL_MESSAGES = frozenset((
|
|
|
|
|
+ '未找到已审核商品',
|
|
|
|
|
+ '匹配到多条商品,请核对起运国和目的国',
|
|
|
|
|
+ '该商品没有应收清关编码,无法试算',
|
|
|
|
|
+ '关税试算失败,请稍后重试',
|
|
|
|
|
+ ))
|
|
|
|
|
|
|
|
NON_RETRYABLE_CODES = frozenset((
|
|
NON_RETRYABLE_CODES = frozenset((
|
|
|
'MCP_1101',
|
|
'MCP_1101',
|
|
@@ -957,6 +987,12 @@ class OutputPresenter:
|
|
|
return self._present_receive_volume_list(
|
|
return self._present_receive_volume_list(
|
|
|
data, tool_result.get('meta'), meta
|
|
data, tool_result.get('meta'), meta
|
|
|
)
|
|
)
|
|
|
|
|
+ if tool_name in self.GOODS_INFO_TOOLS:
|
|
|
|
|
+ return self._present_goods_info(
|
|
|
|
|
+ data, tool_result.get('meta'), meta
|
|
|
|
|
+ )
|
|
|
|
|
+ if tool_name in self.GOODS_TAX_TOOLS:
|
|
|
|
|
+ return self._present_goods_tax(data, meta)
|
|
|
if tool_name in self.CONTAINER_TIMELINESS_TOOLS:
|
|
if tool_name in self.CONTAINER_TIMELINESS_TOOLS:
|
|
|
return self._present_container_timeliness_list(
|
|
return self._present_container_timeliness_list(
|
|
|
data, tool_result.get('meta'), meta
|
|
data, tool_result.get('meta'), meta
|
|
@@ -971,6 +1007,8 @@ class OutputPresenter:
|
|
|
return self._present_headhaul_save(data, meta)
|
|
return self._present_headhaul_save(data, meta)
|
|
|
if tool_name in self.HEADHAUL_DELETE_TOOLS:
|
|
if tool_name in self.HEADHAUL_DELETE_TOOLS:
|
|
|
return self._present_headhaul_delete(data, meta)
|
|
return self._present_headhaul_delete(data, meta)
|
|
|
|
|
+ if tool_name in self.FREIGHTOWER_TRACK_TOOLS:
|
|
|
|
|
+ return self._present_freightower_track(data, meta)
|
|
|
if tool_name in self.TABLE_TOOLS:
|
|
if tool_name in self.TABLE_TOOLS:
|
|
|
return self._present_table(
|
|
return self._present_table(
|
|
|
tool_name,
|
|
tool_name,
|
|
@@ -1834,6 +1872,79 @@ class OutputPresenter:
|
|
|
}
|
|
}
|
|
|
return self._success_result(content, self._render_table(content), meta)
|
|
return self._success_result(content, self._render_table(content), meta)
|
|
|
|
|
|
|
|
|
|
+ def _present_goods_tax(self, data, meta):
|
|
|
|
|
+ expected = {
|
|
|
|
|
+ 'estimated_tax', 'sku', 'departure_country', 'destination_country',
|
|
|
|
|
+ }
|
|
|
|
|
+ if set(data) != expected:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ amount = data.get('estimated_tax')
|
|
|
|
|
+ sku = data.get('sku')
|
|
|
|
|
+ departure = data.get('departure_country')
|
|
|
|
|
+ destination = data.get('destination_country')
|
|
|
|
|
+ if (
|
|
|
|
|
+ isinstance(amount, bool) or not isinstance(amount, (int, float))
|
|
|
|
|
+ or not math.isfinite(float(amount))
|
|
|
|
|
+ or not isinstance(sku, str) or not sku.strip()
|
|
|
|
|
+ or not self._country_code(departure)
|
|
|
|
|
+ or not self._country_code(destination)
|
|
|
|
|
+ ):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ content = {
|
|
|
|
|
+ 'estimated_tax': amount,
|
|
|
|
|
+ 'sku': sku,
|
|
|
|
|
+ 'departure_country': departure,
|
|
|
|
|
+ 'destination_country': destination,
|
|
|
|
|
+ }
|
|
|
|
|
+ text = '预计关税金额: {0}'.format(amount)
|
|
|
|
|
+ return self._success_result(content, text, meta)
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _country_code(value):
|
|
|
|
|
+ return (
|
|
|
|
|
+ isinstance(value, str)
|
|
|
|
|
+ and value.isalpha()
|
|
|
|
|
+ and value.isupper()
|
|
|
|
|
+ and 2 <= len(value) <= 3
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _present_goods_info(self, data, raw_meta, meta):
|
|
|
|
|
+ if set(data) != {'columns', 'records'}:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ expected = list(self.GOODS_INFO_COLUMNS)
|
|
|
|
|
+ pagination = self._customer_pagination(raw_meta)
|
|
|
|
|
+ columns = data.get('columns')
|
|
|
|
|
+ records = data.get('records')
|
|
|
|
|
+ if (
|
|
|
|
|
+ not isinstance(columns, list)
|
|
|
|
|
+ or not isinstance(records, list)
|
|
|
|
|
+ or len(columns) != len(expected)
|
|
|
|
|
+ or pagination is None
|
|
|
|
|
+ ):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ for column, (key, name) in zip(columns, expected):
|
|
|
|
|
+ if column != {'key': key, 'name': name}:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ rows = []
|
|
|
|
|
+ expected_keys = [key for key, _ in expected]
|
|
|
|
|
+ for record in records:
|
|
|
|
|
+ if not isinstance(record, dict) or set(record) != set(expected_keys):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ row = []
|
|
|
|
|
+ for key in expected_keys:
|
|
|
|
|
+ value = record[key]
|
|
|
|
|
+ if not isinstance(value, str):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ row.append(value)
|
|
|
|
|
+ rows.append(row)
|
|
|
|
|
+ headers = [{'label': name} for _, name in expected]
|
|
|
|
|
+ content = {
|
|
|
|
|
+ 'headers': headers,
|
|
|
|
|
+ 'rows': rows,
|
|
|
|
|
+ 'pagination': pagination,
|
|
|
|
|
+ }
|
|
|
|
|
+ return self._success_result(content, self._render_table(content), meta)
|
|
|
|
|
+
|
|
|
def _present_receive_volume_list(self, data, raw_meta, meta):
|
|
def _present_receive_volume_list(self, data, raw_meta, meta):
|
|
|
if set(data) != {'columns', 'records'}:
|
|
if set(data) != {'columns', 'records'}:
|
|
|
return self._format_error(meta)
|
|
return self._format_error(meta)
|
|
@@ -2389,6 +2500,11 @@ class OutputPresenter:
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
def _business_error(self, tool_name, code, raw_message, meta):
|
|
def _business_error(self, tool_name, code, raw_message, meta):
|
|
|
|
|
+ message = str(raw_message or '').strip()
|
|
|
|
|
+ if tool_name == 'trial_goods_tax' and message in self.TAX_TRIAL_MESSAGES:
|
|
|
|
|
+ safe_code = code if code in self.ERROR_MESSAGES else 'MCP_9001'
|
|
|
|
|
+ retryable = safe_code not in self.NON_RETRYABLE_CODES
|
|
|
|
|
+ return self._error_result(safe_code, message, retryable, meta)
|
|
|
if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
|
|
if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
|
|
|
logger.warning(
|
|
logger.warning(
|
|
|
'Unknown MCP business error code',
|
|
'Unknown MCP business error code',
|
|
@@ -2404,6 +2520,19 @@ class OutputPresenter:
|
|
|
if code == 'MCP_1401':
|
|
if code == 'MCP_1401':
|
|
|
label = self._find_field_label(tool_name, raw_message)
|
|
label = self._find_field_label(tool_name, raw_message)
|
|
|
message = self._parameter_message(label)
|
|
message = self._parameter_message(label)
|
|
|
|
|
+ elif tool_name in self.FREIGHTOWER_TRACK_TOOLS:
|
|
|
|
|
+ allowed = str(raw_message or '').strip()
|
|
|
|
|
+ if allowed not in (
|
|
|
|
|
+ '暂时没有飞驼轨迹',
|
|
|
|
|
+ '未找到本公司排舱单',
|
|
|
|
|
+ '匹配到多票排舱单,请同时提供提单号和柜号',
|
|
|
|
|
+ '未找到飞驼轨迹订单',
|
|
|
|
|
+ '飞驼查询超时',
|
|
|
|
|
+ '飞驼查询失败',
|
|
|
|
|
+ ):
|
|
|
|
|
+ message = self.ERROR_MESSAGES.get(code, self.ERROR_MESSAGES['MCP_9001'])
|
|
|
|
|
+ else:
|
|
|
|
|
+ message = allowed
|
|
|
else:
|
|
else:
|
|
|
message = self.ERROR_MESSAGES[code]
|
|
message = self.ERROR_MESSAGES[code]
|
|
|
retryable = code not in self.NON_RETRYABLE_CODES
|
|
retryable = code not in self.NON_RETRYABLE_CODES
|
|
@@ -2487,6 +2616,98 @@ class OutputPresenter:
|
|
|
)
|
|
)
|
|
|
return '\n'.join(lines)
|
|
return '\n'.join(lines)
|
|
|
|
|
|
|
|
|
|
+ return self._success_result(content, '\n'.join(lines), meta)
|
|
|
|
|
+
|
|
|
|
|
+ def _present_freightower_track(self, data, meta):
|
|
|
|
|
+ if set(data) != {'schedule', 'ports', 'tracks'}:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ schedule = data.get('schedule')
|
|
|
|
|
+ ports = data.get('ports')
|
|
|
|
|
+ tracks = data.get('tracks')
|
|
|
|
|
+ schedule_keys = (
|
|
|
|
|
+ 'bl_number', 'container_code', 'vessel', 'voyage', 'etd', 'eta',
|
|
|
|
|
+ )
|
|
|
|
|
+ if (
|
|
|
|
|
+ not isinstance(schedule, dict)
|
|
|
|
|
+ or set(schedule) != set(schedule_keys)
|
|
|
|
|
+ or not isinstance(ports, list)
|
|
|
|
|
+ or not isinstance(tracks, list)
|
|
|
|
|
+ ):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ labels = {
|
|
|
|
|
+ 'bl_number': '提单号',
|
|
|
|
|
+ 'container_code': '柜号',
|
|
|
|
|
+ 'vessel': '船名',
|
|
|
|
|
+ 'voyage': '航次',
|
|
|
|
|
+ 'etd': '预计离港',
|
|
|
|
|
+ 'eta': '预计到达',
|
|
|
|
|
+ }
|
|
|
|
|
+ safe_schedule = {}
|
|
|
|
|
+ for key in schedule_keys:
|
|
|
|
|
+ value = schedule.get(key)
|
|
|
|
|
+ if not isinstance(value, str):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ safe_schedule[labels[key]] = value
|
|
|
|
|
+ safe_ports = []
|
|
|
|
|
+ for port in ports:
|
|
|
|
|
+ if not isinstance(port, dict) or set(port) != {
|
|
|
|
|
+ 'place_type', 'port_name', 'eta', 'ata',
|
|
|
|
|
+ }:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ if port['place_type'] not in ('中转港', '目的港', '交货地'):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ for key in ('port_name', 'eta', 'ata'):
|
|
|
|
|
+ if not isinstance(port[key], str):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ safe_ports.append({
|
|
|
|
|
+ '地点类型': port['place_type'],
|
|
|
|
|
+ '港口': port['port_name'],
|
|
|
|
|
+ '预计到达': port['eta'],
|
|
|
|
|
+ '实际到达': port['ata'],
|
|
|
|
|
+ })
|
|
|
|
|
+ safe_tracks = []
|
|
|
|
|
+ for track in tracks:
|
|
|
|
|
+ if not isinstance(track, dict) or set(track) != {
|
|
|
|
|
+ 'time', 'location', 'content', 'event_code',
|
|
|
|
|
+ }:
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ for key in ('time', 'location', 'content', 'event_code'):
|
|
|
|
|
+ if not isinstance(track[key], str):
|
|
|
|
|
+ return self._format_error(meta)
|
|
|
|
|
+ safe_tracks.append({
|
|
|
|
|
+ '时间': track['time'],
|
|
|
|
|
+ '地点': track['location'],
|
|
|
|
|
+ '内容': track['content'],
|
|
|
|
|
+ '状态代码': track['event_code'],
|
|
|
|
|
+ })
|
|
|
|
|
+ content = {
|
|
|
|
|
+ '船期': safe_schedule,
|
|
|
|
|
+ '港口': safe_ports,
|
|
|
|
|
+ '轨迹': safe_tracks,
|
|
|
|
|
+ }
|
|
|
|
|
+ lines = ['飞驼船期']
|
|
|
|
|
+ for label, value in safe_schedule.items():
|
|
|
|
|
+ lines.append('- {0}: {1}'.format(label, value))
|
|
|
|
|
+ lines.append('港口')
|
|
|
|
|
+ if not safe_ports:
|
|
|
|
|
+ lines.append('- 无')
|
|
|
|
|
+ for port in safe_ports:
|
|
|
|
|
+ lines.append(
|
|
|
|
|
+ '- {0} {1} 预计到达 {2} 实际到达 {3}'.format(
|
|
|
|
|
+ port['地点类型'], port['港口'], port['预计到达'], port['实际到达'],
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ lines.append('轨迹')
|
|
|
|
|
+ if not safe_tracks:
|
|
|
|
|
+ lines.append('- 无')
|
|
|
|
|
+ for track in safe_tracks:
|
|
|
|
|
+ lines.append(
|
|
|
|
|
+ '- {0} {1} {2} {3}'.format(
|
|
|
|
|
+ track['时间'], track['地点'], track['内容'], track['状态代码'],
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ return self._success_result(content, '\n'.join(lines), meta)
|
|
|
|
|
+
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def _success_result(content, text, meta):
|
|
def _success_result(content, text, meta):
|
|
|
return {
|
|
return {
|