query_receivable_cost_list.py 8.3 KB

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