提交 05d3e519 authored 作者: 刘擎阳's avatar 刘擎阳

1.接收邮件自动更新提单小包

上级 d975d6da
......@@ -29,6 +29,7 @@
'views/cc_big_package_view.xml',
'views/cc_node_exception_reason_view.xml',
'views/cc_bl_view.xml',
'views/res_config_setting.xml',
# 'views/cc_customers_declaration_order_view.xml',
'templates/login.xml',
],
......
......@@ -6,3 +6,6 @@ from . import cc_node_exception_reason
from . import mail_thread
from . import common_common
from . import fetch_mail
from . import order_state_change_rule
from . import res_config_setting
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import poplib
import time
from ssl import SSLError
from socket import gaierror, timeout
from imaplib import IMAP4, IMAP4_SSL
from poplib import POP3, POP3_SSL
import email
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
from datetime import datetime, timedelta
from email.utils import parseaddr
import pytz
_logger = logging.getLogger(__name__)
MAX_POP_MESSAGES = 50
MAIL_TIMEOUT = 60
# Workaround for Python 2.7.8 bug https://bugs.python.org/issue23906
poplib._MAXLINE = 65536
month_dict = {
'Jan': '1',
'Feb': '2',
'Mar': '3',
'Apr': '4',
'May': '5',
'Jun': '6',
'Jul': '7',
'Aug': '8',
'Sep': '9',
'Oct': '10',
'Nov': '11',
'Dec': '12',
'1': 'Jan',
'2': 'Feb',
'3': 'Mar',
'4': 'Apr',
'5': 'May',
'6': 'Jun',
'7': 'Jul',
'8': 'Aug',
'9': 'Sep',
'10': 'Oct',
'11': 'Nov',
'12': 'Dec',
}
class FetchmailServer(models.Model):
"""Incoming POP/IMAP mail server account"""
_inherit = 'fetchmail.server'
_description = 'Incoming Mail Server'
_order = 'priority'
def get_location_time(self):
"""
获取当前时区时间(带时区)
:return:
"""
now_time = datetime.utcnow()
tz = self.env.user.tz or 'Asia/Shanghai'
return now_time.replace(tzinfo=pytz.timezone(tz))
def fetch_mail(self):
""" WARNING: meant for cron usage only - will commit() after each email! """
additionnal_context = {
'fetchmail_cron_running': True
}
MailThread = self.env['mail.thread']
for server in self:
_logger.info('start checking for new emails on %s server %s', server.server_type, server.name)
additionnal_context['default_fetchmail_server_id'] = server.id
count, failed = 0, 0
imap_server = None
pop_server = None
if server.server_type == 'imap':
try:
imap_server = server.connect()
imap_server.select()
# 匹配时间
mail_send_timezone = self.env['ir.config_parameter'].sudo().get_param('mail_send_timezone') or '+1'
mail_match_minute = self.env['ir.config_parameter'].sudo().get_param('mail_match_minute') or 180
utc_now = self.get_location_time()
if mail_send_timezone[0] == '+':
now = utc_now + timedelta(hours=int(mail_send_timezone[1:]))
else:
now = utc_now - timedelta(hours=int(mail_send_timezone[1:]))
before_now = (now - timedelta(minutes=int(mail_match_minute))).strftime("%Y-%m-%d %H:%M:%S")
# 今天 mail_before_day查询多少天以前的
today = datetime.now()
mail_before_day = int(self.env['ir.config_parameter'].sudo().get_param('mail_before_day') or 0)
offset = timedelta(days=-mail_before_day)
last_day = (today + offset).strftime('%d-%b-%Y')
_logger.info('last_day:%s,before_now:%s' % (last_day, before_now))
# imap_server._mode_utf8()
# result, data = imap_server.search(None, 'SINCE', last_day)
result, data = imap_server.search(None, '(UNSEEN)', last_day)
# result, data = imap_server.search(None, '(UNSEEN)')
_logger.info('mail_data:%s' % data[0])
for num in data[0].split():
result, data = imap_server.fetch(num, '(RFC822)')
msg = email.message_from_string(data[0][1].decode('utf-8', 'ignore'))
subject = msg.get('subject') # 标题
date = email.header.decode_header(msg.get('date')) # 发件时间
time_list = date[0][0].split(',')[1].split(' ')
# date里面的空格可能是一个两个
time_list = list(filter(None, time_list))
year = time_list[2] # 发件时间的年份
# 取8分钟之内接收的邮件
b_time = '%s %s %s %s' % (time_list[0], time_list[1], time_list[2], time_list[3])
_logger.info('--b_time:%s--' % b_time)
try:
a_time = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(b_time, "%d %b %Y %H:%M:%S"))
except Exception as e:
b_time = '%s-%s-%s %s' % (time_list[2], month_dict[time_list[1]], time_list[0], time_list[3])
a_time = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(b_time, "%Y-%m-%d %H:%M:%S"))
# print(a_time, before_now)
if a_time > before_now:
hdr, sender_email = parseaddr(msg.get('From')) # 获取发件人邮箱
email_body = ''
for part in msg.walk():
# 如果ture的话内容是没用的
if not part.is_multipart():
# 解码出内容
email_body = part.get_payload(decode=True).decode('utf-8', 'ignore')
break
if email_body:
if 'CDS REPORT' in subject.upper():
rule_obj = self.env['order.state.change.rule'].sudo()
rule_obj.fetch_mail_dlv(email_body=email_body, year=year)
imap_server.store(num, '-FLAGS', '\\Seen')
try:
pass
# res_id = MailThread.with_context(**additionnal_context).message_process(server.object_id.model, data[0][1], save_original=server.original, strip_attachments=(not server.attach))
except Exception:
_logger.info('Failed to process mail from %s server %s.', server.server_type,
server.name,
exc_info=True)
failed += 1
imap_server.store(num, '+FLAGS', '\\Seen')
self._cr.commit()
count += 1
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count,
server.server_type, server.name, (count - failed), failed)
except Exception as e:
_logger.info("General failure when trying to fetch mail from %s server %s.error:%s",
server.server_type,
server.name, e, exc_info=True)
finally:
if imap_server:
imap_server.close()
imap_server.logout()
elif server.server_type == 'pop':
try:
while True:
pop_server = server.connect()
(num_messages, total_size) = pop_server.stat()
pop_server.list()
for num in range(1, min(MAX_POP_MESSAGES, num_messages) + 1):
(header, messages, octets) = pop_server.retr(num)
message = (b'\n').join(messages)
res_id = None
try:
res_id = MailThread.with_context(**additionnal_context).message_process(
server.object_id.model, message, save_original=server.original,
strip_attachments=(not server.attach))
pop_server.dele(num)
except Exception:
_logger.info('Failed to process mail from %s server %s.', server.server_type,
server.name, exc_info=True)
failed += 1
self.env.cr.commit()
if num_messages < MAX_POP_MESSAGES:
break
pop_server.quit()
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", num_messages,
server.server_type, server.name, (num_messages - failed), failed)
except Exception:
_logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type,
server.name, exc_info=True)
finally:
if pop_server:
pop_server.quit()
server.write({'date': fields.Datetime.now()})
return True
......@@ -101,3 +101,33 @@ class MailThread(models.AbstractModel):
# raise exceptions.UserError(_("Unable to send message, please configure the sender's email address."))
return author_id, email_from
@api.model
def message_new(self, msg_dict, custom_values=None):
"""Called by ``message_process`` when a new message is received
for a given thread model, if the message did not belong to
an existing thread.
The default behavior is to create a new record of the corresponding
model (based on some very basic info extracted from the message).
Additional behavior may be implemented by overriding this method.
:param dict msg_dict: a map containing the email details and
attachments. See ``message_process`` and
``mail.message.parse`` for details.
:param dict custom_values: optional dictionary of additional
field values to pass to create()
when creating the new thread record.
Be careful, these values may override
any other values coming from the message.
:rtype: int
:return: the id of the newly created thread object
"""
data = {}
if isinstance(custom_values, dict):
data = custom_values.copy()
fields = self.fields_get()
name_field = self._rec_name or 'name'
if name_field in fields and not data.get('name'):
data[name_field] = msg_dict.get('subject', '')
# return self.create(data)
return True
\ No newline at end of file
# -*- coding: utf-8 -*-
# © <2016> <ToproERP hy>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import datetime
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, Warning
import logging
import re
import demjson
from datetime import datetime, timedelta
import re
_logger = logging.getLogger(__name__)
import html
class OrderStateChangeRule(models.Model):
_name = "order.state.change.rule"
_inherit = ['mail.thread']
_description = '提单状态变更规则'
# @api.constrains('email_subject', 'change_order_state')
# def check_change_order_state(self):
# if self.email_subject == 'DLV':
# if self.change_order_state != '已提货':
# raise ValidationError('邮件主题包含字段为DLV时,变更后提单状态必须是 已提货')
#
# name = fields.Char('变更规则名称', index=True, tracking=True)
# email_address = fields.Char('发件人邮箱', tracking=True)
# email_subject = fields.Selection([('DLV', 'DLV'), ('FFM', 'FFM'), ('RCF', 'RCF')], string='邮件主题包含字段', tracking=True)
# change_subject = fields.Selection([('提单', '提单'), ('配舱单', '配舱单')], string='变更主体', default='提单', tracking=True)
# order_state = fields.Selection(
# [('待提交', '待提交'), ('待审核', '待审核'), ('已订舱', '已订舱'), ('已发出', '已发出'), ('已收货', '已收货'), ('已安检', '已安检'), ('已入库', '已入库'),
# ('已组板', '已组板'), ('已发货', '已发货'),
# ('已落地', '已落地'), ('已卸机', '已卸机'), ('已提货', '已提货'), ('已完成', '已完成'), ('已取消', '已取消')], string='提单所属状态', index=True, tracking=True)
# change_order_state = fields.Selection(
# [('待提交', '待提交'), ('待审核', '待审核'), ('已订舱', '已订舱'), ('已发出', '已发出'), ('已收货', '已收货'), ('已安检', '已安检'), ('已入库', '已入库'),
# ('已组板', '已组板'), ('已发货', '已发货'),
# ('已落地', '已落地'), ('已提货', '已提货'), ('已完成', '已完成'), ('已取消', '已取消')], string='变更后提单状态', default='已提货', index=True,
# tracking=True)
# method = fields.Char('方法')
# regular_expression = fields.Char('正则表达式')
# active = fields.Boolean('有效性', default=True)
# 函数处理输出
def print_match(self, text):
# 正则表达式
pattern = r'([A-Za-z]+\d+)\s+(\d{3}-\d+)\s+(\d{4})?\.?(\d{1,2})\.(\d{1,2})\s+(\d{2}:\d{2})\s+\(([\+\-]?\d+)\)'
match = re.match(pattern, text)
if match:
return match
else:
return False
def fetch_mail_dlv(self, **kwargs):
email_body = kwargs['email_body']
year = kwargs['year']
# datas = demjson.decode(kwargs['datas'])
current_year = datetime.now().year
text_arr = email_body.split('\r\n') if '\r\n' in email_body else email_body.split('\n')
for text in text_arr:
try:
text = html.unescape(text)
match = self.print_match(text)
if match:
voyage_name = match.group(1) # SE901
order_no = match.group(2) # 436-10133970
year = match.group(3) # 11 或 2024
month = match.group(4) # 11
day = match.group(5) # 20
time = match.group(6) # 12:41
timezone_offset = int(match.group(7)) # +1 或 -8
# 如果没有提供年份,则使用当前年份
if not year: # 只给了月份和日,默认使用当前年份
year = str(current_year)
# 拼接日期时间字符串
date_str = f"{year}-{month}-{day} {time}"
# 转换为 datetime 对象
local_time = datetime.strptime(date_str, "%Y-%m-%d %H:%M")
# 调整时区
utc_time = local_time - timedelta(hours=timezone_offset)
before_min = self.env['ir.config_parameter'].sudo().get_param('before_min') or 20
before_utc_time = utc_time - timedelta(minutes=int(before_min))
sql = "select id from cc_bl where UPPER(REPLACE(REPLACE(REPLACE(bl_no, ' ', ''), '-', ''), '/', '')) = '{0}' " \
"and transport_tool_name='{1}' order by create_date desc limit 1".format(order_no.replace(' ', '').replace('-', '').replace('/', ''), voyage_name)
self._cr.execute(sql)
result = self._cr.fetchall()
print(result)
# bl_obj = self.env['cc.bl'].sudo().search([('bl_no', '=', order_no), ('transport_tool_name', '=', voyage_name)], order='create_date desc', limit=1)
bl_obj = self.env['cc.bl'].sudo().search([('id', '=', result[0][0])]) if result else False
if bl_obj and bl_obj.state != 'done':
self.push_clear_customs_start(bl_obj, before_utc_time)
self.push_clear_customs_end(bl_obj, utc_time)
except Exception as err:
logging.error('fetch_mail_dlv--error:%s' % str(err))
def push_clear_customs_start(self, order, utc_time):
# 创建向导
node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('tk_code', 'in', [False, ''])], limit=1)
push_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('tk_code', '=', 'cb_imcustoms_start')], limit=1)
vals = {
'bl_id': order.id,
'bl_count': 1,
'current_status': node_obj.id,
'update_status': push_node_obj.id,
'process_time': utc_time,
'is_ok': True
}
wizard_obj = self.env['batch.input.ship.package.status.wizard'].sudo().create(vals)
wizard_obj.change_ship_package_ids()
wizard_obj = wizard_obj.with_context(dict(self._context, active_id=order.id))
wizard_obj.submit()
def push_clear_customs_end(self, order, utc_time):
# 创建向导
node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('tk_code', '=', 'cb_imcustoms_start')], limit=1)
push_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('tk_code', '=', 'cb_imcustoms_finished')], limit=1)
vals = {
'bl_id': order.id,
'bl_count': 1,
'current_status': node_obj.id,
'update_status': push_node_obj.id,
'process_time': utc_time,
'is_ok': True
}
wizard_obj = self.env['batch.input.ship.package.status.wizard'].sudo().create(vals)
wizard_obj.change_ship_package_ids()
wizard_obj = wizard_obj.with_context(dict(self._context, active_id=order.id))
# print(wizard_obj.get_order())
wizard_obj.submit()
def calc_date(self, pick_date_text):
"""
获取月份
:param pick_date_text:
:return:
"""
month_abbr_arr = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
for mon in month_abbr_arr:
if mon != '':
month = mon.upper()
if month in pick_date_text:
return int(month_abbr_arr.index(mon))
return 0
# -*- coding: utf-8 -*-
# Part of SmartGo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models, _
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
before_min = fields.Integer('清关时间取值(早于清关结束)')
@api.model
def get_values(self):
"""
重载获取参数的方法,参数都存在系统参数中
:return:
"""
values = super(ResConfigSettings, self).get_values()
config = self.env['ir.config_parameter'].sudo()
before_min = config.get_param('before_min', default=10)
values.update(
before_min=before_min,
)
return values
def set_values(self):
super(ResConfigSettings, self).set_values()
ir_config = self.env['ir.config_parameter'].sudo()
ir_config.set_param("before_min", self.before_min or 10)
......@@ -56,3 +56,5 @@ access_cc_clearance_file_base.group_user,cc_clearance_file base.group_user,ccs_b
access_cc_clearance_file_base.group_erp_manager,cc_clearance_file base.group_erp_manager,ccs_base.model_cc_clearance_file,base.group_erp_manager,1,1,1,1
access_cc_clearance_file_ccs_base.group_clearance_of_customs_manager,cc_clearance_file ccs_base.group_clearance_of_customs_manager,ccs_base.model_cc_clearance_file,ccs_base.group_clearance_of_customs_manager,1,1,1,1
access_cc_clearance_file_ccs_base.group_clearance_of_customs_user,cc_clearance_file ccs_base.group_clearance_of_customs_user,ccs_base.model_cc_clearance_file,ccs_base.group_clearance_of_customs_user,1,0,0,0
order_state_change_rule_group_user,order_state_change_rule_group_user,ccs_base.model_order_state_change_rule,base.group_user,1,1,1,1
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_auto_push" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.auto.push</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('app_settings_block')]/div" position="before">
<div>
<h2>自动推送配置</h2>
<div class="row mt16 o_settings_container" id="auto_push">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<div class="text-muted">
<label for="before_min"/>
<field name="before_min"/>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论