query_receivable_cost_list.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import re
  2. from datetime import date
  3. class QueryReceivableCostListTool:
  4. name = 'query_receivable_cost_list'
  5. route_path = '/mcp/tools/queryReceivableCostList'
  6. NUMBER_FIELDS = (
  7. 'reference_numbers',
  8. 'tracking_numbers',
  9. 'order_numbers',
  10. 'bill_numbers',
  11. )
  12. def __init__(self, api_client=None):
  13. self.api_client = api_client
  14. def metadata(self):
  15. number_array = {
  16. 'type': 'array',
  17. 'minItems': 1,
  18. 'maxItems': 200,
  19. 'items': {
  20. 'type': 'string',
  21. 'minLength': 1,
  22. 'maxLength': 100,
  23. 'pattern': '.*\\S.*',
  24. },
  25. }
  26. date_field = {
  27. 'type': 'string',
  28. 'format': 'date',
  29. 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
  30. }
  31. return {
  32. 'name': self.name,
  33. 'description': (
  34. '分页查询当前员工有权查看的应收费用列表。仅可按已明确业务类型的参考号、'
  35. '跟踪号、订单号或账单编号精确查询;禁止根据格式猜测号码类型,禁止跨字段'
  36. '试查、并行试查或失败后改用其他号码字段。没有明确号码时必须提供不超过31个'
  37. '日历日的业务日期闭区间。客户、出账状态、核销状态、单据类型和费用项筛选值'
  38. '必须先调用list_receivable_cost_filter_options选择并使用其返回的value,'
  39. '不得猜测内部ID。'
  40. ),
  41. 'input_schema': {
  42. 'type': 'object',
  43. 'properties': {
  44. 'reference_numbers': dict(number_array),
  45. 'tracking_numbers': dict(number_array),
  46. 'order_numbers': dict(number_array),
  47. 'bill_numbers': dict(number_array),
  48. 'business_date_start': dict(date_field),
  49. 'business_date_end': dict(date_field),
  50. 'customer_id': {'type': 'integer', 'minimum': 1},
  51. 'sub_customer_id': {'type': 'integer', 'minimum': 1},
  52. 'billing_status': {'type': 'integer', 'enum': [0, 1]},
  53. 'verification_status': {
  54. 'type': 'integer', 'enum': [-1, 0, 1],
  55. },
  56. 'document_type': {'type': 'integer', 'minimum': 0},
  57. 'cost_type_id': {'type': 'integer', 'minimum': 1},
  58. 'page': {
  59. 'type': 'integer', 'minimum': 1, 'maximum': 100,
  60. 'default': 1,
  61. },
  62. 'limit': {
  63. 'type': 'integer', 'minimum': 1, 'maximum': 100,
  64. 'default': 20,
  65. },
  66. },
  67. 'required': [],
  68. 'allOf': [
  69. {
  70. 'anyOf': [
  71. {'required': ['reference_numbers']},
  72. {'required': ['tracking_numbers']},
  73. {'required': ['order_numbers']},
  74. {'required': ['bill_numbers']},
  75. {
  76. 'required': [
  77. 'business_date_start',
  78. 'business_date_end',
  79. ],
  80. },
  81. ],
  82. },
  83. {
  84. 'if': {'required': ['sub_customer_id']},
  85. 'then': {'required': ['customer_id']},
  86. },
  87. ],
  88. 'additionalProperties': False,
  89. },
  90. }
  91. def call(
  92. self,
  93. reference_numbers=None,
  94. tracking_numbers=None,
  95. order_numbers=None,
  96. bill_numbers=None,
  97. business_date_start=None,
  98. business_date_end=None,
  99. customer_id=None,
  100. sub_customer_id=None,
  101. billing_status=None,
  102. verification_status=None,
  103. document_type=None,
  104. cost_type_id=None,
  105. page=1,
  106. limit=20,
  107. request_id='rq_query_receivable_cost_list',
  108. ):
  109. if self.api_client is None:
  110. raise RuntimeError(
  111. 'api client is required for query_receivable_cost_list'
  112. )
  113. supplied_numbers = {}
  114. total_numbers = 0
  115. for field, values in (
  116. ('reference_numbers', reference_numbers),
  117. ('tracking_numbers', tracking_numbers),
  118. ('order_numbers', order_numbers),
  119. ('bill_numbers', bill_numbers),
  120. ):
  121. if values is not None:
  122. normalized = self._number_list(values, field)
  123. supplied_numbers[field] = normalized
  124. total_numbers += len(normalized)
  125. if total_numbers > 200:
  126. raise ValueError('combined exact number count is invalid')
  127. start = self._optional_date(business_date_start, 'business_date_start')
  128. end = self._optional_date(business_date_end, 'business_date_end')
  129. if bool(start) != bool(end):
  130. raise ValueError('business date range is invalid')
  131. if start and end:
  132. span = (date.fromisoformat(end) - date.fromisoformat(start)).days
  133. if span < 0 or span > 30:
  134. raise ValueError('business date range is invalid')
  135. if not supplied_numbers and not (start and end):
  136. raise ValueError('exact numbers or business date range is required')
  137. if sub_customer_id is not None and customer_id is None:
  138. raise ValueError('customer_id is required with sub_customer_id')
  139. payload = dict(supplied_numbers)
  140. if start:
  141. payload['business_date_start'] = start
  142. payload['business_date_end'] = end
  143. for field, value, minimum in (
  144. ('customer_id', customer_id, 1),
  145. ('sub_customer_id', sub_customer_id, 1),
  146. ('document_type', document_type, 0),
  147. ('cost_type_id', cost_type_id, 1),
  148. ):
  149. if value is not None:
  150. payload[field] = self._integer(value, field, minimum)
  151. for field, value, allowed in (
  152. ('billing_status', billing_status, (0, 1)),
  153. ('verification_status', verification_status, (-1, 0, 1)),
  154. ):
  155. if value is not None:
  156. value = self._integer(value, field)
  157. if value not in allowed:
  158. raise ValueError('{0} is invalid'.format(field))
  159. payload[field] = value
  160. payload['page'] = self._bounded_integer(page, 'page')
  161. payload['limit'] = self._bounded_integer(limit, 'limit')
  162. return self.api_client.call_tool(
  163. self.name, self.route_path, payload, request_id
  164. )
  165. @staticmethod
  166. def _number_list(values, field):
  167. if not isinstance(values, list) or not 1 <= len(values) <= 200:
  168. raise ValueError('{0} is invalid'.format(field))
  169. result = []
  170. for value in values:
  171. if not isinstance(value, str):
  172. raise ValueError('{0} is invalid'.format(field))
  173. value = value.strip()
  174. if not value or len(value) > 100:
  175. raise ValueError('{0} is invalid'.format(field))
  176. result.append(value)
  177. return result
  178. @staticmethod
  179. def _optional_date(value, field):
  180. if value is None or value == '':
  181. return ''
  182. if not isinstance(value, str):
  183. raise ValueError('{0} is invalid'.format(field))
  184. if re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}', value) is None:
  185. raise ValueError('{0} is invalid'.format(field))
  186. try:
  187. date.fromisoformat(value)
  188. except ValueError:
  189. raise ValueError('{0} is invalid'.format(field))
  190. return value
  191. @staticmethod
  192. def _integer(value, field, minimum=None):
  193. if isinstance(value, bool) or not isinstance(value, int):
  194. raise ValueError('{0} is invalid'.format(field))
  195. if minimum is not None and value < minimum:
  196. raise ValueError('{0} is invalid'.format(field))
  197. return value
  198. @classmethod
  199. def _bounded_integer(cls, value, field):
  200. value = cls._integer(value, field, 1)
  201. if value > 100:
  202. raise ValueError('{0} is invalid'.format(field))
  203. return value