中国本土应用
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.

662 lines
26KB

  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import time
  4. from odoo import fields, models, api, Command, tools, _
  5. from odoo.exceptions import UserError
  6. from lxml import etree
  7. # from lxml_to_dict import lxml_to_dict
  8. # from xmltodict import lxml_to_dict
  9. import xmltodict
  10. from odoo.addons.oec_im_wecom_api.api.wecom_abstract_api import ApiException
  11. from odoo.addons.base.models.ir_mail_server import MailDeliveryException
  12. _logger = logging.getLogger(__name__)
  13. WECOM_USER_MAPPING_ODOO_USER = {
  14. "UserID": "wecom_userid", # 成员UserID
  15. "Name": "name", # 成员名称;
  16. "Department": "", # 成员部门列表,仅返回该应用有查看权限的部门id
  17. "MainDepartment": "", # 主部门
  18. "IsLeaderInDept": "", # 表示所在部门是否为上级,0-否,1-是,顺序与Department字段的部门逐一对应
  19. "DirectLeader": "", # 直属上级
  20. "Mobile": "mobile_phone", # 手机号码
  21. "Position": "", # 职位信息
  22. "Gender": "", # 企微性别:0表示未定义,1表示男性,2表示女性;odoo性别:male为男性,female为女性,other为其他
  23. "Email": "work_email", # 邮箱;
  24. "Status": "active", # 激活状态:1=已激活 2=已禁用 4=未激活 已激活代表已激活企业微信或已关注微工作台(原企业号)5=成员退出
  25. "Avatar": "", # 头像url。注:如果要获取小图将url最后的”/0”改成”/100”即可。
  26. "Alias": "", # 成员别名
  27. "Telephone": "work_phone", # 座机;
  28. "Address": "", # 地址;
  29. "ExtAttr": {
  30. "Type": "", # 扩展属性类型: 0-本文 1-网页
  31. "Text": "", # 文本属性类型,扩展属性类型为0时填写
  32. "Value": "", # 文本属性内容
  33. "Web": "", # 网页类型属性,扩展属性类型为1时填写
  34. "Title": "", # 网页的展示标题
  35. "Url": "", # 网页的url
  36. }, # 扩展属性;
  37. }
  38. class User(models.Model):
  39. _inherit = ["res.users"]
  40. # employee_id = fields.Many2one(
  41. # "hr.employee",
  42. # string="Company employee",
  43. # compute="_compute_company_employee",
  44. # search="_search_company_employee",
  45. # store=True,
  46. # ) # 变更用户类型时,需要绑定用户,避免出现“创建员工”的按钮,故 store=True
  47. # ----------------------------------------------------------------------------------
  48. # 开发人员注意:hr模块中
  49. # hr.employee.work_email = res.users.email
  50. # hr.employee.private_email = res.partner.email
  51. # ----------------------------------------------------------------------------------
  52. # base 模块中
  53. # res.user.email = res.partner.email
  54. # res.user.private_email = res.partner.email
  55. # ------------------------------------------
  56. # hr.employee.create() 方法中 创建hr.employee.work_email时会将 res.users.email更新到hr.employee.work_email
  57. # res.users.write() 方法中 更新res.users.email时会将 res.users.email更新到hr.employee.work_email
  58. # ------------------------------------------
  59. # 故重写了 将 private_email = address_home_id.email 修改为 private_email=employee_id.private_email
  60. # 故重写了 SELF_WRITEABLE_FIELDS
  61. # ----------------------------------------------------------------------------------
  62. # private_email = fields.Char(related='employee_id.private_email', string="Private Email")
  63. def _get_or_create_user_by_wecom_userid(self, object, send_mail, send_message):
  64. """
  65. 通过企微用户id获取odoo用户
  66. """
  67. login = tools.ustr(object.wecom_userid)
  68. self.env.cr.execute(
  69. "SELECT id, active FROM res_users WHERE lower(login)=%s", (login,)
  70. )
  71. res = self.env.cr.fetchone()
  72. if res:
  73. if res[1]:
  74. return res[0]
  75. else:
  76. group_portal_id = self.env["ir.model.data"]._xmlid_to_res_id(
  77. "base.group_portal"
  78. ) # 门户用户组
  79. SudoUser = self.sudo()
  80. values = {
  81. "name": object.name,
  82. "login": login,
  83. "notification_type": "inbox",
  84. "groups_id": [(6, 0, [group_portal_id])],
  85. "share": False,
  86. "active": object.active,
  87. "image_1920": object.image_1920,
  88. "password": self.env["wecomapi.tools.security"].random_passwd(8),
  89. "company_ids": [(6, 0, [object.company_id.id])],
  90. "company_id": object.company_id.id,
  91. "employee_ids": [(6, 0, [object.id])],
  92. "employee_id": object.id,
  93. "lang": self.env.lang,
  94. "company_id": object.company_id.id,
  95. # 以下为企业微信字段
  96. "wecom_userid": object.wecom_userid.lower(),
  97. "wecom_openid": object.wecom_openid,
  98. "is_wecom_user": object.is_wecom_user,
  99. "qr_code": object.qr_code,
  100. "wecom_user_order": object.wecom_user_order,
  101. }
  102. # 判断是否已存在 partner
  103. partner = (
  104. self.env["res.partner"]
  105. .sudo()
  106. .search(
  107. [
  108. ("wecom_userid", "=", object.wecom_userid),
  109. ("company_id", "=", object.company_id.id),
  110. ("is_wecom_user", "=", True),
  111. "|",
  112. ("active", "=", True),
  113. ("active", "=", False),
  114. ],
  115. limit=1,
  116. )
  117. )
  118. if not partner:
  119. pass
  120. else:
  121. values.update({"partner_id": partner.id})
  122. """
  123. MailThread功能可以通过上下文键进行一定程度的控制:
  124. -'mail_create_nosubscribe':在create或message_post上,不要订阅记录线程的uid
  125. -'mail_create_nolog':在创建时,不要记录自动的“<Document>“创建”消息
  126. -'mail_notrack':在创建和写入时,不要执行值跟踪创建消息
  127. -'tracking_disable':在创建和写入时,不执行邮件线程功能(自动订阅、跟踪、发布等)
  128. -'mail_notify_force_send': 如果要发送的电子邮件通知少于50封,直接发送,而不是使用队列;默认情况下为True
  129. """
  130. return (
  131. SudoUser.with_context(
  132. mail_create_nosubscribe=True,
  133. mail_create_nolog=True,
  134. mail_notrack=True,
  135. tracking_disable=True,
  136. send_mail=send_mail,
  137. send_message=send_message,
  138. )
  139. .create(values)
  140. .id
  141. )
  142. # return SudoUser.with_context(send_mail=send_mail).create(values).id
  143. @api.model_create_multi
  144. def create(self, vals_list):
  145. """
  146. 重写以自动邀请用户注册
  147. send_mail: true 表示发送邀请邮件, false 表示不发送邀请邮件
  148. 批量创建用户时,建议 send_mail=False
  149. """
  150. users = super(User, self).create(vals_list)
  151. if not self.env.context.get("no_reset_password") and self.env.context.get(
  152. "send_mail"
  153. ):
  154. users_with_email = users.filtered("email")
  155. if users_with_email:
  156. try:
  157. users_with_email.with_context(
  158. create_user=True
  159. ).action_reset_password()
  160. except MailDeliveryException:
  161. users_with_email.partner_id.with_context(
  162. create_user=True
  163. ).signup_cancel()
  164. return users
  165. # ------------------------------------------------------------
  166. # 企微部门下载
  167. # ------------------------------------------------------------
  168. @api.model
  169. def download_wecom_contacts(self):
  170. """
  171. 下载企微通讯录
  172. """
  173. start_time = time.time()
  174. company = self.env.context.get("company_id")
  175. tasks = []
  176. if not company:
  177. company = self.env.company
  178. if company.is_wecom_organization:
  179. try:
  180. wxapi = self.env["wecom.service_api"].InitServiceApi(
  181. company.corpid, company.contacts_app_id.secret
  182. )
  183. app_config = self.env["wecom.app_config"].sudo()
  184. contacts_sync_hr_department_id = app_config.get_param(
  185. company.contacts_app_id.id, "contacts_sync_hr_department_id"
  186. ) # 需要同步的企业微信部门ID
  187. response = wxapi.httpCall(
  188. self.env["wecom.service_api_list"].get_server_api_call("USER_LIST"),
  189. {
  190. "department_id": contacts_sync_hr_department_id,
  191. "fetch_child": "1",
  192. },
  193. )
  194. except ApiException as ex:
  195. end_time = time.time()
  196. self.env["wecomapi.tools.action"].ApiExceptionDialog(
  197. ex, raise_exception=False
  198. )
  199. tasks = [
  200. {
  201. "name": "download_contact_data",
  202. "state": False,
  203. "time": end_time - start_time,
  204. "msg": str(ex),
  205. }
  206. ]
  207. except Exception as e:
  208. end_time = time.time()
  209. tasks = [
  210. {
  211. "name": "download_contact_data",
  212. "state": False,
  213. "time": end_time - start_time,
  214. "msg": str(e),
  215. }
  216. ]
  217. else:
  218. wecom_users = response["userlist"]
  219. # 获取block
  220. blocks = (
  221. self.env["wecom.contacts.block"]
  222. .sudo()
  223. .search(
  224. [
  225. ("company_id", "=", company.id),
  226. ]
  227. )
  228. )
  229. block_list = []
  230. # 生成 block_list
  231. if len(blocks) > 0:
  232. for obj in blocks:
  233. if obj.wecom_userid != None:
  234. block_list.append(obj.wecom_userid)
  235. # 从 wecom_users 移除 block_list
  236. for b in block_list:
  237. for item in wecom_users:
  238. # userid不区分大小写
  239. if item["userid"].lower() == b.lower():
  240. wecom_users.remove(item)
  241. # 1. 下载联系人
  242. for wecom_user in wecom_users:
  243. download_user_result = self.download_user(company, wecom_user)
  244. if download_user_result:
  245. for r in download_user_result:
  246. tasks.append(r) # 加入设置下载联系人失败结果
  247. # 2.完成下载
  248. end_time = time.time()
  249. task = {
  250. "name": "download_contact_data",
  251. "state": True,
  252. "time": end_time - start_time,
  253. "msg": _("Contacts list downloaded successfully."),
  254. }
  255. tasks.append(task)
  256. else:
  257. end_time = time.time()
  258. tasks = [
  259. {
  260. "name": "download_contact_data",
  261. "state": False,
  262. "time": end_time - start_time,
  263. "msg": _(
  264. "The current company does not identify the enterprise wechat organization. Please configure or switch the company."
  265. ),
  266. }
  267. ] # 返回失败结果
  268. return tasks
  269. def download_user(self, company, wecom_user):
  270. """
  271. 下载联系人
  272. """
  273. user = self.sudo().search(
  274. [
  275. ("wecom_userid", "=", wecom_user["userid"].lower()),
  276. ("company_id", "=", company.id),
  277. ("is_wecom_user", "=", True),
  278. "|",
  279. ("active", "=", True),
  280. ("active", "=", False),
  281. ],
  282. limit=1,
  283. )
  284. result = {}
  285. app_config = self.env["wecom.app_config"].sudo()
  286. contacts_allow_add_system_users = app_config.get_param(
  287. company.contacts_app_id.id, "contacts_allow_add_system_users"
  288. ) # 允许创建用户
  289. if contacts_allow_add_system_users == "True":
  290. contacts_allow_add_system_users = True
  291. elif contacts_allow_add_system_users is None:
  292. contacts_allow_add_system_users = False
  293. else:
  294. contacts_allow_add_system_users = False
  295. if not user and contacts_allow_add_system_users:
  296. result = self.create_user(company, user, wecom_user)
  297. else:
  298. result = self.update_user(company, user, wecom_user)
  299. return result
  300. def create_user(self, company, user, wecom_user):
  301. """
  302. 创建联系人
  303. """
  304. params = self.env["ir.config_parameter"].sudo()
  305. debug = params.get_param("wecom.debug_enabled")
  306. app_config = self.env["wecom.app_config"].sudo()
  307. contacts_use_default_avatar = app_config.get_param(
  308. company.contacts_app_id.id,
  309. "contacts_use_default_avatar_when_adding_employees",
  310. ) # 使用系统微信默认头像的标识
  311. try:
  312. groups_id = (
  313. self.sudo()
  314. .env["res.groups"]
  315. .search(
  316. [
  317. ("id", "=", 9),
  318. ],
  319. limit=1,
  320. )
  321. .id
  322. ) # id=1是内部用户, id=9是门户用户
  323. user.create(
  324. {
  325. "notification_type": "inbox",
  326. "company_ids": [Command.link(user.company_id.id)],
  327. "company_id": user.company_id.id,
  328. "name": wecom_user["name"],
  329. "login": wecom_user["userid"].lower(), # 登陆账号 使用 企业微信用户id的小写
  330. "password": self.env["wecomapi.tools.security"].random_passwd(8),
  331. "email": wecom_user["email"],
  332. "work_phone": wecom_user["telephone"],
  333. "mobile_phone": wecom_user["mobile"],
  334. "employee_phone": wecom_user["mobile"],
  335. "work_email": wecom_user["email"],
  336. "gender": self.env["wecomapi.tools.convert"].sex2gender(
  337. wecom_user["gender"]
  338. ),
  339. "wecom_userid": wecom_user["userid"].lower(),
  340. "image_1920": self.env["wecomapi.tools.file"].get_avatar_base64(
  341. contacts_use_default_avatar,
  342. wecom_user["gender"],
  343. wecom_user["avatar"],
  344. ),
  345. "qr_code": wecom_user["qr_code"],
  346. "active": True if wecom_user["status"] == 1 else False,
  347. "is_wecom_user": True,
  348. "is_company": False,
  349. "share": False,
  350. "groups_id": [(6, 0, [groups_id])], # 设置用户为门户用户
  351. "tz": "Asia/Shanghai",
  352. "lang": "zh_CN",
  353. }
  354. )
  355. except Exception as e:
  356. result = _("Error creating company %s partner %s %s, error reason: %s") % (
  357. company.name,
  358. wecom_user["userid"].lower(),
  359. wecom_user["name"],
  360. repr(e),
  361. )
  362. if debug:
  363. _logger.warning(result)
  364. return {
  365. "name": "add_partner",
  366. "state": False,
  367. "time": 0,
  368. "msg": result,
  369. } # 返回失败结果
  370. def update_user(self, company, user, wecom_user):
  371. """
  372. 更新联系人
  373. """
  374. params = self.env["ir.config_parameter"].sudo()
  375. debug = params.get_param("wecom.debug_enabled")
  376. app_config = self.env["wecom.app_config"].sudo()
  377. contacts_use_default_avatar = app_config.get_param(
  378. company.contacts_app_id.id,
  379. "contacts_use_default_avatar_when_adding_employees",
  380. ) # 使用系统微信默认头像的标识
  381. try:
  382. user.write(
  383. {
  384. "notification_type": "inbox",
  385. "name": wecom_user["name"],
  386. "login": wecom_user["userid"].lower(), # 登陆账号 使用 企业微信用户id的小写
  387. "email": wecom_user["email"],
  388. "work_phone": wecom_user["telephone"],
  389. "mobile_phone": wecom_user["mobile"],
  390. "employee_phone": wecom_user["mobile"],
  391. "work_email": wecom_user["email"],
  392. "gender": self.env["wecomapi.tools.convert"].sex2gender(
  393. wecom_user["gender"]
  394. ),
  395. "wecom_userid": wecom_user["userid"].lower(),
  396. # "image_1920": self.env["wecomapi.tools.file"].get_avatar_base64(
  397. # contacts_use_default_avatar,
  398. # wecom_user["gender"],
  399. # wecom_user["avatar"],
  400. # ),
  401. "qr_code": wecom_user["qr_code"],
  402. "active": True if wecom_user["status"] == 1 else False,
  403. }
  404. )
  405. except Exception as e:
  406. result = _("Error creating company %s partner %s %s, error reason: %s") % (
  407. company.name,
  408. wecom_user["userid"].lower(),
  409. wecom_user["name"],
  410. repr(e),
  411. )
  412. if debug:
  413. _logger.warning(result)
  414. return {
  415. "name": "update_partner",
  416. "state": False,
  417. "time": 0,
  418. "msg": result,
  419. } # 返回失败结果
  420. # ------------------------------------------------------------
  421. # 企微通讯录事件
  422. # ------------------------------------------------------------
  423. def wecom_event_change_contact_user(self, cmd):
  424. """
  425. 通讯录事件变更系统用户
  426. """
  427. xml_tree = self.env.context.get("xml_tree")
  428. company_id = self.env.context.get("company_id")
  429. xml_tree_str = etree.fromstring(bytes.decode(xml_tree))
  430. dic = lxml_to_dict(xml_tree_str)["xml"]
  431. domain = [
  432. "|",
  433. ("active", "=", True),
  434. ("active", "=", False),
  435. ]
  436. user = self.sudo().search([("company_id", "=", company_id.id)] + domain)
  437. callback_user = user.search(
  438. [("wecom_userid", "=", dic["UserID"].lower())] + domain,
  439. limit=1,
  440. )
  441. # print("用户CMD", cmd)
  442. if callback_user:
  443. # 如果存在,则更新
  444. # 用于退出企业微信又重新加入企业微信的员工
  445. cmd = "update"
  446. else:
  447. # 如果不存在且不允许添加系统用户,停止
  448. app_config = self.env["wecom.app_config"].sudo()
  449. allow_add_system_users = app_config.get_param(
  450. company_id.contacts_app_id.id,
  451. "contacts_allow_add_system_users",
  452. ) # 使用系统微信默认头像的标识
  453. if allow_add_system_users is False:
  454. return
  455. update_dict = {}
  456. for key, value in dic.items():
  457. if (
  458. key == "ToUserName"
  459. or key == "FromUserName"
  460. or key == "CreateTime"
  461. or key == "Event"
  462. or key == "MsgType"
  463. or key == "ChangeType"
  464. ):
  465. # 忽略掉 不需要的key
  466. pass
  467. else:
  468. if key in WECOM_USER_MAPPING_ODOO_USER.keys():
  469. if WECOM_USER_MAPPING_ODOO_USER[key] != "":
  470. if WECOM_USER_MAPPING_ODOO_USER[key] == "wecom_userid":
  471. update_dict.update({"wecom_userid": value.lower()})
  472. elif WECOM_USER_MAPPING_ODOO_USER[key] == "active":
  473. # 状态
  474. # 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。
  475. # 已激活代表已激活企业微信或已关注微工作台(原企业号)。未激活代表既未激活企业微信又未关注微工作台(原企业号)。
  476. if value == "1":
  477. update_dict.update({"active": True})
  478. else:
  479. update_dict.update({"active": False})
  480. elif WECOM_USER_MAPPING_ODOO_USER[key] == "gender":
  481. gender = self.env["wecomapi.tools.convert"].sex2gender(
  482. value
  483. )
  484. update_dict.update({"gender": gender})
  485. else:
  486. update_dict[WECOM_USER_MAPPING_ODOO_USER[key]] = value
  487. else:
  488. _logger.info(
  489. _(
  490. "There is no mapping for field [%s], please contact the developer."
  491. )
  492. % key
  493. )
  494. if cmd == "create":
  495. update_dict.update(
  496. {
  497. "is_wecom_user": True,
  498. }
  499. )
  500. try:
  501. wecomapi = self.env["wecom.service_api"].InitServiceApi(
  502. company_id.corpid, company_id.contacts_app_id.secret
  503. )
  504. response = wecomapi.httpCall(
  505. self.env["wecom.service_api_list"].get_server_api_call("USER_GET"),
  506. {"userid": update_dict["wecom_userid"]},
  507. )
  508. if response.get("errcode") == 0:
  509. update_dict.update({"qr_code": response.get("qr_code")})
  510. except:
  511. pass
  512. callback_user.create(update_dict)
  513. elif cmd == "update":
  514. if "wecom_userid" in update_dict:
  515. del update_dict["wecom_userid"]
  516. callback_user.write(update_dict)
  517. elif cmd == "delete":
  518. callback_user.write(
  519. {
  520. "active": False,
  521. }
  522. )
  523. # ------------------------------------------------------------
  524. # 变更用户类型向导
  525. # ------------------------------------------------------------
  526. class ChangeTypeWizard(models.TransientModel):
  527. _name = "change.type.wizard"
  528. _description = "Wizard to change user type(WeCom)"
  529. def _default_user_ids(self):
  530. user_ids = (
  531. self._context.get("active_model") == "res.users"
  532. and self._context.get("active_ids")
  533. or []
  534. )
  535. return [
  536. (
  537. 0,
  538. 0,
  539. {
  540. "user_id": user.id,
  541. "user_login": user.login,
  542. "user_name": user.name,
  543. },
  544. )
  545. for user in self.env["res.users"].browse(user_ids)
  546. ]
  547. user_ids = fields.One2many(
  548. "change.type.user", "wizard_id", string="Users", default=_default_user_ids
  549. )
  550. def change_type_button(self):
  551. self.ensure_one()
  552. self.user_ids.change_type_button()
  553. if self.env.user in self.mapped("user_ids.user_id"):
  554. return {"type": "ir.actions.client", "tag": "reload"}
  555. return {"type": "ir.actions.act_window_close"}
  556. class ChangeTypeUser(models.TransientModel):
  557. _name = "change.type.user"
  558. _description = "User, Change Type Wizard"
  559. wizard_id = fields.Many2one(
  560. "change.type.wizard", string="Wizard", required=True, ondelete="cascade"
  561. )
  562. user_id = fields.Many2one(
  563. "res.users", string="User", required=True, ondelete="cascade"
  564. )
  565. user_login = fields.Char(
  566. string="Login account",
  567. readonly=True,
  568. )
  569. user_name = fields.Char(string="Login name", readonly=True)
  570. # 用户类型参见res_group
  571. new_type = fields.Selection(
  572. [
  573. ("1", _("Internal User")),
  574. ("9", _("Portal User")),
  575. ("10", _("Public User")),
  576. ],
  577. string="User Type",
  578. default="1",
  579. )
  580. def change_type_button(self):
  581. for line in self:
  582. if not line.new_type:
  583. raise UserError(
  584. _(
  585. "Before clicking the 'Change User Type' button, you must modify the new user type"
  586. )
  587. )
  588. if (
  589. # 排除初始系统自带的用户
  590. line.user_id.id == 1
  591. or line.user_id.id == 2
  592. or line.user_id.id == 3
  593. or line.user_id.id == 4
  594. or line.user_id.id == 5
  595. ):
  596. pass
  597. else:
  598. if line.new_type == "1":
  599. try:
  600. line.user_id.employee_id = (
  601. self.env["hr.employee"].search(
  602. [
  603. ("id", "in", line.user_id.employee_ids.ids),
  604. ("company_id", "=", line.user_id.company_id.id),
  605. ],
  606. limit=1,
  607. ),
  608. )
  609. except Exception as e:
  610. print("用户 %s 类型变更错误,错误:%s" % (line.user_id.name, repr(e)))
  611. line.user_id.write({"groups_id": [(6, 0, line.new_type)]})
  612. self.write({"new_type": False})
上海开阖软件有限公司 沪ICP备12045867号-1