output_presenter.py 134 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195
  1. import json
  2. import logging
  3. import math
  4. from urllib.parse import urlparse
  5. from constants import DEVICE_INVALID_MESSAGE
  6. logger = logging.getLogger(__name__)
  7. # Builds AI-safe MCP results for the explicitly supported tool set.
  8. class OutputPresenter:
  9. TABLE_TOOLS = frozenset((
  10. 'query_order_exact',
  11. 'query_track',
  12. 'query_customs_declaration_files',
  13. 'query_outbound_list',
  14. 'query_customer_list',
  15. 'query_remote_address_list',
  16. 'query_fuel_rate_list',
  17. 'save_fuel_rate',
  18. 'enable_fuel_rate',
  19. ))
  20. FUEL_RATE_TOOLS = frozenset((
  21. 'query_fuel_rate_list', 'save_fuel_rate', 'enable_fuel_rate',
  22. ))
  23. PAYMENT_FOLLOWUP_TOOLS = frozenset(('query_customer_payment_followup',))
  24. PAYMENT_DETAIL_TOOLS = frozenset((
  25. 'query_customer_unverified_bill_details',
  26. ))
  27. PAYMENT_RECORD_TOOLS = frozenset(('query_customer_payment_records',))
  28. RECEIVABLE_COST_TOOLS = frozenset((
  29. 'query_order_receivable_cost_details',
  30. ))
  31. RECEIVABLE_LIST_TOOLS = frozenset(('query_receivable_cost_list',))
  32. RECEIVABLE_FILTER_TOOLS = frozenset((
  33. 'list_receivable_cost_filter_options',
  34. ))
  35. PAYABLE_COST_TOOLS = frozenset(('query_payable_cost_list',))
  36. PAYABLE_FILTER_TOOLS = frozenset(('list_payable_cost_filter_options',))
  37. PAYABLE_EXPORT_TOOLS = frozenset(('export_payable_cost_list',))
  38. DESTINATION_TRAILER_TOOLS = frozenset(('query_destination_trailer_list',))
  39. ARRANGE_TRAILER_TOOLS = frozenset(('arrange_destination_trailer',))
  40. ORDER_ABNORMAL_TOOLS = frozenset(('query_order_abnormal_list',))
  41. RECEIVE_VOLUME_TOOLS = frozenset(('query_receive_volume_list',))
  42. GOODS_INFO_TOOLS = frozenset(('query_goods_info',))
  43. GOODS_TAX_TOOLS = frozenset(('trial_goods_tax',))
  44. CONTAINER_TIMELINESS_TOOLS = frozenset(('query_container_timeliness_list',))
  45. HEADHAUL_DOCUMENT_TOOLS = frozenset(('query_headhaul_document_list',))
  46. REMOTE_ADDRESS_MAINTAIN_TOOLS = frozenset(('maintain_remote_address',))
  47. REMOTE_ADDRESS_SAVE_TOOLS = frozenset(('save_remote_address',))
  48. HEADHAUL_PREPARE_TOOLS = frozenset(('prepare_headhaul_document_upload',))
  49. HEADHAUL_SAVE_TOOLS = frozenset(('save_headhaul_document',))
  50. HEADHAUL_DELETE_TOOLS = frozenset(('delete_headhaul_document',))
  51. INBOUND_RECEIPT_TOOLS = frozenset(('list_inbound_receipt_templates',))
  52. FREIGHTOWER_TRACK_TOOLS = frozenset(('query_freightower_track',))
  53. DETAIL_TOOLS = frozenset(('query_outbound_detail',))
  54. ORDER_DETAIL_TOOLS = frozenset(('query_order_detail',))
  55. OPTION_TOOLS = frozenset((
  56. 'list_order_filter_options',
  57. 'list_outbound_filter_options',
  58. 'list_pending_outbound_export_filter_options',
  59. 'list_customer_filter_options',
  60. 'list_destination_trailer_filter_options',
  61. 'list_order_abnormal_filter_options',
  62. 'list_receive_volume_filter_options',
  63. 'list_headhaul_document_filter_options',
  64. ))
  65. EXPORT_TOOLS = frozenset((
  66. 'export_pending_outbound_orders',
  67. 'export_out_of_province_port_data',
  68. 'export_receivable_cost_list',
  69. 'export_pallet_data',
  70. 'export_container_timeliness_report',
  71. 'export_inbound_receipt',
  72. ))
  73. TASK_TOOLS = frozenset(('query_export_task',))
  74. SAFE_TOOLS = (
  75. TABLE_TOOLS | DETAIL_TOOLS | ORDER_DETAIL_TOOLS | PAYMENT_FOLLOWUP_TOOLS
  76. | PAYMENT_DETAIL_TOOLS | PAYMENT_RECORD_TOOLS | RECEIVABLE_COST_TOOLS
  77. | RECEIVABLE_LIST_TOOLS | RECEIVABLE_FILTER_TOOLS
  78. | PAYABLE_COST_TOOLS | PAYABLE_FILTER_TOOLS | PAYABLE_EXPORT_TOOLS
  79. | DESTINATION_TRAILER_TOOLS
  80. | ARRANGE_TRAILER_TOOLS
  81. | ORDER_ABNORMAL_TOOLS
  82. | RECEIVE_VOLUME_TOOLS
  83. | GOODS_INFO_TOOLS
  84. | GOODS_TAX_TOOLS
  85. | CONTAINER_TIMELINESS_TOOLS
  86. | HEADHAUL_DOCUMENT_TOOLS
  87. | REMOTE_ADDRESS_MAINTAIN_TOOLS
  88. | REMOTE_ADDRESS_SAVE_TOOLS
  89. | HEADHAUL_PREPARE_TOOLS
  90. | HEADHAUL_SAVE_TOOLS
  91. | HEADHAUL_DELETE_TOOLS
  92. | INBOUND_RECEIPT_TOOLS
  93. | FREIGHTOWER_TRACK_TOOLS
  94. | OPTION_TOOLS | EXPORT_TOOLS | TASK_TOOLS
  95. )
  96. ORDER_DETAIL_SECTIONS = {
  97. 'overview': '订单概览',
  98. 'packages': '箱单信息',
  99. 'package_items': '箱单商品',
  100. 'dw_auth': 'DW授权信息',
  101. 'attachments': '附件信息',
  102. 'inbound': '入库信息',
  103. 'checks': '查验信息',
  104. 'tracks': '订单轨迹',
  105. 'operation_logs': '操作日志',
  106. 'cost_logs': '应收与结算日志',
  107. 'delivery': '派送信息',
  108. 'all': '全部',
  109. }
  110. ORDER_DETAIL_FIELDS = {
  111. 'overview': {
  112. 'order_info': {
  113. 'order_number': '订单号', 'reference_number': '客户参考号',
  114. 'customer_name': '客户名称', 'departure_country': '起运国',
  115. 'destination_country': '目的国', 'package_method': '包装类型',
  116. 'product_name': '物流产品', 'forecast_pwv': '预报件重体',
  117. 'inbound_pwv': '入库件重体',
  118. 'receivable_charge_weight': '应收计费重/体积',
  119. 'settlement_charge_weight': '结算计费重/体积',
  120. 'declaration_type': '报关类型', 'pre_recording': '是否需要预录单',
  121. 'idcard_name': '企业/个人姓名',
  122. 'idcard_number': '社会信用代码/身份证号',
  123. 'outbound_number': '排舱单号', 'container_mode': '直送柜/拼柜',
  124. 'track_map_available': '是否可查看轨迹地图', 'insurance': '购买保险',
  125. 'delivery_way': '派送方式', 'return_shipping_surcharge': '退运附加',
  126. 'work_order_count': '工单数量', 'bill_remark': '账单备注',
  127. 'remark': '订单备注',
  128. },
  129. 'freight_info': {
  130. 'estimated_inbound_time': '预计入库时间', 'cargo_type': '货物类型',
  131. 'pickup_way': '交货方式', 'warehouse': '交货仓库',
  132. 'pickup_address': '提货地址', 'pickup_contact': '提货联系人',
  133. 'pickup_phone': '提货联系电话', 'delivery_type': '派送类型',
  134. 'delivery_address': '派送地址', 'delivery_remark': '派送备注',
  135. 'oversea_warehouse': '海外仓',
  136. },
  137. 'package_summary': {
  138. 'box_count': '箱数', 'total_weight': '重量', 'weight_unit': '重量单位',
  139. 'total_volume': '体积', 'sku_count': 'SKU数量',
  140. 'clearance_total_price': '清关总价', 'clearance_currency': '清关币种',
  141. 'purchase_total_price': '采购总价', 'purchase_currency': '采购币种',
  142. },
  143. 'trader_info': {
  144. 'exporter': '出口商', 'importer': '进口商',
  145. 'coo_required': '是否做产地证',
  146. },
  147. },
  148. 'packages': {
  149. 'shipment_id': 'SHIPMENT ID', 'reference_id': 'REFERENCE ID',
  150. 'sku_summary': 'SKU', 'product_name_summary': '商品名',
  151. 'box_quantity': '箱数', 'box_length_cm': '单箱长(CM)',
  152. 'box_width_cm': '单箱宽(CM)', 'box_height_cm': '单箱高(CM)',
  153. 'gross_weight_kg': '单箱毛重量(KG)', 'net_weight_kg': '单箱净重量(KG)',
  154. 'is_over_length': '长度是否超限', 'is_over_width': '宽度是否超限',
  155. 'is_over_height': '高度是否超限', 'is_over_weight': '重量是否超限',
  156. },
  157. 'package_items': {
  158. 'shipment_id': 'SHIPMENT ID', 'reference_id': 'REFERENCE ID',
  159. 'box_specification': '箱规', 'box_specification_unit': '箱规单位',
  160. 'package_gross_weight_kg': '箱单单箱毛重(KG)',
  161. 'package_net_weight_kg': '箱单单箱净重(KG)',
  162. 'inspection_result': '查验结果', 'exception_reason': '异常原因',
  163. 'inspection_photo_count': '查验图片数量', 'sku': 'SKU',
  164. 'units_per_box': '单箱个数', 'declaration_type': '报关类型',
  165. 'box_number': '箱号', 'item_gross_weight_kg': '商品单箱毛重量(KG)',
  166. 'item_net_weight_kg': '商品单箱净重量(KG)', 'chinese_name': '中文品名',
  167. 'english_name': '英文名称', 'brand_type': '品牌类型', 'brand': '品牌',
  168. 'model': '型号', 'material_cn': '材质(CN)', 'material_en': '材质(EN)',
  169. 'purpose_cn': '用途', 'goods_attributes': '商品属性',
  170. 'declaration_hs_code': '报关编码', 'clearance_hs_code': '清关编码',
  171. 'material_ratio': '材质占比', 'clearance_unit_price': '清关单价',
  172. 'clearance_currency': '清关币种', 'purchase_unit_price': '采购单价',
  173. 'purchase_currency': '采购币种', 'product_image_count': '商品图片数量',
  174. 'remark': '备注',
  175. },
  176. 'dw_auth': {
  177. 'fba_id': 'FBAID', 'dw_start': 'DW开始时间', 'dw_end': 'DW结束时间',
  178. 'dw_display': 'DW', 'authorization_status': '客户授权状态',
  179. },
  180. 'attachments': {
  181. 'category': '附件分类', 'file_name': '文件名称',
  182. 'file_extension': '文件类型', 'is_image': '是否图片',
  183. 'preview_url': '预览链接', 'download_url': '下载链接',
  184. 'cost_name': '收费项目',
  185. },
  186. 'inbound_summary': {
  187. 'inbound_status': '入库状态', 'inbound_time': '入库时间',
  188. 'inbound_operator': '操作人', 'warehouse': '仓库',
  189. 'abnormal_reason': '异常原因', 'inbound_photo_count': '入库图片数量',
  190. 'forecast_pwv_summary': '预报件重体', 'inbound_pwv_summary': '入库件重体',
  191. },
  192. 'inbound': {
  193. 'has_label': '是否贴标', 'shipment_id': 'SHIPMENT ID',
  194. 'inbound_quantity': '入库件数',
  195. 'measurement_photo_count': '测量图片数量',
  196. 'box_specification': '入库箱规', 'weight_mode': None, 'weight': None,
  197. 'weight_unit': '重量单位', 'is_over_length': '长度是否超限',
  198. 'is_over_width': '宽度是否超限', 'is_over_height': '高度是否超限',
  199. 'is_over_weight': '重量是否超限', 'operated_at': '操作时间',
  200. },
  201. 'checks': {
  202. 'shipment_id': '货件编号', 'sku': 'SKU', 'box_number': '箱号',
  203. 'box_quantity': '箱数', 'box_specification': '箱规',
  204. 'inspection_result': '查验结果',
  205. 'inspection_photo_count': '查验图片数量', 'remark': '备注',
  206. 'inspector': '查验人', 'inspected_at': '查验时间',
  207. },
  208. 'tracks': {
  209. 'track_type': '轨迹类型', 'status': '轨迹节点', 'location': '轨迹地点',
  210. 'occurred_at': '时间', 'tracking_number': '跟踪号',
  211. 'shipment_id': 'SHIPMENT ID', 'content': '轨迹内容',
  212. },
  213. 'operation_logs': {
  214. 'operated_at': '操作时间', 'content': '操作内容', 'operator': '操作人',
  215. },
  216. 'cost_logs': {
  217. 'operated_at': '操作时间', 'content': '操作内容', 'operator': '操作人',
  218. },
  219. 'delivery_summary': {
  220. 'master_tracking_number': '主跟踪号', 'sub_order_count': '子单数量',
  221. },
  222. 'delivery': {'sub_tracking_number': '子跟踪号'},
  223. }
  224. ORDER_DETAIL_STATUS_FIELDS = {
  225. 'stage': '阶段', 'label': '节点', 'state': '状态',
  226. 'occurred_at': '时间', 'time_kind': '时间类型',
  227. }
  228. ORDER_DETAIL_STATES = {
  229. 'completed': '已完成', 'in_progress': '进行中',
  230. 'pending': '待完成', 'hidden': '不展示',
  231. }
  232. ORDER_DETAIL_TIME_KINDS = {
  233. 'actual': '实际', 'estimated': '预计', 'empty': '暂无',
  234. }
  235. TABLE_COLUMNS = {
  236. 'query_order_exact': {
  237. 'order_number': ('订单号',),
  238. 'reference_number': ('客户参考号',),
  239. 'status_txt_name': ('状态',),
  240. 'check_status_txt_name': ('是否已查验',),
  241. 'customer_name': ('客户名称',),
  242. 'customer_account_type_name': ('客户属性',),
  243. 'inbound_date': ('入库时间',),
  244. 'wo_num': ('未完成工单',),
  245. 'product_name': ('物流产品',),
  246. 'inbound_pieces': ('件数',),
  247. 'inbound_volume': ('体积(CBM)',),
  248. 'inbound_weight': ('重量(KG)',),
  249. 'pro_cn_name': ('品名',),
  250. 'export_declaration_type': ('报关方式',),
  251. 'merge_declare_number': ('合并报关单号',),
  252. 'delivery_address': ('派送地址',),
  253. 'container_code': ('柜号',),
  254. 'out_status_txt': ('排舱单状态',),
  255. 'hinge_of_destination': ('目的港',),
  256. 'etd': ('ETD',),
  257. 'atd': ('ATD',),
  258. 'eta': ('ETA',),
  259. 'ata': ('ATA',),
  260. 'release_time': ('清关放行时间',),
  261. 'oversea_inbound_date': ('海外入库时间',),
  262. 'appt_time': ('APPT时间',),
  263. 'est_loading_time': ('预计装柜时间',),
  264. 'pickup_time': ('海外提柜时间',),
  265. 'delivery_way_title': ('派送方式',),
  266. 'tracking_number': ('快递单号', '承运商跟踪号码'),
  267. 'shipment_id': ('Shipment ID',),
  268. 'goods_attribute': ('商品属性',),
  269. 'sku': ('SKU',),
  270. 'sales_user': ('商务经理',),
  271. 'service_user': ('客户经理',),
  272. 'department_name': ('事业部',),
  273. 'remark': ('订单备注',),
  274. 'importer_name': ('进口商',),
  275. 'warehouse_name': ('交货仓库',),
  276. 'paid_status_name': ('付款状态',),
  277. },
  278. 'query_track': {
  279. 'status': ('轨迹节点', '轨迹状态,如:开始集港、离港放行、清关、配送等'),
  280. 'location': ('轨迹地点', '发生地点'),
  281. 'time': ('时间', '轨迹发生时间(已转换为用户时区)'),
  282. 'content': ('轨迹内容', '详细描述'),
  283. 'tracking_number': ('跟踪号', '快递单号'),
  284. 'shipment_id': ('Shipment ID', '包裹ID'),
  285. },
  286. 'query_customs_declaration_files': {
  287. 'outbound_number': ('排舱单号',),
  288. 'order_number': ('订单号',),
  289. 'file_name': ('文件名',),
  290. 'file_type': ('文件类型',),
  291. 'file_url': ('文件链接',),
  292. },
  293. 'query_outbound_list': {
  294. 'outbound_number': ('排舱单号',),
  295. 'direct_send': ('直送柜',),
  296. 'remark': ('备注',),
  297. 'cargo_type': ('超长超重',),
  298. 'warehouse_name': ('集货仓库',),
  299. 'status': ('状态',),
  300. 'so_number': ('SO号',),
  301. 'container_code': ('柜号',),
  302. 'seal_number': ('封条号',),
  303. 'container_type': ('柜型',),
  304. 'cabinet_type': ('整拼类型',),
  305. 'shipping_method': ('运输方式',),
  306. 'total_volume': ('体积(CBM)',),
  307. 'total_weight': ('重量(KG)',),
  308. 'ship_schedule': ('船期',),
  309. 'closing_time': ('截关时间',),
  310. 'est_loading_time': ('预计装柜时间',),
  311. 'operator': ('操作人',),
  312. 'operation_modes': ('拖报清方式',),
  313. 'handle_method': ('操作方式',),
  314. 'operation_time': ('操作时间',),
  315. 'ship_company': ('船司',),
  316. 'vessel_name': ('船名航次',),
  317. 'route': ('航线',),
  318. 'cutoff_time_si': ('截SI时间',),
  319. 'clearance_port': ('清关口岸',),
  320. 'clearance_port_eta': ('清关口岸ETA',),
  321. 'loading_port': ('起运港',),
  322. 'destination_port': ('目的港',),
  323. 'wharf_name': ('到港码头',),
  324. 'transit_port': ('中转港',),
  325. 'etd': ('ETD',),
  326. 'eta': ('ETA',),
  327. 'ata': ('ATA',),
  328. 'clearance_release_time': ('清关放行时间',),
  329. 'overseas_warehouse': ('海外仓',),
  330. 'providers_name': ('拖车行',),
  331. 'has_fda': ('是否含FDA认证商品',),
  332. 'has_cpsc': ('是否含CPSC商品',),
  333. 'has_food': ('是否含食品',),
  334. },
  335. 'query_outbound_detail': {
  336. 'order_number': ('订单号',),
  337. 'cargo_type': ('超长超重',),
  338. 'customs_files': ('报关资料',),
  339. 'reference_number': ('客户参考号',),
  340. 'customer_name': ('客户名称',),
  341. 'declaration_type': ('报关方式',),
  342. 'merge_declare_number': ('合并报关单号',),
  343. 'order_remark': ('订单备注',),
  344. 'bl_number': ('提单号',),
  345. 'container_code': ('柜号',),
  346. 'customer_service': ('客户经理',),
  347. 'est_inbound_date': ('预计入库时间',),
  348. 'inbound_date': ('实际入库时间',),
  349. 'status': ('状态',),
  350. 'pieces': ('预报件数 / 入库件数',),
  351. 'weight': ('预报实重(KG) / 入库实重(KG)',),
  352. 'volume': ('预报体积(m³) / 入库体积(m³)',),
  353. 'pickup_place': ('交货地',),
  354. 'product_name': ('物流产品',),
  355. 'goods_name': ('品名',),
  356. 'closing_time': ('截关时间',),
  357. 'clearance_remark': ('报关备注',),
  358. 'import_clearance_remark': ('清关备注',),
  359. 'delivery_address': ('派送地址',),
  360. 'delivery_type': ('派送类型',),
  361. 'delivery_way': ('派送方式',),
  362. 'channel': ('派送渠道',),
  363. 'country_name': ('目的国',),
  364. 'importer_name': ('进口商',),
  365. 'vat_type': ('清关税号',),
  366. 'packing_type': ('货物类型',),
  367. 'is_abnormal': ('是否问题件',),
  368. 'package_method': ('包装类型',),
  369. 'order_reply': ('到货回复',),
  370. 'is_must_load': ('订单是否必装',),
  371. 'has_fda': ('是否含FDA认证商品',),
  372. 'has_cpsc': ('是否含CPSC商品',),
  373. 'has_food': ('是否含食品',),
  374. },
  375. 'query_customer_list': {
  376. 'customer_name': ('客户名称',),
  377. 'customer_code': ('客户代码',),
  378. 'create_time': ('开户时间',),
  379. 'business_type': ('业务类型',),
  380. 'customer_attribute': ('客户属性',),
  381. 'customer_source': ('客户来源',),
  382. 'first_inbound_date': ('首次成交时间',),
  383. 'last_inbound_date': ('最后一次走货时间',),
  384. 'active_status': ('活跃状态',),
  385. 'contract_status': ('合同状态',),
  386. 'contract_validity': ('合同有效期',),
  387. 'credit_limit': ('信用额度',),
  388. 'currency_code': ('结算币种',),
  389. 'billing_modes': ('结算模式(分业务类型)',),
  390. 'sales_name': ('商务经理',),
  391. 'merchandiser_name': ('客户经理',),
  392. 'department_name': ('事业部',),
  393. },
  394. 'query_remote_address_list': {
  395. 'address_no': ('偏远地址库编号',),
  396. 'remote_name': ('偏远地址库名称',),
  397. 'status_text': ('状态',),
  398. 'create_time': ('添加时间',),
  399. 'create_user': ('添加人',),
  400. },
  401. 'query_fuel_rate_list': {
  402. 'fuel_no': ('燃油代码',),
  403. 'price_type_name': ('燃油类型',),
  404. 'setting_type_text': ('配置方式',),
  405. 'fuel_ratio': ('燃油系数',),
  406. 'start_date': ('开始生效时间',),
  407. 'end_date': ('截止生效时间',),
  408. 'conditions_text': ('条件',),
  409. 'product_type_name': ('产品分类',),
  410. 'channel_type_name': ('渠道分类',),
  411. 'product_name': ('产品',),
  412. 'channel_name': ('渠道',),
  413. 'status_name': ('状态',),
  414. 'source': ('来源',),
  415. 'user_name': ('创建人',),
  416. 'create_date': ('创建时间',),
  417. },
  418. 'save_fuel_rate': {
  419. 'fuel_no': ('燃油代码',),
  420. 'price_type_name': ('燃油类型',),
  421. 'fuel_ratio': ('燃油系数',),
  422. 'status_text': ('状态',),
  423. 'source': ('来源',),
  424. 'start_date': ('开始生效时间',),
  425. 'end_date': ('截止生效时间',),
  426. 'conditions_text': ('条件',),
  427. },
  428. 'enable_fuel_rate': {
  429. 'fuel_no': ('燃油代码',),
  430. 'status_text': ('状态',),
  431. },
  432. }
  433. PAYMENT_FOLLOWUP_COLUMNS = {
  434. 'customer_name': '客户名称',
  435. 'settlement_currency': '结算币种',
  436. 'billed_unverified_amount': '已出账未核销金额',
  437. 'unbilled_amount': '未出账金额',
  438. 'overdue_unpaid_amount': '逾期未回款金额',
  439. 'billed_unverified_amount_cny': '已出账未核销金额(人民币)',
  440. 'unbilled_amount_cny': '未出账金额(人民币)',
  441. 'overdue_unpaid_amount_cny': '逾期未回款金额(人民币)',
  442. 'unverified_receivable_monthly_summary': '未回款月份汇总',
  443. 'receipt_unverified_amount': '收款单未核销金额',
  444. 'current_balance': '当前余额',
  445. 'credit_limit': '信用额度',
  446. 'bad_debt_total': '坏账合计',
  447. 'contract_status': '合同状态',
  448. }
  449. PAYMENT_FOLLOWUP_DETAIL_COLUMNS = {
  450. 'bill_month': '账单月份',
  451. 'bill_no': '账单号',
  452. 'business_type': '业务类型',
  453. 'settlement_mode': '结算模式',
  454. 'unverified_amount': '未核销金额',
  455. 'customer_receivable_date': '客户应收款日期',
  456. }
  457. PAYMENT_RECORD_COLUMNS = {
  458. 'customer_name': '客户名称',
  459. 'payment_reference': '收款水单号',
  460. 'original_received_amount': '原币到账金额',
  461. 'actual_received_amount': '实际收款金额',
  462. 'receive_date': '收款日期',
  463. 'verified_amount': '已核销金额',
  464. 'unverified_amount': '未核销金额',
  465. 'payment_approval_status': '收款审核状态',
  466. }
  467. RECEIVABLE_COST_COLUMNS = {
  468. 'order_number': '订单号',
  469. 'customer_name': '客户名称',
  470. 'cost_name': '费用项',
  471. 'cost_customer_name': '费用客户',
  472. 'charge_weight': '计费重',
  473. 'unit_price': '计费单价',
  474. 'quote_currency': '原币币种',
  475. 'receivable_original_amount': '应收原币',
  476. 'receivable_amount_cny': '应收金额(CNY)',
  477. 'settlement_amount_cny': '结算金额(CNY)',
  478. 'cost_confirmation_status': '费用确认状态',
  479. 'closing_status': '关账状态',
  480. 'verification_status': '核销状态',
  481. }
  482. RECEIVABLE_COST_SUMMARY = {
  483. 'order_total_receivable_cny': '订单总应收(CNY)',
  484. 'order_total_settlement_cny': '订单总结算(CNY)',
  485. 'special_approval_amount_cny': '特批金额(CNY)',
  486. 'settlement_gross_profit_cny': '结算毛利(CNY)',
  487. }
  488. RECEIVABLE_LIST_COLUMNS = {
  489. 'main_customer_name': '主客户',
  490. 'sub_customer_name': '子客户',
  491. 'customer_attribute': '客户属性',
  492. 'department_name': '事业部',
  493. 'document_type': '单据类型',
  494. 'warehouse_name': '仓库',
  495. 'business_number': '业务单号',
  496. 'reference_number': '参考号',
  497. 'cost_name': '费用项',
  498. 'original_amount': '原币金额',
  499. 'total_amount': '总金额',
  500. 'billed_amount': '已出账金额',
  501. 'unbilled_amount': '未出账金额',
  502. 'bill_number': '账单编号',
  503. 'verification_status': '核销状态',
  504. 'receipt_number': '收款水单号',
  505. 'receipt_date': '收款日期',
  506. 'sales_name': '商务经理',
  507. 'merchandiser_name': '客户经理',
  508. 'drainage_user_name': '引流人',
  509. 'first_leg_order_status': '头程订单状态',
  510. 'settlement_mode': '结算模式',
  511. 'cost_occurred_at': '费用发生时间',
  512. 'business_occurred_at': '业务发生时间',
  513. }
  514. PAYABLE_COST_COLUMNS = {
  515. 1: (
  516. ('number', '单号'), ('so_number', 'SO号'), ('container_code', '柜号'),
  517. ('bl_number', '提单号'), ('sub_number', '订单号'),
  518. ('business_node_name', '业务节点'), ('providers_name', '物流商'),
  519. ('cost_name', '费用名称'), ('payable_cost_q', '金额'), ('currency', '币种'),
  520. ('company_money_q', '本位币金额'), ('yf_lock', '应付锁定'),
  521. ('trade_date', '费用发生时间'), ('verify_status_text', '核销状态'),
  522. ('remark', '备注'), ('bill_no', '账单编号'), ('payment_order_number', '付款单号'),
  523. ),
  524. 2: (
  525. ('number', '单号'), ('sub_number', '跟踪单号'), ('workorder_number', '工单号'),
  526. ('order_type_name', '单据类型'), ('providers_name', '物流商'),
  527. ('cost_name', '费用名称'), ('payable_cost_q', '应付原币金额'),
  528. ('currency', '应付原币币种'), ('company_money_q', '本位币金额%s'),
  529. ('yf_lock', '应付锁定'), ('business_date', '业务发生时间'),
  530. ('trade_date', '费用发生时间'), ('create_date', '操作时间'),
  531. ('verify_status_text', '核销状态'), ('remark', '备注'),
  532. ('bill_no', '账单编号'), ('payment_order_number', '付款单号'),
  533. ),
  534. 3: (
  535. ('number', '单号'), ('order_status_text', '包裹状态'),
  536. ('providers_name', '物流商'), ('business_node_name', '费用业务节点'),
  537. ('cost_name', '费用名称'), ('detail_price', '计费单价'),
  538. ('charge_weight', '计费重'), ('quote_cost', '计费金额'),
  539. ('payable_cost_q', '实际应付金额'), ('quote_currency', '原币币种'),
  540. ('company_money_q', '计费本位币金额'), ('yf_lock', '应付锁定'),
  541. ('bill_no', '账单编号'), ('payment_order_number', '付款单号'),
  542. ('trade_date', '费用发生时间'), ('verify_status_text', '核销状态'),
  543. ('remark', '财务备注'), ('create_user_name', '操作人'), ('create_date', '录入时间'),
  544. ),
  545. 4: (
  546. ('number', '订单号'), ('business_bill_no', '货代账单号'),
  547. ('providers_name', '物流商'), ('cost_name', '费用名称'),
  548. ('payable_cost_q', '金额'), ('currency', '币种'),
  549. ('company_money_q', '本位币金额'), ('department_name', '事业部'),
  550. ('yf_lock', '应付锁定'), ('trade_date', '费用发生时间'),
  551. ('verify_status_text', '核销状态'), ('business_node_name', '业务节点'),
  552. ('remark', '备注'), ('bill_no', '应付账单编号'),
  553. ('payment_order_number', '付款单号'),
  554. ),
  555. 5: (
  556. ('order_type_name', '单据类型'), ('number', '订单号'), ('warehouse_name', '仓库'),
  557. ('business_node_name', '业务节点'), ('providers_name', '物流商'),
  558. ('cost_name', '费用名称'), ('payable_cost_q', '金额'), ('currency', '币种'),
  559. ('company_money_q', '本位币金额'), ('yf_lock', '应付锁定'),
  560. ('trade_date', '费用发生时间'), ('verify_status_text', '核销状态'),
  561. ('remark', '备注'), ('bill_no', '账单编号'), ('payment_order_number', '付款单号'),
  562. ),
  563. 7: (
  564. ('number', '订单号'), ('ep_order_status_text', '订单状态'),
  565. ('business_node_name', '业务节点'), ('providers_name', '物流商'),
  566. ('cost_name', '费用名称'), ('detail_price', '计费单价'),
  567. ('charge_weight', '计费重'), ('quote_cost', '计费金额'),
  568. ('payable_cost_q', '实际应付金额'), ('quote_currency', '原币币种'),
  569. ('company_money_q', '本位币金额'), ('trade_date', '费用发生时间'),
  570. ('remark', '备注'), ('verify_status_text', '核销状态'),
  571. ('bill_no', '账单编号'), ('payment_order_number', '付款单号'),
  572. ('create_user_name', '操作人'), ('create_date', '录入时间'),
  573. ),
  574. }
  575. PAYABLE_AMOUNT_KEYS = frozenset((
  576. 'detail_price', 'charge_weight', 'quote_cost', 'payable_cost_q', 'company_money_q',
  577. ))
  578. PAYMENT_FOLLOWUP_MONTHLY_COLUMNS = {
  579. 'receivable_month': '应收月份',
  580. 'unverified_amount': '未核销金额',
  581. 'is_overdue': '是否逾期',
  582. }
  583. DESTINATION_TRAILER_COLUMNS = {
  584. 'progress': (
  585. ('bl_number', '提单号'), ('container_code', '柜号'),
  586. ('container_type', '柜型'), ('warehouse_name', '仓库名称'),
  587. ('providers_name', '拖车行'), ('trailer_status', '提柜状态'),
  588. ('latest_remark', '最新操作备注'), ('is_direct_send', '直送'),
  589. ('cabinet_type', '整拼类型'), ('vessel_name', '船名航次'),
  590. ('route', '航线'), ('clearance_check', '清关查验'),
  591. ('etd', 'ETD'), ('atd', 'ATD'), ('eta', 'ETA'), ('ata', 'ATA'),
  592. ('loading_port', '起运港'), ('destination_port', '目的港'),
  593. ('wharf_name', '到港码头'), ('appt_time', 'APPT时间'),
  594. ('delivery_end_time', '卡车实际派送时间'),
  595. ('est_pickup_time', '预计提柜时间'),
  596. ('pickup_time', '实际提柜时间'), ('inbound_date', '到仓时间'),
  597. ('container_return_time', '还柜时间'),
  598. ('pickup_prescription', '提柜时效'),
  599. ('return_prescription', '还柜时效'),
  600. ('wharf_wait_time', '码头等待时间'),
  601. ('amazon_wait_time', '亚马逊等待时间'),
  602. ('bill_status', '应付费用状态'),
  603. ('issue_way', '出单方式'),
  604. ('release_status', '放货状态'),
  605. ('freight_collect', '海运费到付'),
  606. ('is_dg', 'DG'),
  607. ('is_soc', 'SOC'),
  608. ('is_oneway', 'ONEWAY'),
  609. ('appointment_method', '预约方式'),
  610. ('need_appointment_code', '是否需要预约码'),
  611. ('warehouse_special_remark', '仓库特殊备注'),
  612. ),
  613. 'do': (
  614. ('trailer_number', '拖车单号'), ('outbound_number', '出库单号'),
  615. ('container_code', '柜号'), ('container_type', '柜型'),
  616. ('bl_number', '提单号'), ('is_direct_send', '是否直送'),
  617. ('original_do_file', '原DO单'), ('new_do_file', '新DO单'),
  618. ('email_subject', '邮件主题'), ('email_from', '邮件发送人'),
  619. ('received_at', '邮件接收时间'),
  620. ),
  621. }
  622. DESTINATION_TRAILER_FILE_KEYS = frozenset(('original_do_file', 'new_do_file'))
  623. DESTINATION_TRAILER_PROGRESS_STAGES = frozenset((
  624. '待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)',
  625. ))
  626. ORDER_ABNORMAL_COLUMNS = (
  627. ('order_number', '订单号'), ('customer_name', '客户'),
  628. ('product_name', '物流产品'), ('abnormal_status', '问题件状态'),
  629. ('abnormal_type', '问题件类型'), ('abnormal_desc', '问题件描述'),
  630. ('outbound_status', '排舱单状态'), ('inbound_date', '入库时间'),
  631. ('marker_name', '标记人'),
  632. )
  633. ORDER_ABNORMAL_STATUSES = frozenset((
  634. '待处理', '放行待审核', '包裹放行', '退件待审核', '退件驳回', '已退件',
  635. ))
  636. GOODS_INFO_COLUMNS = (
  637. ('sku', 'SKU'), ('departure_country', '起运国'),
  638. ('destination_country', '目的国'), ('goods_attr', '商品属性'),
  639. ('cn_name', '中文名称'), ('en_name', '英文名称'),
  640. ('declaration_hscode', '报关编码'), ('clearance_hscode', '清关编码'),
  641. ('tax_rate', '税率'), ('is_inspection_fee', '木制/竹制品商检费'),
  642. ('is_fda_declaration', 'FDA申报'), ('product_surcharge', '产品附加费'),
  643. ('tax_surcharge', '关税附加费'), ('is_epa_declaration', 'EPA申报'),
  644. ('is_act_declaration', 'LACY ACT申报'), ('is_dot_declaration', 'DOT申报'),
  645. ('is_cpsc_declaration', 'CPSC申报'), ('brand_type', '品牌类型'),
  646. ('brand', '品牌'), ('goods_type', '型号'),
  647. ('material_cn', '材质(CN)'), ('material_en', '材质(EN)'),
  648. ('purpose_cn', '用途(CN)'), ('purpose_en', '用途(EN)'),
  649. )
  650. RECEIVE_VOLUME_COLUMNS = (
  651. ('order_number', '订单号'), ('customer_name', '客户名称'),
  652. ('packing_type', '货物类型'), ('inbound_volume', '入库体积'),
  653. ('inbound_date', '入库时间'), ('sales_name', '商务经理'),
  654. ('merchandiser_name', '客户经理'), ('department_name', '事业部'),
  655. ('product_name', '物流产品'),
  656. )
  657. HEADHAUL_DOCUMENT_COLUMNS = (
  658. ('document_type_name', '文档类型'),
  659. ('entry_no_text', '录入单号快照'),
  660. ('original_name', '文件名'),
  661. ('safe_url', '安全链接'),
  662. ('operator_name', '操作人'),
  663. ('operate_time', '操作时间'),
  664. )
  665. REMOTE_ADDRESS_DETAIL_COLUMNS = (
  666. ('shipper_country', '发件人国家'),
  667. ('shipper_province', '发件人州/省'),
  668. ('shipper_city', '发件人城市'),
  669. ('shipper_start_postcode', '发件人开始邮编'),
  670. ('shipper_end_postcode', '发件人结束邮编'),
  671. ('country', '收件人国家'),
  672. ('province', '收件人州/省'),
  673. ('city', '收件人城市'),
  674. ('start_postcode', '收件人开始邮编'),
  675. ('end_postcode', '收件人结束邮编'),
  676. ('create_time', '添加时间'),
  677. ('create_user', '添加人'),
  678. )
  679. CONTAINER_TIMELINESS_COLUMNS = (
  680. ('ship_company', '船公司'), ('bl_number', '提单号'),
  681. ('container_code', '柜号'), ('container_type', '柜型'),
  682. ('is_direct_send_name', '是否直送'), ('shipping_method', '运输方式'),
  683. ('route', '航线'), ('vessel_name', '航名航次'),
  684. ('wharf_name', '到港码头'), ('providers_name', '拖车行'),
  685. ('dt_status_name', '柜子状态'), ('warehouse_name', '交货仓库'),
  686. ('order_total', '件数'), ('total_volume', '体积'),
  687. ('total_weight', '重量'), ('loading_name', '起运港'),
  688. ('destination_name', '目的港'), ('delivery_address', '派送地址'),
  689. ('has_throw_off_name', '甩柜'), ('throw_off_time', '甩柜时间'),
  690. ('ed_check_name', '报关查验'), ('ed_check_time', '报关查验时间'),
  691. ('qg_check_name', '清关查验'), ('check_time', '清关查验时间'),
  692. ('in_inspection_site', '进查验场时间'),
  693. ('out_inspection_site', '出查验场时间'),
  694. ('is_close_area_name', '封闭区'), ('start_close_area', '进封闭区时间'),
  695. ('end_close_area', '出封闭区时间'), ('outbound_date', '出库时间'),
  696. ('export_release_time', '出口报关放行时间'),
  697. ('etd', '干线预计出发时间'), ('eta', '干线预计到达时间'),
  698. ('gxyjsx', '干线预计时效'), ('departure_time', '干线实际出发时间'),
  699. ('arrival_time', '干线实际到达时间'), ('gxsjsx', '干线实际时效'),
  700. ('hcsx', '航程时效'), ('start_clearance_time', '开始清关时间'),
  701. ('import_release_time', '海外清关放行时间'),
  702. ('wharf_wait_time', '码头等待时间'), ('pickup_time', '码头提柜时间'),
  703. ('second_pickup_time', '二次提柜时间'), ('tgsx', '提柜时效'),
  704. ('max_appointment_delivery_time', 'APPT时间'),
  705. ('delivery_end_time', '卡车实际派送时间'),
  706. ('oversea_warehouse_name', '海外仓库名称'),
  707. ('inbound_date', '到仓时间'), ('inbound_time', '海外仓入库时间'),
  708. ('container_return_date', '还空柜时间'), ('hksx', '还空时效'),
  709. ('amazon_wait_time', '亚马逊等待时间'),
  710. ('inner_remark', '内部备注'), ('financial_remark', '财务备注'),
  711. )
  712. OUTBOUND_DETAIL_SUMMARY = {
  713. 'bl_number': '提单号',
  714. 'container_code': '柜号',
  715. 'container_type': '柜型',
  716. 'total_volume': '总体积',
  717. 'total_weight': '总重量',
  718. 'total_pieces': '总件数',
  719. 'sku': 'SKU',
  720. 'buy_declaration_count': '买单报关数量',
  721. 'general_declaration_count': '一般贸易报关数量',
  722. 'must_load': '必装单量/体积',
  723. 'backup_load': '备装单量/体积',
  724. }
  725. FIELD_LABELS = {
  726. 'query_order_exact': {
  727. 'order_number': '订单号',
  728. 'order_numbers': '订单号',
  729. 'reference_number': '客户参考号',
  730. 'reference_numbers': '客户参考号',
  731. 'tracking_number': '快递单号',
  732. 'tracking_numbers': '快递单号',
  733. 'outbound_number': '排舱单号',
  734. 'outbound_numbers': '排舱单号',
  735. 'container_code': '柜号',
  736. 'container_codes': '柜号',
  737. 'so_number': 'SO号',
  738. 'so_numbers': 'SO号',
  739. 'shipment_id': 'Shipment ID',
  740. 'receiver_country': '收货国家',
  741. 'product_ids': '物流产品',
  742. 'customer_ids': '客户',
  743. 'sales_id': '销售人员',
  744. 'warehouse_ids': '仓库',
  745. 'department_id': '事业部',
  746. 'inbound_date_start': '入库开始日期',
  747. 'inbound_date_end': '入库结束日期',
  748. 'outbound_date_start': '出库开始日期',
  749. 'outbound_date_end': '出库结束日期',
  750. 'page': '页码',
  751. 'limit': '每页数量',
  752. },
  753. 'query_track': {
  754. 'order_id': '订单ID',
  755. 'order_number': '订单号',
  756. 'tracking_number': '快递单号',
  757. 'page': '页码',
  758. 'limit': '每页数量',
  759. },
  760. 'query_customs_declaration_files': {
  761. 'outbound_numbers': '排舱单号',
  762. 'order_numbers': '订单号',
  763. 'page': '页码',
  764. 'limit': '每页数量',
  765. },
  766. 'query_outbound_list': {
  767. 'outbound_numbers': '排舱单号',
  768. 'order_numbers': '订单号',
  769. 'container_codes': '柜号',
  770. 'so_numbers': 'SO号',
  771. 'bl_numbers': '提单号',
  772. 'outbound_status': '排舱状态',
  773. 'shipping_method': '运输方式',
  774. 'warehouse_id': '集货仓库',
  775. 'is_direct_send': '是否直送柜',
  776. 'page': '页码',
  777. 'limit': '每页数量',
  778. },
  779. 'query_outbound_detail': {
  780. 'outbound_number': '排舱单号',
  781. 'page': '页码',
  782. 'limit': '每页数量',
  783. },
  784. 'query_order_detail': {
  785. 'order_number': '订单号', 'section': '详情模块',
  786. 'page': '页码', 'limit': '每页数量',
  787. },
  788. 'list_order_filter_options': {
  789. 'filter_type': '筛选项类型',
  790. 'keyword': '关键词',
  791. 'page': '页码',
  792. 'limit': '每页数量',
  793. },
  794. 'list_outbound_filter_options': {
  795. 'filter_type': '筛选类别',
  796. 'keyword': '显示名称',
  797. 'page': '页码',
  798. 'limit': '每页数量',
  799. },
  800. 'list_pending_outbound_export_filter_options': {
  801. 'filter_type': '筛选项类型',
  802. 'product_type_id': '产品分类',
  803. 'keyword': '关键词',
  804. 'page': '页码',
  805. 'limit': '每页数量',
  806. },
  807. 'query_customer_list': {
  808. 'customer_id': '客户名称',
  809. 'department_id': '事业部',
  810. 'sales_id': '商务经理',
  811. 'merchandiser_id': '客户经理',
  812. 'page': '页码',
  813. 'limit': '每页数量',
  814. },
  815. 'query_remote_address_list': {
  816. 'address_no': '偏远地址库编号',
  817. 'remote_name': '偏远地址库名称',
  818. 'status': '状态',
  819. 'country': '国家',
  820. 'province': '州/省',
  821. 'city': '城市',
  822. 'postcode': '邮编',
  823. 'page': '页码',
  824. 'limit': '每页数量',
  825. },
  826. 'maintain_remote_address': {
  827. 'address_no': '偏远地址库编号',
  828. 'page': '页码',
  829. 'limit': '每页数量',
  830. },
  831. 'save_remote_address': {
  832. 'address_no': '偏远地址库编号',
  833. 'remote_name': '偏远地址库名称',
  834. 'status': '状态',
  835. 'details': '地址明细',
  836. },
  837. 'query_fuel_rate_list': {
  838. 'fuel_no': '燃油代码',
  839. 'product': '产品',
  840. 'channel': '渠道',
  841. 'product_type': '产品分类',
  842. 'channel_type': '渠道分类',
  843. 'price_type': '燃油类型',
  844. 'status': '状态',
  845. 'start_date': '生效时间',
  846. 'page': '页码',
  847. 'limit': '每页数量',
  848. },
  849. 'save_fuel_rate': {
  850. 'price_type': '燃油类型',
  851. 'start_date': '开始生效时间',
  852. 'end_date': '结束生效时间',
  853. 'items': '燃油系数',
  854. 'fuel_ratio': '燃油费率',
  855. 'conditions': '计费条件',
  856. 'condition': '计费条件',
  857. 'condition_value': '条件值',
  858. 'condition_relation': '条件关系',
  859. 'group': '条件组',
  860. },
  861. 'enable_fuel_rate': {
  862. 'fuel_no': '燃油代码',
  863. },
  864. 'query_customer_payment_followup': {
  865. 'customer_id': '客户名称',
  866. 'department_id': '事业部',
  867. 'sales_id': '商务经理',
  868. 'merchandiser_id': '客户经理',
  869. 'has_unverified_receivable_only': '只看有应收未核销费用的客户',
  870. 'page': '页码',
  871. 'limit': '每页数量',
  872. },
  873. 'query_customer_unverified_bill_details': {
  874. 'customer_id': '客户名称',
  875. 'page': '页码',
  876. 'limit': '每页数量',
  877. },
  878. 'list_customer_filter_options': {
  879. 'filter_type': '筛选项类型',
  880. 'keyword': '关键词',
  881. 'page': '页码',
  882. 'limit': '每页数量',
  883. },
  884. 'export_pending_outbound_orders': {
  885. 'number': '单号',
  886. 'product_type_id': '产品分类',
  887. 'product_id': '物流产品',
  888. 'order_warehouse_id': '集货仓库',
  889. 'receiver_country': '目的国',
  890. 'is_remove': '是否装柜剔除',
  891. 'address': '派送地址',
  892. 'inbound_date': '入库时间',
  893. 'status': '订单状态',
  894. 'merge_declare_number': '合并报关单号',
  895. 'importer_id': '进口商',
  896. 'packing_type': '货物类型',
  897. 'is_battery': '带电属性',
  898. 'is_magnetic': '带磁属性',
  899. 'is_wood': '带木属性',
  900. 'is_other': '其他商品属性',
  901. 'is_fda': 'FDA产品属性',
  902. 'is_toy': '玩具属性',
  903. 'is_ultra_limit': '超长超重属性',
  904. 'is_sensitive': '敏感货属性',
  905. 'is_food': '食品属性',
  906. 'no_property': '无属性',
  907. },
  908. 'export_out_of_province_port_data': {
  909. 'outbound_numbers': '排舱单号',
  910. 'container_codes': '柜号',
  911. 'bl_numbers': '提单号',
  912. 'so_numbers': 'SO号',
  913. 'file_type': '资料类型',
  914. },
  915. 'export_pallet_data': {
  916. 'container_codes': '柜号',
  917. 'bl_numbers': '提单号',
  918. 'inbound_time_start': '海外仓入库时间开始',
  919. 'inbound_time_end': '海外仓入库时间结束',
  920. },
  921. 'query_container_timeliness_list': {
  922. 'container_codes': '柜号',
  923. 'bl_numbers': '提单号',
  924. 'departure_time_start': '干线实际出发时间开始',
  925. 'departure_time_end': '干线实际出发时间结束',
  926. 'page': '页码',
  927. 'limit': '每页数量',
  928. },
  929. 'export_container_timeliness_report': {
  930. 'container_codes': '柜号',
  931. 'bl_numbers': '提单号',
  932. 'departure_time_start': '干线实际出发时间开始',
  933. 'departure_time_end': '干线实际出发时间结束',
  934. },
  935. 'query_headhaul_document_list': {
  936. 'document_type_id': '文档类型',
  937. 'original_name': '文件名',
  938. 'declaration_numbers': '报关单号',
  939. 'sys_bl_numbers': '系统提单号',
  940. 'container_codes': '柜号',
  941. 'order_numbers': '订单号',
  942. 'clearance_numbers': '清关单号',
  943. 'customer_names': '客户',
  944. 'so_numbers': 'SO号',
  945. 'bl_numbers': '提单号',
  946. 'trailer_numbers': '拖车单',
  947. 'truck_bol_numbers': '卡车单号',
  948. 'provider_names': '供应商',
  949. 'importer_names': '进口商',
  950. 'page': '页码',
  951. 'limit': '每页数量',
  952. },
  953. 'list_headhaul_document_filter_options': {
  954. 'filter_type': '筛选类别',
  955. 'keyword': '显示名称',
  956. 'page': '页码',
  957. 'limit': '每页数量',
  958. },
  959. 'prepare_headhaul_document_upload': {
  960. 'document_type_id': '文档类型',
  961. },
  962. 'save_headhaul_document': {
  963. 'document_type_id': '文档类型',
  964. 'ticket_ref': '上传凭证',
  965. 'link_url': '外链',
  966. 'entries': '录入单号',
  967. },
  968. 'delete_headhaul_document': {
  969. 'document_ref': '文档身份',
  970. },
  971. 'export_receivable_cost_list': {
  972. 'reference_numbers': '参考号',
  973. 'tracking_numbers': '跟踪号',
  974. 'order_numbers': '订单号',
  975. 'bill_numbers': '账单编号',
  976. 'business_date_start': '业务发生开始日期',
  977. 'business_date_end': '业务发生结束日期',
  978. 'customer_id': '主客户',
  979. 'sub_customer_id': '子客户',
  980. 'billing_status': '出账状态',
  981. 'verification_status': '核销状态',
  982. 'document_type': '单据类型',
  983. 'cost_type_id': '费用项',
  984. },
  985. 'query_export_task': {
  986. 'task_ref': '导出任务引用',
  987. },
  988. 'query_freightower_track': {
  989. 'bl_number': '提单号',
  990. 'container_code': '柜号',
  991. },
  992. }
  993. ERROR_MESSAGES = {
  994. 'MCP_1101': '设备配置已失效,请重新生成配置',
  995. 'MCP_1102': '设备会话已过期,请重新连接',
  996. 'MCP_1103': '员工账号不可用,请联系管理员',
  997. 'MCP_1104': '身份信息已失效,请重新登录或重新生成设备配置',
  998. 'MCP_1201': '工具当前不可用',
  999. 'MCP_1202': '工具当前不可用',
  1000. 'MCP_1301': '没有权限使用此工具',
  1001. 'MCP_1501': '目标数据不可用',
  1002. 'MCP_1502': '结果过多,请缩小查询范围',
  1003. 'MCP_1601': '没有可导出的数据',
  1004. 'MCP_9001': '工具调用失败,请稍后重试',
  1005. }
  1006. TAX_TRIAL_MESSAGES = frozenset((
  1007. '未找到已审核商品',
  1008. '匹配到多条商品,请核对起运国和目的国',
  1009. '该商品没有应收清关编码,无法试算',
  1010. '关税试算失败,请稍后重试',
  1011. ))
  1012. NON_RETRYABLE_CODES = frozenset((
  1013. 'MCP_1101',
  1014. 'MCP_1102',
  1015. 'MCP_1103',
  1016. 'MCP_1104',
  1017. 'MCP_1201',
  1018. 'MCP_1202',
  1019. 'MCP_1301',
  1020. 'MCP_1401',
  1021. 'MCP_1501',
  1022. 'MCP_1502',
  1023. 'MCP_1601',
  1024. ))
  1025. def handles(self, tool_name):
  1026. return isinstance(tool_name, str) and tool_name in self.SAFE_TOOLS
  1027. # Convert a backend envelope into text and structured display data.
  1028. def present(self, tool_name, tool_result):
  1029. if not self.handles(tool_name) or not isinstance(tool_result, dict):
  1030. return self._format_error()
  1031. code = str(tool_result.get('code') or '').strip()
  1032. meta = self._build_meta(tool_result.get('meta'))
  1033. if code not in ('MCP_0000', '0'):
  1034. return self._business_error(
  1035. tool_name,
  1036. code or 'MCP_9001',
  1037. tool_result.get('msg'),
  1038. meta,
  1039. )
  1040. data = tool_result.get('data')
  1041. if not isinstance(data, dict):
  1042. return self._format_error(meta)
  1043. if tool_name in self.DETAIL_TOOLS:
  1044. return self._present_outbound_detail(
  1045. tool_name,
  1046. data,
  1047. tool_result.get('meta'),
  1048. meta,
  1049. )
  1050. if tool_name in self.ORDER_DETAIL_TOOLS:
  1051. return self._present_order_detail(data, tool_result.get('meta'), meta)
  1052. if tool_name in self.PAYMENT_FOLLOWUP_TOOLS:
  1053. return self._present_customer_payment_followup(
  1054. data, tool_result.get('meta'), meta
  1055. )
  1056. if tool_name in self.PAYMENT_DETAIL_TOOLS:
  1057. return self._present_customer_unverified_bill_details(
  1058. data, tool_result.get('meta'), meta
  1059. )
  1060. if tool_name in self.PAYMENT_RECORD_TOOLS:
  1061. return self._present_customer_payment_records(
  1062. data, tool_result.get('meta'), meta
  1063. )
  1064. if tool_name in self.RECEIVABLE_COST_TOOLS:
  1065. return self._present_order_receivable_cost_details(
  1066. data, tool_result.get('meta'), meta
  1067. )
  1068. if tool_name in self.RECEIVABLE_LIST_TOOLS:
  1069. return self._present_receivable_cost_list(
  1070. data, tool_result.get('meta'), meta
  1071. )
  1072. if tool_name in self.RECEIVABLE_FILTER_TOOLS:
  1073. return self._present_receivable_cost_filter_options(
  1074. data, tool_result.get('meta'), meta
  1075. )
  1076. if tool_name in self.PAYABLE_COST_TOOLS:
  1077. return self._present_payable_cost_list(
  1078. data, tool_result.get('meta'), meta
  1079. )
  1080. if tool_name in self.PAYABLE_FILTER_TOOLS:
  1081. return self._present_receivable_cost_filter_options(
  1082. data, tool_result.get('meta'), meta
  1083. )
  1084. if tool_name in self.DESTINATION_TRAILER_TOOLS:
  1085. return self._present_destination_trailer_list(
  1086. data, tool_result.get('meta'), meta
  1087. )
  1088. if tool_name in self.ARRANGE_TRAILER_TOOLS:
  1089. return self._present_arrange_destination_trailer(data, meta)
  1090. if tool_name in self.ORDER_ABNORMAL_TOOLS:
  1091. return self._present_order_abnormal_list(
  1092. data, tool_result.get('meta'), meta
  1093. )
  1094. if tool_name in self.RECEIVE_VOLUME_TOOLS:
  1095. return self._present_receive_volume_list(
  1096. data, tool_result.get('meta'), meta
  1097. )
  1098. if tool_name in self.GOODS_INFO_TOOLS:
  1099. return self._present_goods_info(
  1100. data, tool_result.get('meta'), meta
  1101. )
  1102. if tool_name in self.GOODS_TAX_TOOLS:
  1103. return self._present_goods_tax(data, meta)
  1104. if tool_name in self.CONTAINER_TIMELINESS_TOOLS:
  1105. return self._present_container_timeliness_list(
  1106. data, tool_result.get('meta'), meta
  1107. )
  1108. if tool_name in self.HEADHAUL_DOCUMENT_TOOLS:
  1109. return self._present_headhaul_document_list(
  1110. data, tool_result.get('meta'), meta
  1111. )
  1112. if tool_name in self.HEADHAUL_PREPARE_TOOLS:
  1113. return self._present_headhaul_prepare(data, meta)
  1114. if tool_name in self.HEADHAUL_SAVE_TOOLS:
  1115. return self._present_headhaul_save(data, meta)
  1116. if tool_name in self.HEADHAUL_DELETE_TOOLS:
  1117. return self._present_headhaul_delete(data, meta)
  1118. if tool_name in self.INBOUND_RECEIPT_TOOLS:
  1119. return self._present_inbound_receipt_templates(data, meta)
  1120. if tool_name in self.FREIGHTOWER_TRACK_TOOLS:
  1121. return self._present_freightower_track(data, meta)
  1122. if tool_name in self.REMOTE_ADDRESS_MAINTAIN_TOOLS:
  1123. return self._present_remote_address_maintain(
  1124. data, tool_result.get('meta'), meta
  1125. )
  1126. if tool_name in self.REMOTE_ADDRESS_SAVE_TOOLS:
  1127. return self._present_remote_address_save(data, meta)
  1128. if tool_name in self.TABLE_TOOLS:
  1129. return self._present_table(
  1130. tool_name,
  1131. data,
  1132. tool_result.get('meta'),
  1133. meta,
  1134. )
  1135. if tool_name in self.OPTION_TOOLS:
  1136. if tool_name == 'list_customer_filter_options':
  1137. return self._present_customer_options(
  1138. data, tool_result.get('meta'), meta
  1139. )
  1140. return self._present_options(data, tool_result.get('meta'), meta)
  1141. if tool_name in self.EXPORT_TOOLS:
  1142. return self._present_export_submission(data, meta)
  1143. if tool_name in self.PAYABLE_EXPORT_TOOLS:
  1144. return self._present_export_submission(data, meta)
  1145. return self._present_export_task(data, meta)
  1146. def _present_order_detail(self, data, raw_meta, meta):
  1147. if set(data) != {'section', 'order_number', 'payload'}:
  1148. return self._format_error(meta)
  1149. section = data.get('section')
  1150. order_number = data.get('order_number')
  1151. payload = data.get('payload')
  1152. if (
  1153. section not in self.ORDER_DETAIL_SECTIONS
  1154. or not isinstance(order_number, str) or not order_number.strip()
  1155. or not isinstance(payload, dict)
  1156. ):
  1157. return self._format_error(meta)
  1158. content = {
  1159. '订单号': order_number.strip(),
  1160. '详情模块': self.ORDER_DETAIL_SECTIONS[section],
  1161. }
  1162. if section == 'all':
  1163. all_content = self._present_order_detail_all(payload)
  1164. if all_content is None:
  1165. return self._format_error(meta)
  1166. content.update(all_content)
  1167. elif section == 'overview':
  1168. expected = {
  1169. 'status_nodes', 'order_info', 'freight_info',
  1170. 'package_summary', 'trader_info',
  1171. }
  1172. if set(payload) != expected or not isinstance(payload['status_nodes'], list):
  1173. return self._format_error(meta)
  1174. nodes = []
  1175. for row in payload['status_nodes']:
  1176. translated = self._translate_exact(row, self.ORDER_DETAIL_STATUS_FIELDS)
  1177. if translated is None:
  1178. return self._format_error(meta)
  1179. state = row.get('state')
  1180. time_kind = row.get('time_kind')
  1181. if state not in self.ORDER_DETAIL_STATES or time_kind not in self.ORDER_DETAIL_TIME_KINDS:
  1182. return self._format_error(meta)
  1183. translated['状态'] = self.ORDER_DETAIL_STATES[state]
  1184. translated['时间类型'] = self.ORDER_DETAIL_TIME_KINDS[time_kind]
  1185. nodes.append(translated)
  1186. content['状态节点'] = nodes
  1187. for raw_key, chinese_key in (
  1188. ('order_info', '订单信息'), ('freight_info', '货运信息'),
  1189. ('package_summary', '箱单汇总'), ('trader_info', '进出口商'),
  1190. ):
  1191. translated = self._translate_exact(
  1192. payload.get(raw_key),
  1193. self.ORDER_DETAIL_FIELDS['overview'][raw_key],
  1194. )
  1195. if translated is None:
  1196. return self._format_error(meta)
  1197. content[chinese_key] = translated
  1198. elif section == 'inbound':
  1199. if set(payload) != {'summary', 'records'}:
  1200. return self._format_error(meta)
  1201. summary = self._translate_exact(
  1202. payload.get('summary'), self.ORDER_DETAIL_FIELDS['inbound_summary']
  1203. )
  1204. records = self._translate_order_detail_rows(section, payload.get('records'))
  1205. if summary is None or records is None:
  1206. return self._format_error(meta)
  1207. content['入库概况'] = summary
  1208. content['明细'] = records
  1209. pagination = self._order_detail_pagination(raw_meta)
  1210. if pagination is None:
  1211. return self._format_error(meta)
  1212. content['分页'] = pagination
  1213. elif section == 'delivery':
  1214. if set(payload) != {'summary', 'records'}:
  1215. return self._format_error(meta)
  1216. summary = self._translate_exact(
  1217. payload.get('summary'), self.ORDER_DETAIL_FIELDS['delivery_summary']
  1218. )
  1219. records = self._translate_order_detail_rows(section, payload.get('records'))
  1220. if summary is None or records is None:
  1221. return self._format_error(meta)
  1222. content['派送汇总'] = summary
  1223. content['明细'] = records
  1224. pagination = self._order_detail_pagination(raw_meta)
  1225. if pagination is None:
  1226. return self._format_error(meta)
  1227. content['分页'] = pagination
  1228. else:
  1229. if set(payload) != {'records'}:
  1230. return self._format_error(meta)
  1231. records = self._translate_order_detail_rows(section, payload.get('records'))
  1232. pagination = self._order_detail_pagination(raw_meta)
  1233. if records is None or pagination is None:
  1234. return self._format_error(meta)
  1235. content['明细'] = records
  1236. content['分页'] = pagination
  1237. text = json.dumps(content, ensure_ascii=False, indent=2)
  1238. return self._success_result(content, text, meta)
  1239. def _present_order_detail_all(self, payload):
  1240. expected_sections = set(self.ORDER_DETAIL_SECTIONS) - {'all'}
  1241. if set(payload) != expected_sections:
  1242. return None
  1243. overview = payload.get('overview')
  1244. if not isinstance(overview, dict) or set(overview) != {
  1245. 'status_nodes', 'order_info', 'freight_info', 'package_summary', 'trader_info',
  1246. } or not isinstance(overview['status_nodes'], list):
  1247. return None
  1248. overview_content = {'状态节点': []}
  1249. for row in overview['status_nodes']:
  1250. translated = self._translate_exact(row, self.ORDER_DETAIL_STATUS_FIELDS)
  1251. if translated is None:
  1252. return None
  1253. state = row.get('state')
  1254. time_kind = row.get('time_kind')
  1255. if state not in self.ORDER_DETAIL_STATES or time_kind not in self.ORDER_DETAIL_TIME_KINDS:
  1256. return None
  1257. translated['状态'] = self.ORDER_DETAIL_STATES[state]
  1258. translated['时间类型'] = self.ORDER_DETAIL_TIME_KINDS[time_kind]
  1259. overview_content['状态节点'].append(translated)
  1260. for raw_key, chinese_key in (
  1261. ('order_info', '订单信息'), ('freight_info', '货运信息'),
  1262. ('package_summary', '箱单汇总'), ('trader_info', '进出口商'),
  1263. ):
  1264. translated = self._translate_exact(
  1265. overview.get(raw_key), self.ORDER_DETAIL_FIELDS['overview'][raw_key]
  1266. )
  1267. if translated is None:
  1268. return None
  1269. overview_content[chinese_key] = translated
  1270. result = {'订单概览': overview_content}
  1271. for section, label in self.ORDER_DETAIL_SECTIONS.items():
  1272. if section in ('overview', 'all'):
  1273. continue
  1274. translated = self._present_order_detail_all_section(section, payload.get(section))
  1275. if translated is None:
  1276. return None
  1277. result[label] = translated
  1278. return result
  1279. def _present_order_detail_all_section(self, section, payload):
  1280. if not isinstance(payload, dict) or 'pagination' not in payload:
  1281. return None
  1282. pagination = self._order_detail_pagination(payload.get('pagination'))
  1283. if pagination is None:
  1284. return None
  1285. if section == 'inbound':
  1286. if set(payload) != {'summary', 'records', 'pagination'}:
  1287. return None
  1288. summary = self._translate_exact(
  1289. payload.get('summary'), self.ORDER_DETAIL_FIELDS['inbound_summary']
  1290. )
  1291. records = self._translate_order_detail_rows(section, payload.get('records'))
  1292. if summary is None or records is None:
  1293. return None
  1294. return {'入库概况': summary, '明细': records, '分页': pagination}
  1295. if section == 'delivery':
  1296. if set(payload) != {'summary', 'records', 'pagination'}:
  1297. return None
  1298. summary = self._translate_exact(
  1299. payload.get('summary'), self.ORDER_DETAIL_FIELDS['delivery_summary']
  1300. )
  1301. records = self._translate_order_detail_rows(section, payload.get('records'))
  1302. if summary is None or records is None:
  1303. return None
  1304. return {'派送汇总': summary, '明细': records, '分页': pagination}
  1305. if set(payload) != {'records', 'pagination'}:
  1306. return None
  1307. records = self._translate_order_detail_rows(section, payload.get('records'))
  1308. if records is None:
  1309. return None
  1310. return {'明细': records, '分页': pagination}
  1311. def _translate_order_detail_rows(self, section, records):
  1312. if not isinstance(records, list):
  1313. return None
  1314. mapping = self.ORDER_DETAIL_FIELDS.get(section)
  1315. if not isinstance(mapping, dict):
  1316. return None
  1317. translated_rows = []
  1318. for row in records:
  1319. translated = self._translate_exact(row, mapping)
  1320. if translated is None:
  1321. return None
  1322. if section == 'inbound':
  1323. mode = row.get('weight_mode')
  1324. if mode not in ('single_box', 'total'):
  1325. return None
  1326. translated[
  1327. '单箱入库重量' if mode == 'single_box' else '总重量KG'
  1328. ] = row.get('weight', '')
  1329. translated_rows.append(translated)
  1330. return translated_rows
  1331. @staticmethod
  1332. def _translate_exact(value, mapping):
  1333. if not isinstance(value, dict) or set(value) != set(mapping):
  1334. return None
  1335. result = {}
  1336. for key, label in mapping.items():
  1337. if label is None:
  1338. continue
  1339. item = value.get(key, '')
  1340. if isinstance(item, (dict, list)):
  1341. return None
  1342. result[label] = '' if item is None else item
  1343. return result
  1344. @staticmethod
  1345. def _order_detail_pagination(raw_meta):
  1346. if not isinstance(raw_meta, dict):
  1347. return None
  1348. if not all(key in raw_meta for key in ('page', 'limit', 'has_more')):
  1349. return None
  1350. page = raw_meta.get('page')
  1351. limit = raw_meta.get('limit')
  1352. has_more = raw_meta.get('has_more')
  1353. if isinstance(page, bool) or not isinstance(page, int) or page < 1:
  1354. return None
  1355. if isinstance(limit, bool) or not isinstance(limit, int) or limit < 1:
  1356. return None
  1357. if not isinstance(has_more, bool):
  1358. return None
  1359. return {'页码': page, '每页数量': limit, '是否还有更多': has_more}
  1360. # Map local tool exceptions without exposing internal error details.
  1361. def present_exception(self, tool_name, exception):
  1362. if not self.handles(tool_name):
  1363. return self._format_error()
  1364. message = str(exception or '').strip()
  1365. if isinstance(exception, ValueError):
  1366. label = self._find_field_label(tool_name, message)
  1367. return self._error_result(
  1368. 'MCP_1401',
  1369. self._parameter_message(label),
  1370. False,
  1371. )
  1372. if message == DEVICE_INVALID_MESSAGE:
  1373. return self._error_result('MCP_1101', DEVICE_INVALID_MESSAGE, False)
  1374. if message.lower().startswith('tool disabled'):
  1375. return self._error_result('MCP_1202', '工具当前不可用', False)
  1376. return self._error_result(
  1377. 'MCP_9001',
  1378. '工具调用失败,请稍后重试',
  1379. True,
  1380. )
  1381. def _present_table(self, tool_name, data, raw_meta, meta):
  1382. columns = data.get('columns')
  1383. records = data.get('records')
  1384. if not isinstance(columns, list) or not columns or not isinstance(records, list):
  1385. return self._format_error(meta)
  1386. allowed_columns = self.TABLE_COLUMNS.get(tool_name)
  1387. if not isinstance(allowed_columns, dict):
  1388. return self._format_error(meta)
  1389. if tool_name == 'query_customer_list':
  1390. expected = list(allowed_columns.keys())
  1391. actual = [
  1392. column.get('key') if isinstance(column, dict) else None
  1393. for column in columns
  1394. ]
  1395. if actual != expected or self._customer_pagination(raw_meta) is None:
  1396. return self._format_error(meta)
  1397. headers = []
  1398. keys = []
  1399. for column in columns:
  1400. if not isinstance(column, dict):
  1401. return self._format_error(meta)
  1402. key = column.get('key')
  1403. label = column.get('name')
  1404. if not isinstance(key, str) or not key.strip():
  1405. return self._format_error(meta)
  1406. if not isinstance(label, str) or not label.strip():
  1407. return self._format_error(meta)
  1408. key = key.strip()
  1409. definition = allowed_columns.get(key)
  1410. if not isinstance(definition, tuple) or not definition:
  1411. return self._format_error(meta)
  1412. keys.append(key)
  1413. header = {'label': definition[0]}
  1414. if len(definition) > 1:
  1415. header['description'] = definition[1]
  1416. headers.append(header)
  1417. rows = []
  1418. for record in records:
  1419. if not isinstance(record, dict):
  1420. return self._format_error(meta)
  1421. if tool_name == 'query_customer_list' and set(record) != set(keys):
  1422. return self._format_error(meta)
  1423. row = []
  1424. for key in keys:
  1425. value = '' if record.get(key) is None else record.get(key, '')
  1426. if tool_name == 'query_customer_list' and key == 'billing_modes':
  1427. value = self._customer_billing_modes(value)
  1428. if value is None:
  1429. return self._format_error(meta)
  1430. elif tool_name == 'query_customer_list' and isinstance(value, (dict, list)):
  1431. return self._format_error(meta)
  1432. row.append(value)
  1433. rows.append(row)
  1434. content = {
  1435. 'summary': self._safe_text(data.get('summary')),
  1436. 'headers': headers,
  1437. 'rows': rows,
  1438. }
  1439. if tool_name == 'query_customer_list':
  1440. content['display_rules'] = {
  1441. 'mode': 'complete',
  1442. 'allow_summary': False,
  1443. 'allow_omit_records': False,
  1444. 'allow_omit_empty_fields': False,
  1445. 'allow_rename_fields': False,
  1446. 'preserve_record_order': True,
  1447. 'required_field_count': len(headers),
  1448. 'returned_record_count': len(rows),
  1449. 'required_value_count': len(headers) * len(rows),
  1450. 'instruction': '最终回复必须逐条展示全部记录及全部17个字段,不得摘要、省略或改写',
  1451. }
  1452. tips = self._safe_tips(data.get('tips'))
  1453. if tips:
  1454. content['tips'] = tips
  1455. pagination = self._build_pagination(raw_meta)
  1456. if pagination:
  1457. content['pagination'] = pagination
  1458. return self._success_result(
  1459. content,
  1460. self._render_table(
  1461. content,
  1462. require_complete=tool_name == 'query_customer_list',
  1463. ),
  1464. meta,
  1465. )
  1466. def _present_remote_address_maintain(self, data, raw_meta, meta):
  1467. required = {
  1468. 'summary', 'mode_text', 'address_no', 'remote_name',
  1469. 'status_text', 'temp_key', 'columns', 'records',
  1470. }
  1471. if not isinstance(data, dict) or not required.issubset(set(data)):
  1472. return self._format_error(meta)
  1473. if set(data) - required - {'tips'}:
  1474. return self._format_error(meta)
  1475. if data.get('mode_text') not in ('新增', '编辑'):
  1476. return self._format_error(meta)
  1477. for key in ('summary', 'address_no', 'remote_name', 'status_text', 'temp_key'):
  1478. if not isinstance(data.get(key), str):
  1479. return self._format_error(meta)
  1480. columns = data.get('columns')
  1481. records = data.get('records')
  1482. expected = list(self.REMOTE_ADDRESS_DETAIL_COLUMNS)
  1483. if not isinstance(columns, list) or len(columns) != len(expected):
  1484. return self._format_error(meta)
  1485. if not isinstance(records, list):
  1486. return self._format_error(meta)
  1487. for index, column in enumerate(columns):
  1488. key, label = expected[index]
  1489. if (
  1490. not isinstance(column, dict)
  1491. or column.get('key') != key
  1492. or column.get('name') != label
  1493. ):
  1494. return self._format_error(meta)
  1495. keys = [item[0] for item in expected]
  1496. rows = []
  1497. for record in records:
  1498. if not isinstance(record, dict) or set(record) != set(keys):
  1499. return self._format_error(meta)
  1500. rows.append(['' if record.get(key) is None else record.get(key) for key in keys])
  1501. headers = [{'label': label} for _, label in expected]
  1502. table = {
  1503. 'summary': self._safe_text(data.get('summary')),
  1504. 'headers': headers,
  1505. 'rows': rows,
  1506. }
  1507. tips = self._safe_tips(data.get('tips'))
  1508. if tips:
  1509. table['tips'] = tips
  1510. content = {
  1511. 'summary': table['summary'],
  1512. 'mode_text': data['mode_text'],
  1513. 'address_no': data['address_no'],
  1514. 'remote_name': data['remote_name'],
  1515. 'status_text': data['status_text'],
  1516. 'headers': headers,
  1517. 'rows': rows,
  1518. }
  1519. if tips:
  1520. content['tips'] = tips
  1521. pagination = self._build_pagination(raw_meta)
  1522. if pagination:
  1523. content['pagination'] = pagination
  1524. return self._success_result(content, self._render_table(table), meta)
  1525. def _present_remote_address_save(self, data, meta):
  1526. required = {'summary', 'mode_text', 'address_no', 'remote_name', 'status_text'}
  1527. if not isinstance(data, dict) or set(data) != required:
  1528. return self._format_error(meta)
  1529. if data.get('mode_text') not in ('新增', '编辑'):
  1530. return self._format_error(meta)
  1531. if data.get('status_text') not in ('启用', '禁用'):
  1532. return self._format_error(meta)
  1533. address_no = data.get('address_no')
  1534. remote_name = data.get('remote_name')
  1535. summary = data.get('summary')
  1536. if (
  1537. not isinstance(address_no, str)
  1538. or not address_no.startswith('PY')
  1539. or not address_no[2:].isdigit()
  1540. or not isinstance(remote_name, str)
  1541. or not remote_name.strip()
  1542. or not isinstance(summary, str)
  1543. or not summary.strip()
  1544. ):
  1545. return self._format_error(meta)
  1546. content = {
  1547. '结果': summary.strip(),
  1548. '操作': data['mode_text'],
  1549. '偏远地址库编号': address_no,
  1550. '偏远地址库名称': remote_name.strip(),
  1551. '状态': data['status_text'],
  1552. }
  1553. return self._success_result(content, summary.strip(), meta)
  1554. @staticmethod
  1555. def _remote_address_save_message(message):
  1556. if message in (
  1557. '偏远地址库名称不能为空',
  1558. '偏远地址库名称过长',
  1559. '状态只能是启用或禁用',
  1560. '偏远地址库编号不正确',
  1561. '地址明细格式不正确',
  1562. '偏远地址库没有详情,请导入',
  1563. '编辑只修改名称和状态,不能同时提交地址明细',
  1564. ):
  1565. return True
  1566. if message.startswith('一次最多保存') and message.endswith('条地址明细'):
  1567. return True
  1568. if (
  1569. message.startswith('第')
  1570. and '条' in message
  1571. and len(message) <= 80
  1572. ):
  1573. return True
  1574. return False
  1575. @staticmethod
  1576. def _fuel_rate_message(message):
  1577. if not message or len(message) > 400:
  1578. return False
  1579. lowered = message.lower()
  1580. if 'sql' in lowered or 'exception' in lowered or '\\' in message:
  1581. return False
  1582. markers = (
  1583. '不正确', '不存在', '不能为空', '只能', '请添加', '请完善',
  1584. '对应多条', '不能大于', '不能同时', '重复', '过多', '未启用',
  1585. '草稿', '失败', '只适用于', '日期', '只新增', '最多新增', '不能修改', '不支持',
  1586. '已启用', '最多启用', '请选择',
  1587. )
  1588. return any(marker in message for marker in markers)
  1589. def _present_customer_payment_followup(self, data, raw_meta, meta):
  1590. if set(data) != {'columns', 'records'}:
  1591. return self._format_error(meta)
  1592. columns = data.get('columns')
  1593. records = data.get('records')
  1594. pagination = self._customer_pagination(raw_meta)
  1595. expected_keys = list(self.PAYMENT_FOLLOWUP_COLUMNS)
  1596. if not isinstance(columns, list) or not isinstance(records, list):
  1597. return self._format_error(meta)
  1598. actual_keys = []
  1599. for column in columns:
  1600. if (
  1601. not isinstance(column, dict)
  1602. or set(column) != {'key', 'name'}
  1603. or not isinstance(column.get('key'), str)
  1604. or not isinstance(column.get('name'), str)
  1605. or not column.get('name').strip()
  1606. ):
  1607. return self._format_error(meta)
  1608. actual_keys.append(column['key'])
  1609. if actual_keys != expected_keys or pagination is None:
  1610. return self._format_error(meta)
  1611. amount_keys = {
  1612. 'billed_unverified_amount', 'unbilled_amount',
  1613. 'overdue_unpaid_amount', 'billed_unverified_amount_cny',
  1614. 'unbilled_amount_cny', 'overdue_unpaid_amount_cny',
  1615. 'receipt_unverified_amount', 'current_balance',
  1616. 'credit_limit', 'bad_debt_total',
  1617. }
  1618. text_keys = {'customer_name', 'settlement_currency', 'contract_status'}
  1619. rows = []
  1620. for record in records:
  1621. if not isinstance(record, dict) or set(record) != set(expected_keys):
  1622. return self._format_error(meta)
  1623. row = []
  1624. for key in expected_keys:
  1625. value = record[key]
  1626. if key in amount_keys:
  1627. if (
  1628. isinstance(value, bool)
  1629. or not isinstance(value, (int, float))
  1630. or not math.isfinite(float(value))
  1631. ):
  1632. return self._format_error(meta)
  1633. elif key in text_keys:
  1634. if not isinstance(value, str):
  1635. return self._format_error(meta)
  1636. else:
  1637. value = self._payment_followup_monthly_summaries(value)
  1638. if value is None:
  1639. return self._format_error(meta)
  1640. row.append(value)
  1641. rows.append(row)
  1642. headers = [
  1643. {'label': self.PAYMENT_FOLLOWUP_COLUMNS[key]}
  1644. for key in expected_keys
  1645. ]
  1646. content = {
  1647. 'headers': headers,
  1648. 'rows': rows,
  1649. 'pagination': pagination,
  1650. 'display_rules': {
  1651. 'mode': 'complete',
  1652. 'allow_summary': False,
  1653. 'allow_omit_records': False,
  1654. 'allow_omit_empty_fields': False,
  1655. 'allow_rename_fields': False,
  1656. 'preserve_record_order': True,
  1657. 'required_field_count': len(headers),
  1658. 'returned_record_count': len(rows),
  1659. 'required_value_count': len(headers) * len(rows),
  1660. 'instruction': '最终回复必须逐条展示全部客户、全部字段和全部未回款月份汇总',
  1661. },
  1662. }
  1663. return self._success_result(
  1664. content,
  1665. self._render_table(content, require_complete=True),
  1666. meta,
  1667. )
  1668. def _payment_followup_monthly_summaries(self, summaries):
  1669. if not isinstance(summaries, list):
  1670. return None
  1671. expected = set(self.PAYMENT_FOLLOWUP_MONTHLY_COLUMNS)
  1672. translated = []
  1673. for summary in summaries:
  1674. if not isinstance(summary, dict) or set(summary) != expected:
  1675. return None
  1676. amount = summary.get('unverified_amount')
  1677. if (
  1678. isinstance(amount, bool)
  1679. or not isinstance(amount, (int, float))
  1680. or not math.isfinite(float(amount))
  1681. ):
  1682. return None
  1683. if (
  1684. not isinstance(summary.get('receivable_month'), str)
  1685. or not isinstance(summary.get('is_overdue'), bool)
  1686. ):
  1687. return None
  1688. item = {}
  1689. for key, label in self.PAYMENT_FOLLOWUP_MONTHLY_COLUMNS.items():
  1690. value = summary[key]
  1691. item[label] = value
  1692. translated.append(item)
  1693. return translated
  1694. def _present_customer_unverified_bill_details(self, data, raw_meta, meta):
  1695. if set(data) != {'columns', 'records'}:
  1696. return self._format_error(meta)
  1697. columns = data.get('columns')
  1698. records = data.get('records')
  1699. pagination = self._customer_pagination(raw_meta)
  1700. expected_keys = list(self.PAYMENT_FOLLOWUP_DETAIL_COLUMNS)
  1701. if not isinstance(columns, list) or not isinstance(records, list):
  1702. return self._format_error(meta)
  1703. actual_keys = []
  1704. for column in columns:
  1705. if (
  1706. not isinstance(column, dict)
  1707. or set(column) != {'key', 'name'}
  1708. or not isinstance(column.get('key'), str)
  1709. or not isinstance(column.get('name'), str)
  1710. or not column.get('name').strip()
  1711. ):
  1712. return self._format_error(meta)
  1713. actual_keys.append(column['key'])
  1714. if actual_keys != expected_keys or pagination is None:
  1715. return self._format_error(meta)
  1716. translated = self._payment_followup_details(records)
  1717. if translated is None:
  1718. return self._format_error(meta)
  1719. headers = [
  1720. {'label': self.PAYMENT_FOLLOWUP_DETAIL_COLUMNS[key]}
  1721. for key in expected_keys
  1722. ]
  1723. rows = [[item[header['label']] for header in headers] for item in translated]
  1724. content = {'headers': headers, 'rows': rows, 'pagination': pagination}
  1725. return self._success_result(content, self._render_table(content), meta)
  1726. def _payment_followup_details(self, details):
  1727. expected = set(self.PAYMENT_FOLLOWUP_DETAIL_COLUMNS)
  1728. translated = []
  1729. for detail in details:
  1730. if not isinstance(detail, dict) or set(detail) != expected:
  1731. return None
  1732. amount = detail.get('unverified_amount')
  1733. if (
  1734. isinstance(amount, bool)
  1735. or not isinstance(amount, (int, float))
  1736. or not math.isfinite(float(amount))
  1737. ):
  1738. return None
  1739. item = {}
  1740. for key, label in self.PAYMENT_FOLLOWUP_DETAIL_COLUMNS.items():
  1741. value = detail[key]
  1742. if key != 'unverified_amount' and not isinstance(value, str):
  1743. return None
  1744. item[label] = value
  1745. translated.append(item)
  1746. return translated
  1747. def _present_customer_payment_records(self, data, raw_meta, meta):
  1748. if set(data) != {'columns', 'records'}:
  1749. return self._format_error(meta)
  1750. columns = data.get('columns')
  1751. records = data.get('records')
  1752. pagination = self._customer_pagination(raw_meta)
  1753. expected_keys = list(self.PAYMENT_RECORD_COLUMNS)
  1754. if not isinstance(columns, list) or not isinstance(records, list):
  1755. return self._format_error(meta)
  1756. actual_keys = []
  1757. for column in columns:
  1758. if (
  1759. not isinstance(column, dict)
  1760. or set(column) != {'key', 'name'}
  1761. or not isinstance(column.get('key'), str)
  1762. or not isinstance(column.get('name'), str)
  1763. or not column.get('name').strip()
  1764. ):
  1765. return self._format_error(meta)
  1766. actual_keys.append(column['key'])
  1767. if actual_keys != expected_keys or pagination is None:
  1768. return self._format_error(meta)
  1769. amount_keys = {
  1770. 'original_received_amount', 'actual_received_amount',
  1771. 'verified_amount', 'unverified_amount',
  1772. }
  1773. rows = []
  1774. for record in records:
  1775. if not isinstance(record, dict) or set(record) != set(expected_keys):
  1776. return self._format_error(meta)
  1777. row = []
  1778. for key in expected_keys:
  1779. value = record[key]
  1780. if key in amount_keys:
  1781. if (
  1782. isinstance(value, bool)
  1783. or not isinstance(value, (int, float))
  1784. or not math.isfinite(float(value))
  1785. ):
  1786. return self._format_error(meta)
  1787. elif not isinstance(value, str):
  1788. return self._format_error(meta)
  1789. row.append(value)
  1790. rows.append(row)
  1791. headers = [
  1792. {'label': self.PAYMENT_RECORD_COLUMNS[key]}
  1793. for key in expected_keys
  1794. ]
  1795. content = {'headers': headers, 'rows': rows, 'pagination': pagination}
  1796. return self._success_result(content, self._render_table(content), meta)
  1797. def _present_order_receivable_cost_details(self, data, raw_meta, meta):
  1798. if set(data) != {'summary', 'columns', 'records'}:
  1799. return self._format_error(meta)
  1800. columns = data.get('columns')
  1801. records = data.get('records')
  1802. summary = data.get('summary')
  1803. pagination = self._customer_pagination(raw_meta)
  1804. expected_keys = list(self.RECEIVABLE_COST_COLUMNS)
  1805. expected_summary = list(self.RECEIVABLE_COST_SUMMARY)
  1806. if (
  1807. not isinstance(columns, list)
  1808. or not isinstance(records, list)
  1809. or not isinstance(summary, dict)
  1810. or set(summary) != set(expected_summary)
  1811. ):
  1812. return self._format_error(meta)
  1813. actual_keys = []
  1814. for column in columns:
  1815. if (
  1816. not isinstance(column, dict)
  1817. or set(column) != {'key', 'name'}
  1818. or not isinstance(column.get('key'), str)
  1819. or not isinstance(column.get('name'), str)
  1820. or not column.get('name').strip()
  1821. ):
  1822. return self._format_error(meta)
  1823. actual_keys.append(column['key'])
  1824. if actual_keys != expected_keys or pagination is None:
  1825. return self._format_error(meta)
  1826. summary_row = []
  1827. for key in expected_summary:
  1828. value = summary[key]
  1829. if (
  1830. isinstance(value, bool)
  1831. or not isinstance(value, (int, float))
  1832. or not math.isfinite(float(value))
  1833. ):
  1834. return self._format_error(meta)
  1835. summary_row.append(value)
  1836. numeric_keys = {
  1837. 'charge_weight', 'unit_price', 'receivable_original_amount',
  1838. 'receivable_amount_cny', 'settlement_amount_cny',
  1839. }
  1840. rows = []
  1841. for record in records:
  1842. if not isinstance(record, dict) or set(record) != set(expected_keys):
  1843. return self._format_error(meta)
  1844. row = []
  1845. for key in expected_keys:
  1846. value = record[key]
  1847. if key in numeric_keys:
  1848. if (
  1849. isinstance(value, bool)
  1850. or not isinstance(value, (int, float))
  1851. or not math.isfinite(float(value))
  1852. ):
  1853. return self._format_error(meta)
  1854. elif not isinstance(value, str):
  1855. return self._format_error(meta)
  1856. row.append(value)
  1857. rows.append(row)
  1858. headers = [
  1859. {'label': self.RECEIVABLE_COST_COLUMNS[key]}
  1860. for key in expected_keys
  1861. ]
  1862. content = {
  1863. 'summary': {
  1864. 'headers': [
  1865. {'label': self.RECEIVABLE_COST_SUMMARY[key]}
  1866. for key in expected_summary
  1867. ],
  1868. 'row': summary_row,
  1869. },
  1870. 'headers': headers,
  1871. 'rows': rows,
  1872. 'pagination': pagination,
  1873. }
  1874. return self._success_result(
  1875. content, self._render_order_receivable_cost_details(content), meta
  1876. )
  1877. @classmethod
  1878. def _render_order_receivable_cost_details(cls, content):
  1879. lines = ['订单汇总:']
  1880. summary = content['summary']
  1881. for header, value in zip(summary['headers'], summary['row']):
  1882. lines.append('- {0}: {1}'.format(header['label'], value))
  1883. lines.append('费用明细:')
  1884. lines.append(cls._render_table({
  1885. 'headers': content['headers'],
  1886. 'rows': content['rows'],
  1887. }))
  1888. return '\n'.join(lines)
  1889. def _present_receivable_cost_list(self, data, raw_meta, meta):
  1890. if set(data) != {'columns', 'records'}:
  1891. return self._format_error(meta)
  1892. columns = data.get('columns')
  1893. records = data.get('records')
  1894. pagination = self._customer_pagination(raw_meta)
  1895. expected_keys = list(self.RECEIVABLE_LIST_COLUMNS)
  1896. if (
  1897. not isinstance(columns, list)
  1898. or not isinstance(records, list)
  1899. or len(columns) != len(expected_keys)
  1900. or pagination is None
  1901. ):
  1902. return self._format_error(meta)
  1903. for column, key in zip(columns, expected_keys):
  1904. if column != {
  1905. 'key': key,
  1906. 'name': self.RECEIVABLE_LIST_COLUMNS[key],
  1907. }:
  1908. return self._format_error(meta)
  1909. amount_keys = {
  1910. 'original_amount', 'total_amount', 'billed_amount', 'unbilled_amount',
  1911. }
  1912. rows = []
  1913. for record in records:
  1914. if not isinstance(record, dict) or set(record) != set(expected_keys):
  1915. return self._format_error(meta)
  1916. row = []
  1917. for key in expected_keys:
  1918. value = record[key]
  1919. if key in amount_keys:
  1920. if not self._valid_amount_cell(value):
  1921. return self._format_error(meta)
  1922. elif not isinstance(value, str):
  1923. return self._format_error(meta)
  1924. row.append(value)
  1925. rows.append(row)
  1926. headers = [
  1927. {'label': self.RECEIVABLE_LIST_COLUMNS[key]}
  1928. for key in expected_keys
  1929. ]
  1930. content = {'headers': headers, 'rows': rows, 'pagination': pagination}
  1931. return self._success_result(content, self._render_table(content), meta)
  1932. def _present_payable_cost_list(self, data, raw_meta, meta):
  1933. if set(data) != {'business_type', 'company_currency', 'columns', 'records'}:
  1934. return self._format_error(meta)
  1935. business_type = data.get('business_type')
  1936. company_currency = data.get('company_currency')
  1937. if (
  1938. isinstance(business_type, bool)
  1939. or not isinstance(business_type, int)
  1940. or business_type not in self.PAYABLE_COST_COLUMNS
  1941. or not isinstance(company_currency, str)
  1942. or not company_currency.strip()
  1943. ):
  1944. return self._format_error(meta)
  1945. pagination = self._customer_pagination(raw_meta)
  1946. columns = data.get('columns')
  1947. records = data.get('records')
  1948. expected = list(self.PAYABLE_COST_COLUMNS[business_type])
  1949. if business_type == 2:
  1950. expected = [
  1951. (key, name.replace('%s', company_currency.strip().upper()))
  1952. for key, name in expected
  1953. ]
  1954. if (
  1955. not isinstance(columns, list)
  1956. or not isinstance(records, list)
  1957. or len(columns) != len(expected)
  1958. or pagination is None
  1959. ):
  1960. return self._format_error(meta)
  1961. for column, (key, name) in zip(columns, expected):
  1962. if column != {'key': key, 'name': name}:
  1963. return self._format_error(meta)
  1964. rows = []
  1965. expected_keys = [key for key, _ in expected]
  1966. for record in records:
  1967. if not isinstance(record, dict) or set(record) != set(expected_keys):
  1968. return self._format_error(meta)
  1969. row = []
  1970. for key in expected_keys:
  1971. value = record[key]
  1972. if key in self.PAYABLE_AMOUNT_KEYS:
  1973. if (
  1974. isinstance(value, bool)
  1975. or not isinstance(value, (int, float))
  1976. or not math.isfinite(float(value))
  1977. ):
  1978. return self._format_error(meta)
  1979. elif not isinstance(value, str):
  1980. return self._format_error(meta)
  1981. row.append(value)
  1982. rows.append(row)
  1983. headers = [{'label': name} for _, name in expected]
  1984. content = {
  1985. 'business_type': business_type,
  1986. 'headers': headers,
  1987. 'rows': rows,
  1988. 'pagination': pagination,
  1989. }
  1990. return self._success_result(content, self._render_table(content), meta)
  1991. def _present_arrange_destination_trailer(self, data, meta):
  1992. expected = ('trailer_number', 'bl_number', 'container_code')
  1993. if set(data) != set(expected):
  1994. return self._format_error(meta)
  1995. for key in expected:
  1996. if not isinstance(data.get(key), str):
  1997. return self._format_error(meta)
  1998. content = {
  1999. '拖车单号': data['trailer_number'],
  2000. '提单号': data['bl_number'],
  2001. '柜号': data['container_code'],
  2002. }
  2003. text = '拖车单号:{0};提单号:{1};柜号:{2}'.format(
  2004. data['trailer_number'], data['bl_number'], data['container_code']
  2005. )
  2006. return self._success_result(content, text, meta)
  2007. def _present_destination_trailer_list(self, data, raw_meta, meta):
  2008. if set(data) != {'trailer_stage', 'columns', 'records'}:
  2009. return self._format_error(meta)
  2010. trailer_stage = data.get('trailer_stage')
  2011. if trailer_stage == 'DO单制作':
  2012. expected = list(self.DESTINATION_TRAILER_COLUMNS['do'])
  2013. elif trailer_stage in self.DESTINATION_TRAILER_PROGRESS_STAGES:
  2014. expected = list(self.DESTINATION_TRAILER_COLUMNS['progress'])
  2015. else:
  2016. return self._format_error(meta)
  2017. pagination = self._customer_pagination(raw_meta)
  2018. columns = data.get('columns')
  2019. records = data.get('records')
  2020. if (
  2021. not isinstance(columns, list)
  2022. or not isinstance(records, list)
  2023. or len(columns) != len(expected)
  2024. or pagination is None
  2025. ):
  2026. return self._format_error(meta)
  2027. for column, (key, name) in zip(columns, expected):
  2028. if column != {'key': key, 'name': name}:
  2029. return self._format_error(meta)
  2030. rows = []
  2031. expected_keys = [key for key, _ in expected]
  2032. for record in records:
  2033. if not isinstance(record, dict) or set(record) != set(expected_keys):
  2034. return self._format_error(meta)
  2035. row = []
  2036. for key in expected_keys:
  2037. value = record[key]
  2038. if key in self.DESTINATION_TRAILER_FILE_KEYS:
  2039. if value != '' and not self._valid_http_url(value):
  2040. return self._format_error(meta)
  2041. elif not isinstance(value, str):
  2042. return self._format_error(meta)
  2043. row.append(value)
  2044. rows.append(row)
  2045. headers = [{'label': name} for _, name in expected]
  2046. content = {
  2047. 'trailer_stage': trailer_stage,
  2048. 'headers': headers,
  2049. 'rows': rows,
  2050. 'pagination': pagination,
  2051. }
  2052. return self._success_result(content, self._render_table(content), meta)
  2053. def _present_order_abnormal_list(self, data, raw_meta, meta):
  2054. if set(data) != {'abnormal_status', 'columns', 'records'}:
  2055. return self._format_error(meta)
  2056. abnormal_status = data.get('abnormal_status')
  2057. if abnormal_status not in self.ORDER_ABNORMAL_STATUSES:
  2058. return self._format_error(meta)
  2059. expected = list(self.ORDER_ABNORMAL_COLUMNS)
  2060. pagination = self._customer_pagination(raw_meta)
  2061. columns = data.get('columns')
  2062. records = data.get('records')
  2063. if (
  2064. not isinstance(columns, list)
  2065. or not isinstance(records, list)
  2066. or len(columns) != len(expected)
  2067. or pagination is None
  2068. ):
  2069. return self._format_error(meta)
  2070. for column, (key, name) in zip(columns, expected):
  2071. if column != {'key': key, 'name': name}:
  2072. return self._format_error(meta)
  2073. rows = []
  2074. expected_keys = [key for key, _ in expected]
  2075. for record in records:
  2076. if not isinstance(record, dict) or set(record) != set(expected_keys):
  2077. return self._format_error(meta)
  2078. row = []
  2079. for key in expected_keys:
  2080. value = record[key]
  2081. if not isinstance(value, str):
  2082. return self._format_error(meta)
  2083. row.append(value)
  2084. rows.append(row)
  2085. headers = [{'label': name} for _, name in expected]
  2086. content = {
  2087. 'abnormal_status': abnormal_status,
  2088. 'headers': headers,
  2089. 'rows': rows,
  2090. 'pagination': pagination,
  2091. }
  2092. return self._success_result(content, self._render_table(content), meta)
  2093. def _present_goods_tax(self, data, meta):
  2094. expected = {
  2095. 'estimated_tax', 'sku', 'departure_country', 'destination_country',
  2096. }
  2097. if set(data) != expected:
  2098. return self._format_error(meta)
  2099. amount = data.get('estimated_tax')
  2100. sku = data.get('sku')
  2101. departure = data.get('departure_country')
  2102. destination = data.get('destination_country')
  2103. if (
  2104. isinstance(amount, bool) or not isinstance(amount, (int, float))
  2105. or not math.isfinite(float(amount))
  2106. or not isinstance(sku, str) or not sku.strip()
  2107. or not self._country_code(departure)
  2108. or not self._country_code(destination)
  2109. ):
  2110. return self._format_error(meta)
  2111. content = {
  2112. 'estimated_tax': amount,
  2113. 'sku': sku,
  2114. 'departure_country': departure,
  2115. 'destination_country': destination,
  2116. }
  2117. text = '预计关税金额: {0}'.format(amount)
  2118. return self._success_result(content, text, meta)
  2119. @staticmethod
  2120. def _country_code(value):
  2121. return (
  2122. isinstance(value, str)
  2123. and value.isalpha()
  2124. and value.isupper()
  2125. and 2 <= len(value) <= 3
  2126. )
  2127. def _present_goods_info(self, data, raw_meta, meta):
  2128. if set(data) != {'columns', 'records'}:
  2129. return self._format_error(meta)
  2130. expected = list(self.GOODS_INFO_COLUMNS)
  2131. pagination = self._customer_pagination(raw_meta)
  2132. columns = data.get('columns')
  2133. records = data.get('records')
  2134. if (
  2135. not isinstance(columns, list)
  2136. or not isinstance(records, list)
  2137. or len(columns) != len(expected)
  2138. or pagination is None
  2139. ):
  2140. return self._format_error(meta)
  2141. for column, (key, name) in zip(columns, expected):
  2142. if column != {'key': key, 'name': name}:
  2143. return self._format_error(meta)
  2144. rows = []
  2145. expected_keys = [key for key, _ in expected]
  2146. for record in records:
  2147. if not isinstance(record, dict) or set(record) != set(expected_keys):
  2148. return self._format_error(meta)
  2149. row = []
  2150. for key in expected_keys:
  2151. value = record[key]
  2152. if not isinstance(value, str):
  2153. return self._format_error(meta)
  2154. row.append(value)
  2155. rows.append(row)
  2156. headers = [{'label': name} for _, name in expected]
  2157. content = {
  2158. 'headers': headers,
  2159. 'rows': rows,
  2160. 'pagination': pagination,
  2161. }
  2162. return self._success_result(content, self._render_table(content), meta)
  2163. def _present_receive_volume_list(self, data, raw_meta, meta):
  2164. if set(data) != {'columns', 'records'}:
  2165. return self._format_error(meta)
  2166. expected = list(self.RECEIVE_VOLUME_COLUMNS)
  2167. pagination = self._customer_pagination(raw_meta)
  2168. columns = data.get('columns')
  2169. records = data.get('records')
  2170. if (
  2171. not isinstance(columns, list)
  2172. or not isinstance(records, list)
  2173. or len(columns) != len(expected)
  2174. or pagination is None
  2175. ):
  2176. return self._format_error(meta)
  2177. for column, (key, name) in zip(columns, expected):
  2178. if column != {'key': key, 'name': name}:
  2179. return self._format_error(meta)
  2180. rows = []
  2181. expected_keys = [key for key, _ in expected]
  2182. for record in records:
  2183. if not isinstance(record, dict) or set(record) != set(expected_keys):
  2184. return self._format_error(meta)
  2185. row = []
  2186. for key in expected_keys:
  2187. value = record[key]
  2188. if key == 'inbound_volume':
  2189. if not self._valid_volume_cell(value):
  2190. return self._format_error(meta)
  2191. row.append(value)
  2192. continue
  2193. if not isinstance(value, str):
  2194. return self._format_error(meta)
  2195. row.append(value)
  2196. rows.append(row)
  2197. headers = [{'label': name} for _, name in expected]
  2198. content = {
  2199. 'headers': headers,
  2200. 'rows': rows,
  2201. 'pagination': pagination,
  2202. }
  2203. return self._success_result(content, self._render_table(content), meta)
  2204. def _present_container_timeliness_list(self, data, raw_meta, meta):
  2205. if set(data) != {'columns', 'records'}:
  2206. return self._format_error(meta)
  2207. expected = list(self.CONTAINER_TIMELINESS_COLUMNS)
  2208. pagination = self._customer_pagination(raw_meta)
  2209. columns = data.get('columns')
  2210. records = data.get('records')
  2211. if (
  2212. not isinstance(columns, list)
  2213. or not isinstance(records, list)
  2214. or len(columns) != len(expected)
  2215. or pagination is None
  2216. ):
  2217. return self._format_error(meta)
  2218. for column, (key, name) in zip(columns, expected):
  2219. if column != {'key': key, 'name': name}:
  2220. return self._format_error(meta)
  2221. rows = []
  2222. expected_keys = [key for key, _ in expected]
  2223. for record in records:
  2224. if not isinstance(record, dict) or set(record) != set(expected_keys):
  2225. return self._format_error(meta)
  2226. row = []
  2227. for key in expected_keys:
  2228. value = record[key]
  2229. if not isinstance(value, str):
  2230. return self._format_error(meta)
  2231. row.append(value)
  2232. rows.append(row)
  2233. headers = [{'label': name} for _, name in expected]
  2234. content = {
  2235. 'headers': headers,
  2236. 'rows': rows,
  2237. 'pagination': pagination,
  2238. }
  2239. return self._success_result(content, self._render_table(content), meta)
  2240. def _present_headhaul_document_list(self, data, raw_meta, meta):
  2241. if set(data) != {'columns', 'records'}:
  2242. return self._format_error(meta)
  2243. expected = list(self.HEADHAUL_DOCUMENT_COLUMNS)
  2244. pagination = self._customer_pagination(raw_meta)
  2245. columns = data.get('columns')
  2246. records = data.get('records')
  2247. if (
  2248. not isinstance(columns, list)
  2249. or not isinstance(records, list)
  2250. or len(columns) != len(expected)
  2251. or pagination is None
  2252. ):
  2253. return self._format_error(meta)
  2254. for column, (key, name) in zip(columns, expected):
  2255. if column != {'key': key, 'name': name}:
  2256. return self._format_error(meta)
  2257. rows = []
  2258. expected_keys = [key for key, _ in expected]
  2259. for record in records:
  2260. if not isinstance(record, dict):
  2261. return self._format_error(meta)
  2262. if set(record) != set(expected_keys + ['document_ref']):
  2263. return self._format_error(meta)
  2264. document_ref = record.get('document_ref')
  2265. if not isinstance(document_ref, str) or not document_ref.strip():
  2266. return self._format_error(meta)
  2267. if not document_ref.startswith('mhdd_'):
  2268. return self._format_error(meta)
  2269. row = []
  2270. for key in expected_keys:
  2271. value = record[key]
  2272. if key == 'safe_url':
  2273. if value != '' and not self._valid_http_url(value):
  2274. return self._format_error(meta)
  2275. row.append(value)
  2276. continue
  2277. if not isinstance(value, str):
  2278. return self._format_error(meta)
  2279. row.append(value)
  2280. row.append(document_ref.strip())
  2281. rows.append(row)
  2282. headers = [{'label': name} for _, name in expected]
  2283. headers.append({'label': '文档身份'})
  2284. content = {
  2285. 'headers': headers,
  2286. 'rows': rows,
  2287. 'pagination': pagination,
  2288. }
  2289. return self._success_result(content, self._render_table(content), meta)
  2290. def _present_headhaul_prepare(self, data, meta):
  2291. expected = {
  2292. 'upload_token', 'upload_url', 'expires_in',
  2293. }
  2294. if set(data) != expected:
  2295. return self._format_error(meta)
  2296. upload_token = data.get('upload_token')
  2297. upload_url = data.get('upload_url')
  2298. expires_in = data.get('expires_in')
  2299. if (
  2300. not isinstance(upload_token, str) or not upload_token.strip()
  2301. or not isinstance(upload_url, str)
  2302. or upload_url != '/mcp/upload-headhaul-document'
  2303. or isinstance(expires_in, bool)
  2304. or not isinstance(expires_in, int)
  2305. or expires_in <= 0
  2306. ):
  2307. return self._format_error(meta)
  2308. from services.headhaul_document_app import public_upload_url
  2309. posted_url = public_upload_url()
  2310. content = {
  2311. 'upload_token': upload_token.strip(),
  2312. 'upload_url': posted_url,
  2313. 'expires_in': expires_in,
  2314. }
  2315. result_meta = dict(meta)
  2316. result_meta['upload_token'] = upload_token.strip()
  2317. text = (
  2318. '请把对话窗口里已添加的文件 POST 到上传地址,并带上上传令牌。'
  2319. '不要使用选文件框,也不要把文件放进工具参数。'
  2320. '上传地址:{0}'
  2321. ).format(posted_url)
  2322. return self._success_result(content, text, result_meta)
  2323. def _present_headhaul_save(self, data, meta):
  2324. if set(data) != {'document_ref'}:
  2325. return self._format_error(meta)
  2326. document_ref = data.get('document_ref')
  2327. if (
  2328. not isinstance(document_ref, str)
  2329. or not document_ref.strip()
  2330. or not document_ref.startswith('mhdd_')
  2331. ):
  2332. return self._format_error(meta)
  2333. content = {'文档身份': document_ref.strip(), '结果': '已保存'}
  2334. return self._success_result(content, '头程文档已保存', meta)
  2335. def _present_headhaul_delete(self, data, meta):
  2336. if set(data) != {'result'}:
  2337. return self._format_error(meta)
  2338. result = data.get('result')
  2339. if result != '已删除':
  2340. return self._format_error(meta)
  2341. content = {'结果': '已删除'}
  2342. return self._success_result(content, '头程文档已删除', meta)
  2343. @staticmethod
  2344. def _valid_volume_cell(value):
  2345. return (
  2346. not isinstance(value, bool)
  2347. and isinstance(value, (int, float))
  2348. and math.isfinite(float(value))
  2349. )
  2350. @staticmethod
  2351. def _valid_amount_cell(value):
  2352. if not isinstance(value, dict) or set(value) != {'amount', 'currency'}:
  2353. return False
  2354. amount = value.get('amount')
  2355. return (
  2356. not isinstance(amount, bool)
  2357. and isinstance(amount, (int, float))
  2358. and math.isfinite(float(amount))
  2359. and isinstance(value.get('currency'), str)
  2360. )
  2361. def _present_receivable_cost_filter_options(self, data, raw_meta, meta):
  2362. if set(data) != {'records'}:
  2363. return self._format_error(meta)
  2364. records = data.get('records')
  2365. pagination = self._customer_pagination(raw_meta)
  2366. if not isinstance(records, list) or pagination is None:
  2367. return self._format_error(meta)
  2368. rows = []
  2369. for record in records:
  2370. if not isinstance(record, dict) or set(record) != {
  2371. 'value', 'label', 'code',
  2372. }:
  2373. return self._format_error(meta)
  2374. value = record.get('value')
  2375. label = record.get('label')
  2376. code = record.get('code')
  2377. if (
  2378. isinstance(value, bool) or not isinstance(value, int)
  2379. or not isinstance(label, str) or not label.strip()
  2380. or not isinstance(code, str)
  2381. ):
  2382. return self._format_error(meta)
  2383. rows.append([value, label.strip(), code.strip()])
  2384. content = {
  2385. 'headers': [
  2386. {'label': '可传值'}, {'label': '显示名称'}, {'label': '业务编码'},
  2387. ],
  2388. 'rows': rows,
  2389. 'pagination': pagination,
  2390. }
  2391. return self._success_result(content, self._render_table(content), meta)
  2392. @staticmethod
  2393. def _customer_billing_modes(value):
  2394. if not isinstance(value, list):
  2395. return None
  2396. result = []
  2397. business_types = set()
  2398. for item in value:
  2399. if not isinstance(item, dict) or set(item) != {
  2400. 'business_type', 'settlement_mode',
  2401. }:
  2402. return None
  2403. business_type = item.get('business_type')
  2404. settlement_mode = item.get('settlement_mode')
  2405. if (
  2406. not isinstance(business_type, str) or not business_type.strip()
  2407. or not isinstance(settlement_mode, str) or not settlement_mode.strip()
  2408. or business_type in business_types
  2409. ):
  2410. return None
  2411. business_types.add(business_type)
  2412. result.append({
  2413. '业务类型': business_type.strip(),
  2414. '结算模式': settlement_mode.strip(),
  2415. })
  2416. return result
  2417. def _present_customer_options(self, data, raw_meta, meta):
  2418. if set(data) != {'records'}:
  2419. return self._format_error(meta)
  2420. records = data.get('records')
  2421. pagination = self._customer_pagination(raw_meta)
  2422. if not isinstance(records, list) or pagination is None:
  2423. return self._format_error(meta)
  2424. rows = []
  2425. for record in records:
  2426. if not isinstance(record, dict) or set(record) != {'value', 'label', 'code'}:
  2427. return self._format_error(meta)
  2428. value = record.get('value')
  2429. label = record.get('label')
  2430. code = record.get('code')
  2431. if (
  2432. isinstance(value, bool) or not isinstance(value, int) or value < 1
  2433. or not isinstance(label, str) or not label.strip()
  2434. or not isinstance(code, str)
  2435. ):
  2436. return self._format_error(meta)
  2437. rows.append([value, label.strip(), code.strip()])
  2438. content = {
  2439. 'headers': [
  2440. {'label': '可传值'}, {'label': '显示名称'}, {'label': '业务编码'},
  2441. ],
  2442. 'rows': rows,
  2443. 'pagination': pagination,
  2444. }
  2445. return self._success_result(content, self._render_table(content), meta)
  2446. @staticmethod
  2447. def _customer_pagination(raw_meta):
  2448. if not isinstance(raw_meta, dict):
  2449. return None
  2450. required = {'page', 'limit', 'has_more'}
  2451. if (
  2452. not required.issubset(raw_meta)
  2453. or not set(raw_meta).issubset(required | {'request_id'})
  2454. ):
  2455. return None
  2456. page = raw_meta.get('page')
  2457. limit = raw_meta.get('limit')
  2458. has_more = raw_meta.get('has_more')
  2459. if (
  2460. isinstance(page, bool) or not isinstance(page, int)
  2461. or page < 1 or page > 100
  2462. or isinstance(limit, bool) or not isinstance(limit, int)
  2463. or limit < 1 or limit > 100
  2464. or not isinstance(has_more, bool)
  2465. ):
  2466. return None
  2467. return {'page': page, 'limit': limit, 'has_more': has_more}
  2468. def _present_outbound_detail(self, tool_name, data, raw_meta, meta):
  2469. summary = data.get('summary')
  2470. columns = data.get('columns')
  2471. records = data.get('records')
  2472. if not isinstance(summary, dict):
  2473. return self._format_error(meta)
  2474. if not isinstance(columns, list) or not columns or not isinstance(records, list):
  2475. return self._format_error(meta)
  2476. if any(key not in summary for key in self.OUTBOUND_DETAIL_SUMMARY):
  2477. return self._format_error(meta)
  2478. allowed_columns = self.TABLE_COLUMNS.get(tool_name)
  2479. headers = []
  2480. keys = []
  2481. for column in columns:
  2482. if not isinstance(column, dict):
  2483. return self._format_error(meta)
  2484. key = column.get('key')
  2485. definition = allowed_columns.get(key) if isinstance(key, str) else None
  2486. if not isinstance(definition, tuple) or not definition:
  2487. return self._format_error(meta)
  2488. keys.append(key)
  2489. headers.append({'label': definition[0]})
  2490. rows = []
  2491. for record in records:
  2492. if not isinstance(record, dict):
  2493. return self._format_error(meta)
  2494. row = []
  2495. for key in keys:
  2496. value = '' if record.get(key) is None else record.get(key, '')
  2497. if key == 'customs_files':
  2498. if not isinstance(value, list):
  2499. return self._format_error(meta)
  2500. files = []
  2501. for item in value:
  2502. if not isinstance(item, dict):
  2503. return self._format_error(meta)
  2504. files.append({
  2505. '文件名称': item.get('file_name', ''),
  2506. '文件类型': item.get('file_type', ''),
  2507. '文件链接': item.get('file_url', ''),
  2508. })
  2509. value = files
  2510. row.append(value)
  2511. rows.append(row)
  2512. summary_keys = list(self.OUTBOUND_DETAIL_SUMMARY.keys())
  2513. content = {
  2514. 'summary': {
  2515. 'headers': [
  2516. {'label': self.OUTBOUND_DETAIL_SUMMARY[key]}
  2517. for key in summary_keys
  2518. ],
  2519. 'row': [summary.get(key, '') for key in summary_keys],
  2520. },
  2521. 'details': {
  2522. 'headers': headers,
  2523. 'rows': rows,
  2524. },
  2525. }
  2526. tips = self._safe_tips(data.get('tips'))
  2527. if tips:
  2528. content['details']['tips'] = tips
  2529. pagination = self._build_pagination(raw_meta)
  2530. if pagination:
  2531. content['details']['pagination'] = pagination
  2532. return self._success_result(
  2533. content,
  2534. self._render_outbound_detail(content),
  2535. meta,
  2536. )
  2537. @classmethod
  2538. def _render_outbound_detail(cls, content):
  2539. lines = ['排舱汇总:']
  2540. summary = content['summary']
  2541. for header, value in zip(summary['headers'], summary['row']):
  2542. lines.append('- {0}: {1}'.format(header['label'], value))
  2543. lines.append('订单明细:')
  2544. details = content['details']
  2545. lines.append('表头共 {0} 列:'.format(len(details['headers'])))
  2546. for index, header in enumerate(details['headers'], start=1):
  2547. lines.append('{0}. {1}'.format(index, header['label']))
  2548. for index, row in enumerate(details['rows'], start=1):
  2549. lines.append('记录 {0}:'.format(index))
  2550. for header, value in zip(details['headers'], row):
  2551. if isinstance(value, (dict, list)):
  2552. value = json.dumps(value, ensure_ascii=False)
  2553. lines.append('- {0}: {1}'.format(header['label'], value))
  2554. tips = details.get('tips') or []
  2555. if tips:
  2556. lines.append('提示:{0}'.format(';'.join(tips)))
  2557. return '\n'.join(lines)
  2558. def _present_options(self, data, raw_meta, meta):
  2559. records = data.get('records')
  2560. if not isinstance(records, list):
  2561. return self._format_error(meta)
  2562. rows = []
  2563. for record in records:
  2564. if not isinstance(record, dict):
  2565. return self._format_error(meta)
  2566. rows.append([
  2567. record.get('value', ''),
  2568. self._safe_text(record.get('label')),
  2569. self._safe_text(record.get('code')),
  2570. ])
  2571. content = {
  2572. 'headers': [
  2573. {'label': '可传值'},
  2574. {'label': '显示名称'},
  2575. {'label': '业务编码'},
  2576. ],
  2577. 'rows': rows,
  2578. }
  2579. pagination = self._build_pagination(raw_meta)
  2580. if pagination:
  2581. content['pagination'] = pagination
  2582. return self._success_result(content, self._render_table(content), meta)
  2583. def _present_inbound_receipt_templates(self, data, meta):
  2584. if set(data) != {'receipt_kind', 'templates'}:
  2585. return self._format_error(meta)
  2586. kind = data.get('receipt_kind')
  2587. templates = data.get('templates')
  2588. if kind not in ('直送入仓单', '非直送入仓单') or not isinstance(templates, list):
  2589. return self._format_error(meta)
  2590. if not templates:
  2591. return self._format_error(meta)
  2592. rows = []
  2593. for item in templates:
  2594. if not isinstance(item, dict) or set(item) != {'name', 'template_code'}:
  2595. return self._format_error(meta)
  2596. name = item.get('name')
  2597. code = item.get('template_code')
  2598. if (
  2599. not isinstance(name, str) or not name.strip()
  2600. or not isinstance(code, str) or not code.strip()
  2601. ):
  2602. return self._format_error(meta)
  2603. rows.append([name.strip(), code.strip()])
  2604. content = {
  2605. 'receipt_kind': kind,
  2606. 'summary': kind,
  2607. 'headers': [
  2608. {'label': '模板名称'},
  2609. {'label': '模板编码'},
  2610. ],
  2611. 'rows': rows,
  2612. }
  2613. return self._success_result(content, self._render_table(content), meta)
  2614. def _present_export_submission(self, data, meta):
  2615. expected = {'task_ref', 'status', 'retry_after_seconds'}
  2616. if set(data) != expected:
  2617. return self._format_error(meta)
  2618. task_ref = data.get('task_ref')
  2619. retry_after = data.get('retry_after_seconds')
  2620. if (
  2621. not isinstance(task_ref, str) or not task_ref.strip()
  2622. or data.get('status') != 'queued'
  2623. or isinstance(retry_after, bool)
  2624. or not isinstance(retry_after, int)
  2625. or retry_after <= 0
  2626. ):
  2627. return self._format_error(meta)
  2628. task = {
  2629. 'task_ref': task_ref.strip(),
  2630. 'status': 'queued',
  2631. 'retry_after_seconds': retry_after,
  2632. }
  2633. content = {
  2634. 'message': '导出任务已提交',
  2635. 'task': task,
  2636. }
  2637. text = (
  2638. '导出任务已提交\n'
  2639. '- 任务引用: {0}\n'
  2640. '- 建议 {1} 秒后单独查询任务状态'
  2641. ).format(task['task_ref'], retry_after)
  2642. return self._success_result(content, text, meta)
  2643. def _present_export_task(self, data, meta):
  2644. task_ref = data.get('task_ref')
  2645. status = data.get('status')
  2646. if (
  2647. not isinstance(task_ref, str)
  2648. or not task_ref.strip()
  2649. or status not in ('queued', 'running', 'completed', 'failed')
  2650. ):
  2651. return self._format_error(meta)
  2652. task = {'task_ref': task_ref.strip(), 'status': status}
  2653. if status in ('queued', 'running'):
  2654. if set(data) != {'task_ref', 'status', 'retry_after_seconds'}:
  2655. return self._format_error(meta)
  2656. retry_after = data.get('retry_after_seconds')
  2657. if (
  2658. isinstance(retry_after, bool)
  2659. or not isinstance(retry_after, int)
  2660. or retry_after <= 0
  2661. ):
  2662. return self._format_error(meta)
  2663. task['retry_after_seconds'] = retry_after
  2664. label = '等待中' if status == 'queued' else '生成中'
  2665. content = {'message': '导出任务{0}'.format(label), 'task': task}
  2666. text = (
  2667. '导出任务{0}\n- 任务引用: {1}\n- 建议 {2} 秒后再次查询'
  2668. ).format(label, task['task_ref'], retry_after)
  2669. return self._success_result(content, text, meta)
  2670. if status == 'failed':
  2671. if set(data) != {'task_ref', 'status'}:
  2672. return self._format_error(meta)
  2673. content = {'message': '导出任务失败,请重新提交', 'task': task}
  2674. text = '导出任务失败,请重新提交'
  2675. return self._success_result(content, text, meta)
  2676. if set(data) != {'task_ref', 'status', 'files'}:
  2677. return self._format_error(meta)
  2678. files = data.get('files')
  2679. if not isinstance(files, list) or not files:
  2680. return self._format_error(meta)
  2681. safe_files = []
  2682. for item in files:
  2683. if not isinstance(item, dict) or set(item) != {'label', 'url'}:
  2684. return self._format_error(meta)
  2685. label = item.get('label')
  2686. url = item.get('url')
  2687. if (
  2688. not isinstance(label, str) or not label.strip()
  2689. or not self._valid_http_url(url)
  2690. ):
  2691. return self._format_error(meta)
  2692. safe_files.append({'label': label.strip(), 'url': url.strip()})
  2693. content = {
  2694. 'message': '文件已生成',
  2695. 'task': task,
  2696. 'files': safe_files,
  2697. }
  2698. lines = ['文件已生成']
  2699. for item in safe_files:
  2700. lines.append('- {0}: {1}'.format(item['label'], item['url']))
  2701. return self._success_result(content, '\n'.join(lines), meta)
  2702. def _valid_http_url(self, value):
  2703. if not isinstance(value, str) or not value.strip():
  2704. return False
  2705. if value != value.strip() or '\\' in value or any(
  2706. character.isspace() or ord(character) < 32 or ord(character) == 127
  2707. for character in value
  2708. ):
  2709. return False
  2710. try:
  2711. parsed = urlparse(value)
  2712. port = parsed.port
  2713. return (
  2714. parsed.scheme in ('http', 'https')
  2715. and bool(parsed.hostname)
  2716. and parsed.username is None
  2717. and parsed.password is None
  2718. and (port is None or 0 < port <= 65535)
  2719. )
  2720. except ValueError:
  2721. return False
  2722. def _business_error(self, tool_name, code, raw_message, meta):
  2723. message = str(raw_message or '').strip()
  2724. if tool_name == 'trial_goods_tax' and message in self.TAX_TRIAL_MESSAGES:
  2725. safe_code = code if code in self.ERROR_MESSAGES else 'MCP_9001'
  2726. retryable = safe_code not in self.NON_RETRYABLE_CODES
  2727. return self._error_result(safe_code, message, retryable, meta)
  2728. if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
  2729. logger.warning(
  2730. 'Unknown MCP business error code',
  2731. extra={
  2732. 'request_id': meta.get('request_id', ''),
  2733. 'tool_code': tool_name,
  2734. 'backend_code': code,
  2735. 'response_code': 'MCP_9001',
  2736. 'diagnostic_reason': 'UNEXPECTED_EXCEPTION',
  2737. },
  2738. )
  2739. code = 'MCP_9001'
  2740. if code == 'MCP_1401' and tool_name in self.ARRANGE_TRAILER_TOOLS:
  2741. if not self._arrange_trailer_message(message):
  2742. message = self._parameter_message('')
  2743. return self._error_result(code, message, False, meta)
  2744. elif code == 'MCP_1401' and tool_name in self.REMOTE_ADDRESS_SAVE_TOOLS:
  2745. if not self._remote_address_save_message(message):
  2746. message = self._parameter_message('')
  2747. return self._error_result(code, message, False, meta)
  2748. elif code == 'MCP_1401' and tool_name in self.FUEL_RATE_TOOLS:
  2749. if not self._fuel_rate_message(message):
  2750. message = self._parameter_message('')
  2751. return self._error_result(code, message, False, meta)
  2752. elif code == 'MCP_1401':
  2753. label = self._find_field_label(tool_name, raw_message)
  2754. message = self._parameter_message(label)
  2755. elif tool_name in self.INBOUND_RECEIPT_TOOLS | self.EXPORT_TOOLS and (
  2756. tool_name in (
  2757. 'list_inbound_receipt_templates',
  2758. 'export_inbound_receipt',
  2759. )
  2760. ):
  2761. allowed = self._inbound_receipt_message(message)
  2762. message = allowed or self.ERROR_MESSAGES.get(
  2763. code, self.ERROR_MESSAGES['MCP_9001']
  2764. )
  2765. elif tool_name in self.FREIGHTOWER_TRACK_TOOLS:
  2766. allowed = str(raw_message or '').strip()
  2767. if allowed not in (
  2768. '暂时没有飞驼轨迹',
  2769. '未找到本公司排舱单',
  2770. '匹配到多票排舱单,请同时提供提单号和柜号',
  2771. '未找到飞驼轨迹订单',
  2772. '飞驼查询超时',
  2773. '飞驼查询失败',
  2774. ):
  2775. message = self.ERROR_MESSAGES.get(code, self.ERROR_MESSAGES['MCP_9001'])
  2776. else:
  2777. message = allowed
  2778. elif tool_name in self.REMOTE_ADDRESS_MAINTAIN_TOOLS and message == '偏远地址库不存在':
  2779. message = '偏远地址库不存在'
  2780. elif tool_name in self.REMOTE_ADDRESS_SAVE_TOOLS and message == '偏远地址库不存在':
  2781. message = '偏远地址库不存在'
  2782. else:
  2783. message = self.ERROR_MESSAGES[code]
  2784. retryable = code not in self.NON_RETRYABLE_CODES
  2785. return self._error_result(code, message, retryable, meta)
  2786. @staticmethod
  2787. def _arrange_trailer_message(message):
  2788. if message in (
  2789. '请提供提单号或柜号',
  2790. '存在不能填写的内容',
  2791. '号码不正确',
  2792. '请填写拖车行',
  2793. '找不到可安排的柜子',
  2794. '匹配到不止一柜,请改说另一个号码',
  2795. '当前柜子不能安排拖车行',
  2796. '当前不需要填写海外仓',
  2797. '当前不需要填写直送改拆',
  2798. '当前不需要填写改拆原因',
  2799. '直送柜只能选择是或否',
  2800. '拖车行请说完整的名称-代码',
  2801. '拖车行不能填',
  2802. '港后跟进人不能填',
  2803. '预计提柜时间不正确',
  2804. '请选择直送改拆',
  2805. '直送改拆只能选择是或否',
  2806. '改拆原因不正确',
  2807. '请填写海外仓',
  2808. '海外仓不能填',
  2809. '请填写仓库联系人',
  2810. '请填写仓库联系电话',
  2811. '当前不需要填写重量体积',
  2812. '该柜子需要在页面上传BOL后再安排',
  2813. '重量体积不正确',
  2814. '订单不在这一柜',
  2815. '请填写重量体积',
  2816. '干线未出发不可安排拖车',
  2817. '当前柜子已安排拖车行',
  2818. '仓库不存在',
  2819. '仓库联系人不能为空',
  2820. '仓库联系电话不能为空',
  2821. '拖车行不存在',
  2822. '拖车行类型不是海外拖车',
  2823. '排舱单不存在',
  2824. '安排拖车行失败',
  2825. ):
  2826. return True
  2827. return message.startswith('请填写订单') and message.endswith('的重量和体积')
  2828. def _find_field_label(self, tool_name, raw_message):
  2829. message = str(raw_message or '')
  2830. fields = self.FIELD_LABELS.get(tool_name, {})
  2831. for field in sorted(fields, key=len, reverse=True):
  2832. if field in message:
  2833. return fields[field]
  2834. return ''
  2835. @staticmethod
  2836. def _parameter_message(label):
  2837. return '{0}参数不正确'.format(label) if label else '工具参数不正确,请检查后重试'
  2838. @classmethod
  2839. def _inbound_receipt_message(cls, message):
  2840. text = str(message or '').strip()
  2841. if text in (
  2842. '当前没有可用的入仓单模板',
  2843. '所选模板不属于这次柜号对应的入仓单',
  2844. ):
  2845. return text
  2846. missing_prefix = '这些柜号没有对应的排舱单:'
  2847. if text.startswith(missing_prefix) and cls._container_code_list(
  2848. text[len(missing_prefix):]
  2849. ):
  2850. return text
  2851. direct_prefix = '这些柜号是直送柜:'
  2852. indirect_mark = '。这些柜号是非直送柜:'
  2853. if text.startswith(direct_prefix) and text.endswith('。') and indirect_mark in text:
  2854. body = text[len(direct_prefix):-1]
  2855. direct, indirect = body.split(indirect_mark, 1)
  2856. if cls._container_code_list(direct) and cls._container_code_list(indirect):
  2857. return text
  2858. return ''
  2859. @staticmethod
  2860. def _container_code_list(text):
  2861. if not text or len(text) > 200 * 101:
  2862. return False
  2863. parts = text.split('、')
  2864. if not parts:
  2865. return False
  2866. for part in parts:
  2867. if (
  2868. not part
  2869. or part != part.strip()
  2870. or len(part) > 100
  2871. or any(ord(char) < 32 for char in part)
  2872. ):
  2873. return False
  2874. return True
  2875. @staticmethod
  2876. def _build_meta(raw_meta):
  2877. if not isinstance(raw_meta, dict):
  2878. return {}
  2879. request_id = raw_meta.get('request_id')
  2880. if not isinstance(request_id, str) or not request_id.strip():
  2881. return {}
  2882. return {'request_id': request_id.strip()}
  2883. @staticmethod
  2884. def _build_pagination(raw_meta):
  2885. if not isinstance(raw_meta, dict):
  2886. return {}
  2887. return {
  2888. key: raw_meta[key]
  2889. for key in ('page', 'limit', 'has_more', 'total')
  2890. if key in raw_meta
  2891. }
  2892. @staticmethod
  2893. def _safe_text(value):
  2894. if value is None:
  2895. return ''
  2896. return str(value).strip()
  2897. @classmethod
  2898. def _safe_tips(cls, value):
  2899. if not isinstance(value, list):
  2900. return []
  2901. return [cls._safe_text(item) for item in value if cls._safe_text(item)]
  2902. @classmethod
  2903. def _render_table(cls, content, require_complete=False):
  2904. headers = content.get('headers') or []
  2905. rows = content.get('rows') or []
  2906. lines = []
  2907. if require_complete:
  2908. lines.append('完整客户数据,禁止摘要、合并、隐藏字段或省略空字段。')
  2909. lines.append(
  2910. '本页记录数:{0};每条字段数:{1};应展示字段值总数:{2}。'.format(
  2911. len(rows), len(headers), len(rows) * len(headers)
  2912. )
  2913. )
  2914. summary = cls._safe_text(content.get('summary'))
  2915. if summary:
  2916. lines.append(summary)
  2917. lines.append('表头共 {0} 列:'.format(len(headers)))
  2918. for index, header in enumerate(headers, start=1):
  2919. lines.append('{0}. {1}'.format(index, header['label']))
  2920. for index, row in enumerate(rows, start=1):
  2921. lines.append('记录 {0}:'.format(index))
  2922. for header, value in zip(headers, row):
  2923. if isinstance(value, (dict, list)):
  2924. value = json.dumps(value, ensure_ascii=False)
  2925. lines.append('- {0}: {1}'.format(header['label'], value))
  2926. tips = content.get('tips') or []
  2927. if tips:
  2928. lines.append('提示:{0}'.format(';'.join(tips)))
  2929. if require_complete:
  2930. lines.append(
  2931. '展示完整性校验:已提供{0}条客户的全部{1}个字段。'.format(
  2932. len(rows), len(headers)
  2933. )
  2934. )
  2935. return '\n'.join(lines)
  2936. def _present_freightower_track(self, data, meta):
  2937. expected = {'schedule', 'ports', 'containers', 'tracks', 'terminal_plan'}
  2938. if set(data) != expected:
  2939. return self._format_error(meta)
  2940. schedule = self._freightower_object(data.get('schedule'), (
  2941. ('bl_number', '提单号'),
  2942. ('container_code', '柜号'),
  2943. ('carrier_name', '船公司'),
  2944. ('vessel', '船名'),
  2945. ('voyage', '航次'),
  2946. ('route_code', '航线'),
  2947. ('current_status', '当前状态'),
  2948. ('current_status_time', '当前状态时间'),
  2949. ('current_status_place', '当前状态地点'),
  2950. ('update_time', '数据更新时间'),
  2951. ('etd', '预计离港'),
  2952. ('atd', '实际离港'),
  2953. ('eta', '预计到达'),
  2954. ))
  2955. if schedule is None:
  2956. return self._format_error(meta)
  2957. ports = self._freightower_rows(data.get('ports'), (
  2958. ('place_type', '地点类型'),
  2959. ('port_name', '港口'),
  2960. ('terminal_name', '码头'),
  2961. ('etd', '预计离港'),
  2962. ('atd', '实际离港'),
  2963. ('eta', '预计到达'),
  2964. ('ata', '实际到达'),
  2965. ), {'place_type': ('起运港', '中转港', '目的港', '交货地')})
  2966. containers = self._freightower_rows(data.get('containers'), (
  2967. ('container_no', '箱号'),
  2968. ('container_type', '箱型'),
  2969. ('seal_no', '铅封号'),
  2970. ('current_status', '当前状态'),
  2971. ('off_load', '是否甩柜'),
  2972. ), {'off_load': ('', '是', '否')})
  2973. tracks = self._freightower_rows(data.get('tracks'), (
  2974. ('time', '时间'),
  2975. ('location', '地点'),
  2976. ('content', '内容'),
  2977. ('event_code', '状态代码'),
  2978. ('estimate_flag', '实际/预计'),
  2979. ('vessel_voyage', '船名航次'),
  2980. ('terminal_name', '码头'),
  2981. ), {'estimate_flag': ('实际', '预计')})
  2982. terminal_plan = self._freightower_object(data.get('terminal_plan'), (
  2983. ('port_name', '港口'),
  2984. ('terminal_name', '码头'),
  2985. ('open_time', '开港'),
  2986. ('close_time', '截港'),
  2987. ('etd', '预计离港'),
  2988. ('atd', '实际离港'),
  2989. ('shipping_agent', '船代'),
  2990. ))
  2991. if None in (ports, containers, tracks, terminal_plan):
  2992. return self._format_error(meta)
  2993. content = {
  2994. '船期': schedule,
  2995. '港口': ports,
  2996. '箱子': containers,
  2997. '轨迹': tracks,
  2998. '港区计划': terminal_plan,
  2999. }
  3000. lines = ['飞驼船期']
  3001. for label, value in schedule.items():
  3002. lines.append('- {0}: {1}'.format(label, value))
  3003. lines.extend(self._freightower_section('港口', ports, (
  3004. '地点类型', '港口', '码头', '预计离港', '实际离港', '预计到达', '实际到达',
  3005. )))
  3006. lines.extend(self._freightower_section('箱子', containers, (
  3007. '箱号', '箱型', '铅封号', '当前状态', '是否甩柜',
  3008. )))
  3009. lines.extend(self._freightower_section('轨迹', tracks, (
  3010. '时间', '地点', '内容', '状态代码', '实际/预计', '船名航次', '码头',
  3011. )))
  3012. if any(terminal_plan.values()):
  3013. lines.append('港区计划')
  3014. for label, value in terminal_plan.items():
  3015. lines.append('- {0}: {1}'.format(label, value))
  3016. else:
  3017. lines.append('港区计划')
  3018. lines.append('- 无')
  3019. return self._success_result(content, '\n'.join(lines), meta)
  3020. def _freightower_object(self, value, fields):
  3021. if not isinstance(value, dict) or set(value) != {key for key, _label in fields}:
  3022. return None
  3023. result = {}
  3024. for key, label in fields:
  3025. item = value.get(key)
  3026. if not isinstance(item, str):
  3027. return None
  3028. result[label] = item
  3029. return result
  3030. def _freightower_rows(self, rows, fields, choices):
  3031. if not isinstance(rows, list):
  3032. return None
  3033. keys = {key for key, _label in fields}
  3034. result = []
  3035. for row in rows:
  3036. if not isinstance(row, dict) or set(row) != keys:
  3037. return None
  3038. item = {}
  3039. for key, label in fields:
  3040. cell = row.get(key)
  3041. if not isinstance(cell, str):
  3042. return None
  3043. allowed = choices.get(key)
  3044. if allowed is not None and cell not in allowed:
  3045. return None
  3046. item[label] = cell
  3047. result.append(item)
  3048. return result
  3049. @staticmethod
  3050. def _freightower_section(title, rows, labels):
  3051. lines = [title]
  3052. if not rows:
  3053. lines.append('- 无')
  3054. return lines
  3055. for row in rows:
  3056. lines.append('- ' + ' '.join(row[label] for label in labels))
  3057. return lines
  3058. @staticmethod
  3059. def _success_result(content, text, meta):
  3060. return {
  3061. 'structured_content': content,
  3062. 'text': text,
  3063. 'is_error': False,
  3064. 'meta': meta,
  3065. }
  3066. @staticmethod
  3067. def _error_result(code, message, retryable, meta=None):
  3068. return {
  3069. 'structured_content': {
  3070. 'code': code,
  3071. 'message': message,
  3072. 'retryable': retryable,
  3073. },
  3074. 'text': '{0}: {1}'.format(code, message),
  3075. 'is_error': True,
  3076. 'meta': meta or {},
  3077. }
  3078. @classmethod
  3079. def _format_error(cls, meta=None):
  3080. return cls._error_result(
  3081. 'MCP_9001',
  3082. '工具返回格式异常',
  3083. True,
  3084. meta,
  3085. )