中国本土应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

560 lines
20KB

  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import json
  4. from collections import defaultdict
  5. import time
  6. from odoo import fields, models, api, Command, tools, _
  7. from odoo.exceptions import UserError
  8. import xmltodict
  9. from odoo.addons.oec_im_wecom_api.api.wecom_abstract_api import ApiException
  10. from odoo.addons.base.models.ir_mail_server import MailDeliveryException
  11. _logger = logging.getLogger(__name__)
  12. class WecomUser(models.Model):
  13. _name = "wecom.user"
  14. _description = "Wecom user"
  15. _order = "order_in_department"
  16. # 企微字段
  17. userid = fields.Char(
  18. string="User ID",
  19. readonly=True,
  20. default="",
  21. ) # 成员UserID。对应管理端的帐号
  22. name = fields.Char(string="Name", readonly=True, default="") # 成员名称
  23. english_name = fields.Char(
  24. string="English name", readonly=True, default=""
  25. ) # 英部门文名称
  26. mobile = fields.Char(string="mobile phone", readonly=True,default="") # 手机号码
  27. department = fields.Char(
  28. string="Multiple Department ID", readonly=True, store=True, default="[]"
  29. ) # 成员所属部门id列表
  30. main_department = fields.Char(
  31. string="Main department", readonly=True, default=""
  32. ) # 主部门
  33. order = fields.Char(
  34. string="Sequence", readonly=True, default="[]"
  35. ) # 部门内的排序值,默认为0。数量必须和department一致,数值越大排序越前面。值范围是[0, 2^32)
  36. position = fields.Char(string="Position", readonly=True, default="") # 职务信息;
  37. gender = fields.Char(
  38. string="Gender", readonly=True, default=""
  39. ) # 性别。0表示未定义,1表示男性,2表示女性。
  40. email = fields.Char(string="Email", readonly=True, default="") # 邮箱
  41. biz_mail = fields.Char(string="BizMail", readonly=True, default="") # 企业邮箱
  42. is_leader_in_dept = fields.Char(
  43. string="Is Department Leader", readonly=True, default="[]"
  44. ) # 表示在所在的部门内是否为部门负责人。0-否;1-是。是一个列表,数量必须与department一致。
  45. direct_leader = fields.Char(
  46. string="Direct Leader", readonly=True, default="[]"
  47. ) # 直属上级UserID,返回在应用可见范围内的直属上级列表,最多有五个直属上级
  48. avatar = fields.Char(string="Avatar", readonly=True, default="") # 头像url
  49. thumb_avatar = fields.Char(
  50. string="Avatar thumbnail", readonly=True, default=""
  51. ) # 头像缩略图url
  52. telephone = fields.Char(string="Telephone", readonly=True, default="") # 座机号码
  53. alias = fields.Char(string="Alias", readonly=True, default="") # 别名
  54. extattr = fields.Text(
  55. string="Extended attributes", readonly=True, default=""
  56. ) # 扩展属性
  57. external_profile = fields.Text(
  58. string="External attributes", readonly=True, default=""
  59. ) # 成员对外属性
  60. external_position = fields.Char(
  61. string="External position", readonly=True, default=""
  62. ) # 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以position来展示。
  63. status = fields.Integer(
  64. string="Status", readonly=True, default=""
  65. ) # 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。已激活代表已激活企业微信或已关注微信插件(原企业号)。未激活代表既未激活企业微信又未关注微信插件(原企业号)。
  66. qr_code = fields.Char(
  67. string="Personal QR code", readonly=True, default=""
  68. ) # 员工个人二维码,扫描可添加为外部联系人
  69. address = fields.Char(string="Address", readonly=True, default="") # 地址
  70. open_userid = fields.Char(
  71. string="Open userid", readonly=True, default=""
  72. ) # 开放用户Id,全局唯一,对于同一个服务商,不同应用获取到企业内同一个成员的open_userid是相同的,最多64个字节。仅第三方应用可获取
  73. # odoo 字段
  74. company_id = fields.Many2one(
  75. "res.company",
  76. required=True,
  77. domain="[('is_wecom_organization', '=', True)]",
  78. copy=False,
  79. store=True,
  80. readonly=True,
  81. )
  82. department_id = fields.Many2one(
  83. "wecom.department",
  84. "Department",
  85. domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
  86. compute="_compute_department_id",
  87. store=True,
  88. )
  89. department_ids = fields.Many2many(
  90. "wecom.department",
  91. "wecom_user_department_rel",
  92. "user_id",
  93. "department_id",
  94. string="Multiple Departments",
  95. readonly=True,
  96. compute="_compute_department_ids",
  97. )
  98. tag_ids = fields.Many2many(
  99. "wecom.tag",
  100. "wecom_user_tag_rel",
  101. "wecom_user_id",
  102. "wecom_tag_id",
  103. string="Tags",
  104. )
  105. department_complete_name = fields.Char(
  106. string="Department complete Name", related="department_id.complete_name"
  107. )
  108. order_in_department = fields.Integer(
  109. string="Sequence in department",
  110. readonly=True,
  111. default="0",
  112. ) # 成员在对应部门中的排序值,默认为0。数量必须和department一致
  113. status_name = fields.Selection(
  114. [
  115. ("1", _("Activated")),
  116. ("2", _("Disabled")),
  117. ("4", _("Not active")),
  118. ("5", _("Exit the enterprise")),
  119. ],
  120. string="Status",
  121. readonly=True,
  122. # compute="_compute_status_name",
  123. ) # 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。已激活代表已激活企业微信或已关注微信插件(原企业号)。未激活代表既未激活企业微信又未关注微信插件(原企业号)。
  124. gender_name = fields.Selection(
  125. [("1", _("Male")), ("2", _("Female")), ("0", _("Undefined"))],
  126. string="Gender",
  127. # compute="_compute_gender_name",
  128. )
  129. color = fields.Integer("Color Index")
  130. active = fields.Boolean(
  131. "Active",
  132. default=True,
  133. store=True,
  134. readonly=True,
  135. # compute="_compute_active",
  136. )
  137. @api.depends("status")
  138. def _compute_status_name(self):
  139. for user in self:
  140. user.status_name = str(user.status)
  141. @api.depends("status")
  142. def _compute_active(self):
  143. for user in self:
  144. if user.status == 1:
  145. user.active = True
  146. else:
  147. user.active = False
  148. @api.depends("main_department")
  149. def _compute_department_id(self):
  150. for user in self:
  151. department_id = self.env["wecom.department"].search(
  152. [
  153. ("department_id", "=", user.main_department),
  154. ("company_id", "=", user.company_id.id),
  155. ],
  156. limit=1,
  157. )
  158. if department_id:
  159. user.department_id = department_id
  160. @api.depends("gender")
  161. def _compute_gender_name(self):
  162. for user in self:
  163. user.gender_name = str(user.gender)
  164. @api.depends("department")
  165. def _compute_department_ids(self):
  166. """
  167. 计算多部门 eval( )
  168. """
  169. for user in self:
  170. department_list = eval(user.department)
  171. department_ids = self.get_parent_department(
  172. user.company_id, department_list
  173. )
  174. user.write({"department_ids": [(6, 0, department_ids)]})
  175. def get_parent_department(self, company, departments):
  176. """
  177. 获取上级部门
  178. """
  179. department_ids = []
  180. for department in departments:
  181. department_id = self.env["wecom.department"].search(
  182. [
  183. ("department_id", "=", department),
  184. ("company_id", "=", company.id),
  185. ],
  186. limit=1,
  187. )
  188. if department_id:
  189. department_ids.append(department_id.id)
  190. return department_ids
  191. # ------------------------------------------------------------
  192. # 企微用户下载
  193. # ------------------------------------------------------------
  194. @api.model
  195. def download_wecom_users(self):
  196. """
  197. 下载用户列表
  198. """
  199. start_time = time.time()
  200. company = self.env.context.get("company_id")
  201. if type(company) == int:
  202. company = self.env["res.company"].browse(company)
  203. tasks = []
  204. try:
  205. wxapi = self.env["wecom.service_api"].InitServiceApi(
  206. company.corpid, company.contacts_app_id.secret
  207. )
  208. # 2020-8-27 按照官方API要求,进行重构
  209. # json对象只有 userid 和 department 两个字段
  210. # 参数:"cursor", 必须:否, 说明:用于分页查询的游标,字符串类型,由上一次调用返回,首次调用不填
  211. # 参数:"limit", 必须:否, 说明:分页,预期请求的数据量,取值范围 1 ~ 10000
  212. response = wxapi.httpCall(
  213. self.env["wecom.service_api_list"].get_server_api_call("USER_LIST_ID"),
  214. {}
  215. # {"department_id": "1", "fetch_child": "1",},
  216. )
  217. except ApiException as ex:
  218. end_time = time.time()
  219. self.env["wecomapi.tools.action"].ApiExceptionDialog(
  220. str(ex), raise_exception=False
  221. )
  222. tasks = [
  223. {
  224. "name": "download_user_data",
  225. "state": False,
  226. "time": end_time - start_time,
  227. "msg": str(ex),
  228. }
  229. ]
  230. except Exception as e:
  231. end_time = time.time()
  232. tasks = [
  233. {
  234. "name": "download_user_data",
  235. "state": False,
  236. "time": end_time - start_time,
  237. "msg": str(e),
  238. }
  239. ]
  240. else:
  241. # response["userlist"] 只含 'department', 'userid' 2个字段
  242. if response["errcode"] == 0:
  243. # 1. 合并多部门
  244. dept_user = response["dept_user"]
  245. d = defaultdict(lambda : defaultdict(list))
  246. for dic in dept_user:
  247. userid, department = dic["userid"], dic["department"]
  248. d[userid]["userid"] = userid
  249. d[userid]["department"].extend(str(department))
  250. userlist = []
  251. for key, value in d.items():
  252. dept_user = {}
  253. department = []
  254. userid = value["userid"]
  255. departments = value["department"]
  256. for dep in departments:
  257. department.append(int(dep))
  258. userlist.append({"userid": userid, "department": department})
  259. # 2.下载用户
  260. if userlist:
  261. for wecom_user in userlist:
  262. download_user_result = self.download_user(company, wecom_user)
  263. if download_user_result:
  264. for r in download_user_result:
  265. tasks.append(r) # 加入 下载员工失败结果
  266. # 3.完成下载
  267. end_time = time.time()
  268. task = {
  269. "name": "download_user_data",
  270. "state": True,
  271. "time": end_time - start_time,
  272. "msg": _("User list sync completed."),
  273. }
  274. tasks.append(task)
  275. finally:
  276. return tasks # 返回结果
  277. def download_user(self, company, wecom_user):
  278. """
  279. 下载用户
  280. """
  281. user = self.sudo().search(
  282. [
  283. ("userid", "=", wecom_user["userid"]),
  284. ("company_id", "=", company.id),
  285. ],
  286. limit=1,
  287. )
  288. result = {}
  289. if not user:
  290. result = self.create_user(company, user, wecom_user)
  291. else:
  292. result = self.update_user(company, user, wecom_user)
  293. return result
  294. def create_user(self, company, user, wecom_user):
  295. """
  296. 创建用户
  297. """
  298. try:
  299. user.create(
  300. {
  301. "userid": wecom_user["userid"],
  302. "department": wecom_user["department"],
  303. "company_id": company.id,
  304. }
  305. )
  306. except Exception as e:
  307. result = _(
  308. "Error creating company [%s]'s user [%s], error reason: %s"
  309. ) % (
  310. company.userid,
  311. wecom_user["userid"].lower(),
  312. repr(e),
  313. )
  314. _logger.warning(result)
  315. return {
  316. "name": "add_user",
  317. "state": False,
  318. "time": 0,
  319. "msg": result,
  320. }
  321. def update_user(self, company, user, wecom_user):
  322. """
  323. 更新用户
  324. """
  325. try:
  326. user.sudo().write(
  327. {
  328. "department": wecom_user["department"],
  329. }
  330. )
  331. except Exception as e:
  332. result = _("Error update company [%s]'s user [%s], error reason: %s") % (
  333. company.name,
  334. wecom_user["userid"].lower(),
  335. repr(e),
  336. )
  337. _logger.warning(result)
  338. return {
  339. "name": "update_user",
  340. "state": False,
  341. "time": 0,
  342. "msg": result,
  343. } # 返回失败结果
  344. def download_single_user(self):
  345. """
  346. 下载单个用户
  347. """
  348. company = self.company_id
  349. params = {}
  350. message = ""
  351. try:
  352. wxapi = self.env["wecom.service_api"].InitServiceApi(
  353. company.corpid, company.contacts_app_id.secret
  354. )
  355. response = wxapi.httpCall(
  356. self.env["wecom.service_api_list"].get_server_api_call("USER_GET"),
  357. {"userid": self.userid},
  358. )
  359. for key in response.keys():
  360. if type(response[key]) in (list, dict) and response[key]:
  361. json_str = json.dumps(
  362. response[key],
  363. sort_keys=False,
  364. indent=2,
  365. separators=(",", ":"),
  366. ensure_ascii=False,
  367. )
  368. response[key] = json_str
  369. self.write(
  370. {
  371. "name": response["name"],
  372. "english_name": self.env["wecomapi.tools.dictionary"].check_dictionary_keywords(
  373. response, "english_name"
  374. ),
  375. "mobile": response["mobile"],
  376. "department": response["department"],
  377. "main_department": response["main_department"],
  378. "order": response["order"],
  379. "position": response["position"],
  380. "gender": response["gender"],
  381. "email": response["email"],
  382. "biz_mail": response["biz_mail"],
  383. "is_leader_in_dept": response["is_leader_in_dept"],
  384. "direct_leader": response["direct_leader"],
  385. "avatar": response["avatar"],
  386. "thumb_avatar": response["thumb_avatar"],
  387. "telephone": response["telephone"],
  388. "alias": response["alias"],
  389. "extattr": response["extattr"],
  390. "external_profile": self.env[
  391. "wecomapi.tools.dictionary"
  392. ].check_dictionary_keywords(response, "external_profile"),
  393. "external_position": self.env[
  394. "wecomapi.tools.dictionary"
  395. ].check_dictionary_keywords(response, "external_position"),
  396. "status": response["status"],
  397. "qr_code": response["qr_code"],
  398. "address": self.env["wecomapi.tools.dictionary"].check_dictionary_keywords(
  399. response, "address"
  400. ),
  401. "open_userid": self.env["wecomapi.tools.dictionary"].check_dictionary_keywords(
  402. response, "open_userid"
  403. ),
  404. }
  405. )
  406. except ApiException as ex:
  407. message = _("User [id:%s, name:%s] failed to download,Reason: %s") % (
  408. self.userid,
  409. self.name,
  410. str(ex),
  411. )
  412. _logger.warning(message)
  413. params = {
  414. "title": _("Download failed!"),
  415. "message": message,
  416. "sticky": True, # 延时关闭
  417. "className": "bg-danger",
  418. "type": "danger",
  419. }
  420. else:
  421. message = _("User [id:%s, name:%s] downloaded successfully") % (
  422. self.userid,
  423. self.name,
  424. )
  425. params = {
  426. "title": _("Download Success!"),
  427. "message": message,
  428. "sticky": False, # 延时关闭
  429. "className": "bg-success",
  430. "type": "success",
  431. "next": {
  432. "type": "ir.actions.client",
  433. "tag": "reload",
  434. }, # 刷新窗体
  435. }
  436. finally:
  437. action = {
  438. "type": "ir.actions.client",
  439. "tag": "display_notification",
  440. "params": params,
  441. }
  442. return action
  443. def get_open_userid(self):
  444. """
  445. 获取企微 open_userid
  446. """
  447. for user in self:
  448. try:
  449. wxapi = self.env["wecom.service_api"].InitServiceApi(
  450. user.company_id.corpid,
  451. user.company_id.contacts_app_id.secret,
  452. )
  453. response = wxapi.httpCall(
  454. self.env["wecom.service_api_list"].get_server_api_call(
  455. "USERID_TO_OPENID"
  456. ),
  457. {
  458. "userid": user.userid,
  459. },
  460. )
  461. except ApiException as ex:
  462. self.env["wecomapi.tools.action"].ApiExceptionDialog(
  463. ex, raise_exception=True
  464. )
  465. else:
  466. user.open_userid = response["openid"]
  467. # ------------------------------------------------------------
  468. # 企微通讯录事件
  469. # ------------------------------------------------------------
  470. def wecom_event_change_contact_user(self, cmd):
  471. """
  472. 通讯录事件变更成员
  473. """
  474. xml_tree = self.env.context.get("xml_tree")
  475. company_id = self.env.context.get("company_id")
  476. user_dict = xmltodict.parse(xml_tree)["xml"]
  477. # print("wecom_event_change_contact_user", user_dict)
  478. domain = [
  479. "|",
  480. ("active", "=", True),
  481. ("active", "=", False),
  482. ]
  483. users = self.sudo().search([("company_id", "=", company_id.id)] + domain)
  484. callback_user = users.search(
  485. [("userid", "=", user_dict["UserID"])],
  486. limit=1,
  487. )
  488. if callback_user:
  489. # 如果存在,则更新
  490. # 用于退出企业微信又重新加入企业微信的员工
  491. cmd = "update"
  492. else:
  493. # 如果不存在,停止
  494. return
  495. update_dict = {}
  496. for key, value in user_dict.items():
  497. if key.lower() in self._fields.keys():
  498. update_dict.update({key.lower(): value})
  499. else:
  500. if key == "MainDepartment":
  501. update_dict.update({"main_department": value})
  502. elif key == "IsLeaderInDept":
  503. update_dict.update({"is_leader_in_dept": value})
  504. elif key == "DirectLeader":
  505. update_dict.update({"direct_leader": value})
  506. elif key == "BizMail":
  507. update_dict.update({"biz_mail": value})
  508. if cmd == "create":
  509. update_dict.update({"company_id": company_id.id})
  510. callback_user.create(update_dict)
  511. elif cmd == "update":
  512. if "userid" in update_dict:
  513. del update_dict["userid"]
  514. callback_user.write(update_dict)
  515. elif cmd == "delete":
  516. callback_user.write(
  517. {
  518. "active": False,
  519. }
  520. )
上海开阖软件有限公司 沪ICP备12045867号-1