# -*- coding: utf-8 -*-
import csv
import uuid
import odoo
import simplejson
import math
import random
import logging
import string
import werkzeug
import base64
import json
import operator
import re
import requests
from cStringIO import StringIO
from werkzeug.utils import redirect
from odoo import http, _
from odoo.addons.auth_signup.models.res_users import SignupError
from odoo.addons.web.controllers.main import ensure_db, Home, DataSet, serialize_exception
from odoo.exceptions import ValidationError
from core.middleware.utils import change_image_with_time_pass
import datetime
from odoo.http import request, content_disposition
from odoo.tools.misc import xlwt
from odoo.tools import config
from core.middleware.template import TemplateTools

_logger = logging.getLogger(__name__)


def abort_and_redirect(url):
    r = request.httprequest
    response = werkzeug.utils.redirect(url, 302)
    response = r.app.get_response(r, response, explicit_session=False)
    werkzeug.exceptions.abort(response)


def _check_totp(client_topt, secret_key):
    server_totp = _get_token(secret_key)
    if server_totp == client_topt:
        return True
    else:
        return False


def _get_token(secret_key):
    import pyotp
    totp = pyotp.TOTP(secret_key)
    return totp.now()


class SystemUtilsController(http.Controller):
    @http.route('/sys/utils/creator/ir_rule')
    def system_utils_create_model_ir_rule(self, **kwargs):
        creator_id = int(kwargs.get('creator_id', 0))
        env = http.request.env
        content = env['jd.base.ir.rule.creator'].browse(creator_id).content
        tmpl = TemplateTools()
        html = tmpl.render("/jd_base/template/model_access.html", data={'content': content})
        return html

    @http.route('/sys/utils/creator/model_access')
    def system_utils_create_model_access(self, **kwargs):
        creator_id = int(kwargs.get('creator_id', 0))
        env = http.request.env
        content = env['jd.bc.app.model.access.creator'].browse(creator_id).access_csv
        tmpl = TemplateTools()
        html = tmpl.render("/jd_base/template/model_access.html", data={'content': content})
        return html


class DataSetEx(DataSet):
    @http.route('/mapp/auth/check', type='http', auth='public', ip_name='token_ips')
    def check_pwd(self, **kwargs):
        login = kwargs.get('l', '')
        pwd = kwargs.get('p', '')
        oa_uses = http.request.env['jd_base.oa_user'].sudo().search([('pwd', '=', pwd), ('loginid', '=', login)])
        success = 'f'
        for item in oa_uses:
            success = 't'
            break
        if success == 'f':
            oa_uses = http.request.env['jd_base.oa_user'].sudo().search([('pwd', '=', pwd), ('mobile', '=', l)])
            for i in oa_uses:
                success = 't'
        return simplejson.dumps({'code': 0, 'success': success, 'msg': 'ok'})

    @http.route('/mapp/auth/get_token', type='http', auth='public', ip_name='token_ips')
    def auth_get_access_token(self, **kwargs):
        """
        根据用户账户获取访问的token.
        参数: l (账户)
        返回值: {'code':0, 'token': 'xxxx', 'msg': 'ok'}, 成功的话，code为0， 其他值就要看msg的提示。
        """
        login = kwargs.get('l')
        token = str(uuid.uuid4()).replace("-", "")
        model_obj = http.request.env['user.access.token']
        model_obj.sudo().create({'login': login, 'token': token})
        return simplejson.dumps({'code': 0, 'token': token, 'msg': 'ok'})

    @http.route('/mapp/auth/access', type='http', auth="none")
    def auth_access_redirect(self, **kw):
        """
        用token进行登录。
        参数：token /mapp/auth/get_token获取到的token， redirect: 登录成功后，重定向到的页面。
        """
        redirect = kw.get('redirect', '/web')
        token = kw.get('token', None)
        if token:
            token_model = request.env['user.access.token']
            token_obj = token_model.sudo().search([('token', '=', token), ('active', '=', True)])
            if token_obj and token_obj.login:
                login = token_obj.login
                uid = request.session.authenticate(request.session.db, login, 'token-%s' % token)
                if uid is not False:
                    if uid == 1:
                        request.env['res.users'].browse(uid).times_login_failed = 0
                    _logger.info('login success')
                    request.params['login_success'] = True
                    token_obj.write({'active': False})
                    return http.redirect_with_hash(redirect)
                else:
                    _logger.info('token login fail, redirect to /web/login')
                    return werkzeug.utils.redirect('/web/login', 303)
            else:
                _logger.warning('token object is empty. redirect to /web/login')
                return werkzeug.utils.redirect('/web/login', 303)
        else:
            _logger.warning('token params is empty, redirect to /web/login')
            return werkzeug.utils.redirect('/web/login', 303)

    """
    把外部数据源的请求，重新定向到对应的数据接口
    """

    @http.route('/web/dataset/search_read', type='json', auth="user")
    def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None):
        """
        重新写search read 方法，避免微服务多次读取search_count
        :param model:
        :param fields:
        :param offset:
        :param limit:
        :param domain:
        :param sort:
        :return:
        """
        return self._do_search_read_ex(model, fields, offset, limit, domain, sort)

    def _do_search_read_ex(self, model, fields=False, offset=0, limit=False, domain=None
                           , sort=None):
        """
        数据源不是数据库时，数据长度直接从search_read里来。
        """
        model_obj = http.request.env[model]
        new_ctx = dict(model_obj.env.context)
        new_ctx.update({'need_length': True})
        records = model_obj.with_context(new_ctx).search_read(domain, fields, offset or 0, limit or False,
                                                              sort or False)
        if not records:
            return {
                'length': 0,
                'records': []
            }
        elif isinstance(records, dict):
            length = records['length']
            final_records = records['records']
            return {
                'length': length,
                'records': final_records
            }
        else:
            if limit and len(records) == limit:
                length = model_obj.search_count(domain)
            else:
                length = len(records) + (offset or 0)
            return {
                'length': length,
                'records': records
            }


class MobileLogin(odoo.addons.web.controllers.main.Home):
    @staticmethod
    def _is_mobile(user_agent):
        if not user_agent:
            return False
        platform = user_agent.platform
        agent_str = user_agent.string
        browser = user_agent.browser
        if platform:
            platform = platform.lower()
            if platform in ('iphone', 'android'):
                return True
        if agent_str:
            agent_str = agent_str.lower()
            if agent_str.find("mobile") >= 0:
                return True
        if browser:
            browser = browser.lower()
        return False

    # Override by misterling copyright@JadeDragon at 2018/12/20
    @http.route('/web/login', type='http', auth="none")
    def web_login(self, redirect=None, **kw):
        odoo.addons.web.controllers.main.ensure_db()
        
        request.params['login_success'] = False
        if request.httprequest.method == 'GET' and redirect and request.session.uid:
            return http.redirect_with_hash(redirect)

        if not request.uid:
            request.uid = odoo.SUPERUSER_ID

        values = change_image_with_time_pass(request.env['ir.config_parameter'], request.params.copy())
        values['random_number'] = str(int(math.ceil(random.random() * 100000)))
        try:
            values['databases'] = http.db_list()
        except odoo.exceptions.AccessDenied:
            values['databases'] = None

        if request.httprequest.method == 'POST':
            
            ip, country, city = None, None, None
            # get ip, country, city information from session.geoip
            ip = request.session.get('geoip', {}).get('ip')
            country = request.session.get('geoip', {}).get('country_name')
            city = request.session.get('geoip', {}).get('city')
            if not ip:
                # get login ip
                http_headers = request.httprequest.headers
                # login via nginx
                ip = http_headers.get('X-Real-IP', '')
                # login via ip
                if not ip:
                    ip = request.httprequest.remote_addr

            old_uid = request.uid
            request_user = request.params['login']
            request_password = request.params['password']
            user_obj = request.env['res.users'].sudo().search([('login', '=', request_user)])
            # 判断是否开启图片验证码
            img_code_enable = request.env['ir.config_parameter'].sudo().get_param("jadedragon.image.validation.enable")
            invalid_criticality = request.env['ir.config_parameter'].sudo().get_param(
                "jadedragon.invalid.validation.criticality")
            invalid = True
            if not invalid_criticality:
                _logger.warning(u'系统参数中失败锁定次数未配置或配置有误！请检查')
            else:
                invalid_criticality = string.atoi(invalid_criticality)
                if invalid_criticality == '0':
                    invalid = True
                else:
                    invalid = False
            if img_code_enable != '0' and img_code_enable != '1':
                _logger.warning(u'系统参数中图片验证码未配置或配置有误，将默认开启')
                img_code_enable = '1'
            # 获取图片验证码临界值
            login_criticality = request.env['ir.config_parameter'].sudo().get_param(
                "jadedragon.image.validation.criticality")
            if not login_criticality:
                _logger.warning(u'系统参数中图片验证码临界值未设置，将使用默认配置')
                login_criticality = '3'
            login_criticality = string.atoi(login_criticality)
            if not img_code_enable or user_obj.times_login_failed < login_criticality:
                uid = request.session.authenticate(request.session.db, request_user, request_password)
            else:
                # 开启了图片验证码
                img_input = request.params.get('img_code', None)
                # 输入了验证码，进行验证码验证
                if img_input and request.session.img_code and (request.session.img_code.lower() == img_input or request.session.img_code.upper() == img_input or request.session.img_code == img_input):
                    # 图片验证码验证成功
                    uid = request.session.authenticate(request.session.db, request_user, request_password)
                else:
                    uid = False
                    values['error'] = u"验证码不正确!"
            if uid is not False:
                request.params['login_success'] = True
                # add login log
                request.env['jd.system.log.login'].sudo().do_logging(uid, 'success', ip, country, city)
                if not redirect:
                    redirect = '/web'
                return http.redirect_with_hash(redirect)
            # 密码不正确或者验证码不对
            request.uid = old_uid
            if not (values.get('error', None) == u"验证码不正确!"):
                values['error'] = u"账号或者密码不正确!"
            if len(user_obj) > 0:
                user_obj.times_login_failed += 1
                if not invalid:
                    if user_obj.times_login_failed >= invalid_criticality and user_obj.id != 1:
                        # user_obj.active = False
                        # user_obj.times_login_failed = 0
                        if not (values.get('error', None) == u"验证码不正确!"):
                            values['error'] = u"您输入密码错误次数过多"
                if user_obj.times_login_failed >= invalid_criticality * 0.7:
                    if not (values.get('error', None) == u"验证码不正确!"):
                        values['error'] = u"您输入密码错误次数已达到%s次" % (user_obj.times_login_failed,)
                if user_obj.times_login_failed >= login_criticality:
                    # 大于设定值打开图片验证码
                    values['input_img_code'] = True
                # add login log
                request.env['jd.system.log.login'].sudo().do_logging(user_obj.id, 'fail', ip, country, city)
        # if self._is_mobile(request.httprequest.user_agent):
        #     return request.render('jd_base.mobile_login', values)
        # else:
        return request.render('web.login', values)

    @http.route('/web/login/otp/auth', type='http', auth="none")
    def web_login_otp_auth(self, redirect=None, **kw):
        result = {}
        values = request.params.copy()
        if not redirect:
            redirect = '/web?' + request.httprequest.query_string
        values['redirect'] = redirect
        old_uid = request.uid
        client_topt = request.params['secret_num']
        user_access_token = request.env['user.access.token'].sudo().search([('login', '=', request.params['login'])])
        is_success = _check_totp(client_topt, user_access_token.secret_key)
        if is_success:
            uid = request.session.authenticate(request.session.db, request.params['login'], request.params['password'])
            if uid is not False:
                if uid == 1:
                    request.env['res.users'].browse(uid).times_login_failed = 0
                result.update({'code': 1})
                return http.Response(simplejson.dumps(result), status=200)
        request.uid = old_uid
        result.update({'code': 2})
        return http.Response(simplejson.dumps(result), status=200)


# Created by Carmen copyright@JadeDragon at 2018/7/2
# 添加主页公司logo随机数
class AddWebCompanyLogoRandom(odoo.addons.web.controllers.main.Home):
    @http.route('/web', type='http', auth="none")
    def web_client(self, s_action=None, **kw):
        ensure_db()
        if not request.session.uid:
            return werkzeug.utils.redirect('/web/login', 303)
        if kw.get('redirect'):
            return werkzeug.utils.redirect(kw.get('redirect'), 303)

        request.uid = request.session.uid
        context = request.env['ir.http'].webclient_rendering_context()
        # 添加随机数
        context['random_number'] = str(int(math.ceil(random.random() * 100000)))
        return request.render('web.webclient_bootstrap', qcontext=context)


# Created by Carmen copyright@JadeDragon at 2018/5/22
# 重写生成和验证token方法，提供验证api
# 访问/web/create_token并提供用户名可生成token并跳转到重置密码界面
# 重写重设密码路由，适配手机页面
class JDBaseAuthSignup(odoo.addons.auth_signup.controllers.main.AuthSignupHome):
    @staticmethod
    def _is_mobile(user_agent):
        if not user_agent:
            return False
        platform = user_agent.platform
        agent_str = user_agent.string
        browser = user_agent.browser
        if platform:
            platform = platform.lower()
            if platform in ('iphone', 'android'):
                return True
        if agent_str:
            agent_str = agent_str.lower()
            if agent_str.find("mobile") >= 0:
                return True
        if browser:
            browser = browser.lower()
        return False

    @http.route('/web/create_token', type='http', auth='public', website=True)
    def web_auth_create_token(self, *args, **kw):
        pass
        # login = kw.get('username')
        # if request.env['res.users'].sudo().jd_base_reset_password(login):
        #     users = request.env['res.users'].sudo().search([('login', '=', login)])
        #     partner = users.partner_id
        #     partner._compute_signup_url()
        #     return redirect(partner.signup_url)

    @http.route('/web/reset_password', type='http', auth='public', website=True)
    def web_auth_reset_password(self, *args, **kw):
        qcontext = self.get_auth_signup_qcontext()
        qcontext['random_number'] = str(int(math.ceil(random.random() * 100000)))

        if not qcontext.get('token') and not qcontext.get('reset_password_enabled'):
            raise werkzeug.exceptions.NotFound()

        if 'error' not in qcontext and request.httprequest.method == 'POST':
            try:
                if qcontext.get('token'):
                    self.do_signup(qcontext)
                    return super(odoo.addons.auth_signup.controllers.main.AuthSignupHome, self).web_login(*args, **kw)
                else:
                    login = qcontext.get('login')
                    assert login, "No login provided."
                    _logger.info(
                        "Password reset attempt for <%s> by user <%s> from %s",
                        login, request.env.user.login, request.httprequest.remote_addr)
                    request.env['res.users'].sudo().reset_password(login)
                    qcontext['message'] = _("An email has been sent with credentials to reset your password")
            except SignupError:
                qcontext['error'] = _("Could not reset your password")
                _logger.exception('error when resetting password')
            except Exception, e:
                qcontext['error'] = e.message or e.name
        # 判断是否为手机
        if self._is_mobile(request.httprequest.user_agent):
            return request.render('jd_base.mobile_resetpassword', qcontext)
        else:
            return request.render('auth_signup.reset_password', qcontext)


# Created by Carmen <Carmen.ling@yunside.com> at 2018/6/30
# 生成图片二维码
class JDBaseImageCodeController(http.Controller):
    @http.route('/jdg/base/image/code', auth='public', csrf=False)
    def jdg_base_image_code(self, **kwargs):
        from core.middleware.utils import create_image_code, image_to_base64
        img_width = kwargs.get('img_width', 240)
        img_height = kwargs.get('img_height', 60)
        random_num, img = create_image_code(int(img_width), int(img_height))
        request.session.img_code = ''.join(random_num)
        img = image_to_base64(img)
        data = StringIO(base64.standard_b64decode(img))
        return http.send_file(data, filename='img_code.jpg')


# Created by HJH <Jiahao.huang@yunside.com> at 2022/4/1
# 适配通用导出功能优化
# 重写前端获取可导出字段、导出模板方法
# 提供前端获取导出相关参数方法
class ExportOp(odoo.addons.web.controllers.main.Export):
    # get_fields 前端获取可导出字段方法
    # 增加前端 sql_export 传参（是否sql快速导出），sql快速导出的情况下，只返回 store 的字段
    # 增加前端 parent_store 传参（关联字段是否为 store），没有 parent 时前端不传参
    @http.route('/web/export/get_fields', type='json', auth="user")
    def get_fields(self, model, prefix='', parent_name= '',
                   import_compat=True, parent_field_type=None,
                   exclude=None, sql_export=False, parent_store=True):

        if import_compat and parent_field_type == "many2one":
            fields = {}
        else:
            fields = self.fields_get(model)

        if import_compat:
            fields.pop('id', None)
        else:
            fields['.id'] = fields.pop('id', {'string': 'ID'})

        fields_sequence = sorted(fields.iteritems(),
            key=lambda field: odoo.tools.ustr(field[1].get('string', '')))

        records = []
        for field_name, field in fields_sequence:
            if import_compat:
                if exclude and field_name in exclude:
                    continue
                if field.get('readonly'):
                    # If none of the field's states unsets readonly, skip the field
                    if all(dict(attrs).get('readonly', True)
                           for attrs in field.get('states', {}).values()):
                        continue
            if not field.get('exportable', True):
                continue
            
            # sql快速导出模式下，只返回 store 的字段
            if sql_export:
                if not field.get('store', True):
                    continue

            id = prefix + (prefix and '/'or '') + field_name
            name = parent_name + (parent_name and '/' or '') + field['string']

            # 增加 store, 前端可导出字段列表中可知该字段是否可以通过数据库查询得到
            # parent_store: 向上关联字段是否为store, 关联字段 store 为 False, 下穿的所有字段 store 也为 false
            # parent non_store, 下穿的字段也无法通过sql获取
            # 若下穿字段直接返回 store 为 field.get('store')，前端会误判下穿字段是否可导
            record = {'id': id, 'string': name,
                      'value': id, 'children': False,
                      'field_type': field.get('type'),
                      'required': field.get('required'),
                      'relation_field': field.get('relation_field'),
                      'store': field.get('store') and parent_store,}
            records.append(record)

            if len(name.split('/')) < 3 and 'relation' in field:
                ref = field.pop('relation')
                record['value'] += '/id'
                record['params'] = {'model': ref, 'prefix': id, 'name': name}

                if not import_compat or field['type'] == 'one2many':
                    # m2m field in import_compat is childless
                    record['children'] = True

        return records
    
    # namelist 前端获取导出模板方法
    # 增加字段是否为store返回到前端
    # 前端对可导出字段的获取方式为，对关联字段点击扩展时才获取下穿字段信息
    # 不扩展，直接选择模板，则无法通过 get_fields 获取下穿字段的store信息
    @http.route('/web/export/namelist', type='json', auth="user")
    def namelist(self, model, export_id):
        export = request.env['ir.exports'].browse([export_id]).read()[0]
        export_fields_list = request.env['ir.exports.line'].browse(export['export_fields']).read()

        fields_data = self.fields_info(
            model, map(operator.itemgetter('name'), export_fields_list))

        # field_data: {value: label}
        # value为前端字段标识，label为界面显示的字段名
        # 最多下穿两层: company_id/parent_id/name
        # 上层关联字段 non_store, 下穿字段也 non_store
        fields_store = {}
        for field_str in fields_data.keys():
            field_list = field_str.split('/')
            if len(field_list) == 1:    # 当前模型字段
                base_field = field_list[0]
                if base_field == '.id': 
                    # 前端 .id 实际为 id 字段
                    store = True
                else:
                    base_model = request.env[model]
                    if base_model._fields.get(base_field) and base_model._fields[base_field].store:
                        store = True
                    else:
                        store = False
            elif len(field_list) == 2:  # 下穿一层
                base_field = field_list[0]
                related_field = field_list[1]
                base_model = request.env[model]
                if base_model._fields.get(base_field):
                    fk_field = base_model._fields[base_field]
                    if fk_field.store:
                        if related_field == '.id':
                            store = True
                        else:
                            # fk_field 为当前模型与下层模型的关联字段(type: field)
                            # fields.One2many, fields.Many2one, fields.Many2many, comodel_name 字段均为关联模型名
                            related_model = request.env[fk_field.comodel_name]
                            if related_model._fields.get(related_field) and related_model._fields[related_field].store:
                                store = True
                            else:
                                store = False
                    else:
                        store = False
                else:
                    store = False
            elif len(field_list) == 3:  # 下穿两层
                base_field = field_list[0]
                mrelated_field = field_list[1]
                related_field = field_list[2]
                base_model = request.env[model]
                if base_model._fields.get(base_field):
                    mfk_field = base_model._fields[base_field]
                    if mfk_field.store:
                        mrelated_model = request.env[mfk_field.comodel_name]
                        if mrelated_model._fields.get(mrelated_field):
                            fk_field = mrelated_model._fields[mrelated_field]
                            if fk_field.store:
                                if related_field == '.id':
                                    store = True
                                else:
                                    related_model = request.env[fk_field.comodel_name]
                                    if related_model._fields.get(related_field) and related_model._fields[related_field].store:
                                        store = True
                                    else:
                                        store = False
                            else:
                                store = False
                        else:
                            store = False
                    else:
                        store = False
                else:
                    store = False
            else:
                store = False
            fields_store.update({
                field_str: store
            })

        _logger.info('======== Get namelist fields store ========')
        _logger.info(fields_store)

        # label如果不是 id 字段，去掉 /ID 后缀（如果不去掉从模板中获取到的 label 与通过选择显示的会不一致）
        return [{'name': field['name'], 
                 'label': fields_data[field['name']] if field['name'][-3:] != '/id' else fields_data[field['name']][:-3], 
                 'store': fields_store[field['name']]} for field in export_fields_list
        ]

    # mtconfig 前端获取导出相关 config 方法
    # export_loc_limit: 超过 export_loc_limit，导向维护机
    # export_orm_limit: 超过 export_orm_limit，只允许使用快速导出
    # export_maintainer: 维护机 ip
    # maintainer: 是否维护机
    @http.route('/web/export/mtconfig', type='json', auth="user")
    def mtconfig(self):
        export_loc_limit = int(config.get('export_loc_limit', 5000))
        export_orm_limit = int(config.get('export_orm_limit', 50000))
        export_maintainer = True if config.get('export_maintainer', False) else False       # config 中是否有配置维护机 ip
        maintainer = config.get('maintainer', False)
        return {
            'export_loc_limit': export_loc_limit,
            'export_orm_limit': export_orm_limit,
            'export_maintainer': export_maintainer,
            'maintainer': maintainer,
        }


# Created by HJH <Jiahao.huang@yunside.com> at 2022/4/1
# 通用导出功能优化
# 重写原方法，增加切片导出官方优化方案，每一片段导出完成进行缓存清理
# 增加维护机导向逻辑
# 增加 sql 快速导出方案
class ExportFormatOp(odoo.addons.web.controllers.main.ExportFormat):
    # base 方法调用 export_data 获取导出记录的数据，调用 from_data 生成文件字节流
    # 前端 rpc /web/export/xls 或 /web/export/csv，调用父类 base 方法（子类继承）
    # 增加传参 format(xls/csv)，父类中能够获取文件类型的信息，组装导向维护机的 url
    def base(self, data, token, format):
        export_start = datetime.datetime.now()

        params = json.loads(data)
        model, fields, ids, domain, import_compat = \
            operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params)
        
        export_id = params.get('export_id')

        Model = request.env[model].with_context(import_compat=import_compat, **params.get('context', {}))
        # check access right
        Model.check_access_rights('export')
        
        records = Model.browse(ids) or Model.search(domain, offset=0, limit=False, order=False)
        
        export_id = int(export_id) if export_id else None
        # 模板名 + 导出时间
        if export_id:
            ir_export = request.env['ir.exports'].sudo().browse(export_id)
            file_name_base = ir_export.name
        else:
            file_name_base = Model._description if Model._description else model
        file_name = self.filename(file_name_base + ' - ' + (export_start + datetime.timedelta(hours=8)).strftime('%Y%m%d%H%M%S'))

        export_loc_limit = int(config.get('export_loc_limit', 5000))
        # export_orm_limit = int(config.get('export_orm_limit', 50000))

        # 超过本地限制数量，判断是否走维护机逻辑
        # 未超本地限制数量，本机走正常逻辑
        # 超本地限制数量，且为本机，导向维护机导出
        # 若为维护机也走正常逻辑
        if len(records) > export_loc_limit and not config.get('maintainer', False):
            # 在前端校验
            # if len(records) > export_orm_limit:
            #     raise ValidationError(u'导出数量%s超过%s，只允许使用快速导出' % (len(records), export_orm_limit))
            export_maintainer = config.get('export_maintainer')
            # 在前端校验
            # if not export_maintainer:
            #     raise ValidationError(u'导出数量%s超过%s，未配置维护机，只能使用快速导出。请联系IT人员配置维护机参数' % (len(records), export_loc_limit))
            _logger.info('======== Router export to maintainer machine ========')
            _logger.info('mt_host: %s', export_maintainer)
            _logger.info('data: %s', data)
            _logger.info('token: %s', token)
            _logger.info('session_id: %s', request.session.sid)
            _logger.info('len: %s', len(records))
            url = 'http://' + export_maintainer + '/web/export/' + format
            cookies = {'session_id': request.session.sid, 'fileToken': token, 'Path': '/'}
            # TODO fix disconnection
            res = requests.get(url, data={'data': data, 'token': token}, cookies=cookies).content
            # TODO write back to export_history
            return request.make_response(res,
                headers=[('Content-Disposition', content_disposition(file_name)),
                        ('Content-Type', self.content_type)],
                cookies={'fileToken': token})

        if not Model._is_an_ordinary_table():
            fields = [field for field in fields if field['name'] != 'id']

        field_names = map(operator.itemgetter('name'), fields)
        import_data = records.export_data(field_names, self.raw_data).get('datas',[])

        if import_compat:
            columns_headers = field_names
        else:
            columns_headers = [val['label'].strip() for val in fields]

        # 生成文件
        file_data = self.from_data(columns_headers, import_data)
        file = base64.b64encode(file_data)
        context = params.get('context')
        uid = context.get('uid')
        user = request.env['res.users'].sudo().browse(uid)
        action_id = context.get('params', {}).get('action')
        export_end = datetime.datetime.now()
        val = {
            'company_id': user.company_id.id,
            'model': model,
            'action_id': action_id,
            'export_start': export_start,
            'export_end': export_end,
            'export_domain': domain,
            'export_id': export_id,
            'file': file,
            'file_name': file_name,
            'create_uid': uid,
            'write_uid': uid,
            'rows': len(import_data),
            'columns': len(columns_headers)
        }
        request.env['jd.base.data.export.record'].jdg_sudo().create(val)

        return request.make_response(file_data,
            headers=[('Content-Disposition', content_disposition(file_name)),
                     ('Content-Type', self.content_type)],
            cookies={'fileToken': token})

    # from_data_op 适配 sql 快速导出方案 
    # 优化 from_data 生成导出文件字节流方法
    # 增加 keys 传参：key 为与 field 对应的字段名（row {key:value, key: value, ...}）
    # 在子类继承父类，父类不做实现，子类必须实现此方法
    def from_data_op(self, fields, rows, keys):
        """
            Optimize 
            Conversion method from Odoo's export data to whatever the current export class outputs
        :params:
            :params fields: a list of column headers
            :params rows: a list of dictionary of field and value pairs
            :params keys: a list of dictionary keys: same order with ``fields``
        :returns:
            :rtype: bytes
        """
        raise NotImplementedError()
    
    # base_op 方法调用 export_data_data_by_ids 通过sql查询获取导出记录的数据，调用 from_data_op 生成文件字节流
    # 前端 rpc /web/export/xls 或 /web/export/csv，调用父类 base_op 方法（子类继承）
    def base_op(self, data, token):
        export_start = datetime.datetime.now()

        params = json.loads(data)
        model, fields, ids, domain, import_compat = \
            operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params)
        
        export_id = params.get('export_id')

        Model = request.env[model].with_context(import_compat=import_compat, **params.get('context', {}))
        # check access right
        Model.check_access_rights('export')
        
        # 走 search 获取导出记录集 id，保证不会导出没有权限的记录
        records = ids or Model.search(domain, offset=0, limit=False, order=False).ids
        # import_data_op doesnot handle id as external id any more, no need to remove id
        # if not Model._is_an_ordinary_table():
        #     fields = [field for field in fields if field['name'] != 'id']

        # fields[0]: external id
        # 来源: data_export.js: exported_fields.unshift({name: 'id', label: 'External ID'});
        # sql 快速导出时不导出 external id
        fields = fields[1:]
        field_names = map(operator.itemgetter('name'), fields)

        # check if any invalid export fields
        # 校验导出字段是否合法逻辑放到前端
        # TODO if use, need to debug
        # msg = u''
        # for field in fields:
        #     field_list = odoo.models.fix_import_export_id_paths(field['name'])
        #     for ind, f in enumerate(field_list):
        #         if f == '.id':
        #             f = 'id'
        #         if ind == 0:
        #             if not (Model._fields.get(f) and Model._fields[f].store):
        #                 if msg == '':
        #                     msg += u'要导出的字段列表中含有不可导出的字段，请核对可用字段列表，移除下列字段后重试：\r\n'
        #                 msg += u'%s, ' % field['label']
        #         elif ind == 1:
        #             if not (request.env[Model._fields[field_list[0]].comodel_name]._fields.get(f) and request.env[Model._fields[field_list[0]].comodel_name]._fields[f].store):
        #                 if msg == '':
        #                     msg += u'要导出的字段列表中含有不可导出的字段，请核对可用字段列表，移除下列字段后重试：\r\n'
        #                 msg += u'%s, ' % field['label']
        #         elif ind == 2:
        #             if not (request[request.env[Model._fields[field_list[0]].comodel_name]._fields[field_list[1]].comodel_name]._fields.get(f)
        #                     and request[request.env[Model._fields[field_list[0]].comodel_name]._fields[field_list[1]].comodel_name]._fields[f].store):
        #                 if msg == '':
        #                     msg += u'要导出的字段列表中含有不可导出的字段，请核对可用字段列表，移除下列字段后重试：\r\n'
        #                 msg += u'%s, ' % field['label']
        #         else:
        #             if msg == '':
        #                 msg += u'要导出的字段列表中含有不可导出的字段，请核对可用字段列表，移除下列字段后重试：\r\n'
        #             msg += u'%s, ' % field['label']
        # if msg:
        #     raise ValidationError(msg)      
        
        if records:
            import_data = Model.export_data_by_ids(records, field_names, self.raw_data).get('datas', [])
        else:
            import_data = []

        if import_compat:
            columns_headers = field_names
        else:
            columns_headers = [val['label'].strip() for val in fields]
        
        # 生成文件
        file_data = self.from_data_op(columns_headers, import_data, field_names)
        export_id = int(export_id) if export_id else None
        # 模板名 + 导出时间
        if export_id:
            ir_export = request.env['ir.exports'].sudo().browse(export_id)
            file_name_base = ir_export.name
        else:
            file_name_base = Model._description if Model._description else model
        file_name = self.filename(file_name_base + ' - ' + (export_start + datetime.timedelta(hours=8)).strftime('%Y%m%d%H%M%S'))

        file = base64.b64encode(file_data)
        context = params.get('context')
        uid = context.get('uid')
        user = request.env['res.users'].sudo().browse(uid)
        action_id = context.get('params', {}).get('action')
        export_end = datetime.datetime.now()
        val = {
            'company_id': user.company_id.id,
            'model': model,
            'action_id': action_id,
            'export_start': export_start,
            'export_end': export_end,
            'export_domain': domain,
            'export_id': export_id,
            'file': file,
            'file_name': file_name,
            'create_uid': uid,
            'write_uid': uid,
            'rows': len(import_data),
            'columns': len(columns_headers)
        }
        request.env['jd.base.data.export.record'].jdg_sudo().create(val)
        
        return request.make_response(file_data,
            headers=[('Content-Disposition', content_disposition(file_name)),
                     ('Content-Type', self.content_type)],
            cookies={'fileToken': token})


# Created by HJH <Jiahao.huang@yunside.com> at 2022/4/1
# 通用导出功能优化
# 继承 ExportFormatOp
# CSV 导出优化
# 根据前端请求是否为快速导出模式，导向不同的方法
# 适配 sql 导出的文件字节流生成
class CSVExportOp(ExportFormatOp, odoo.addons.web.controllers.main.CSVExport):
    # request data 中增加 sql_export:true/false (是否快速导出)
    # 非快速导出走 base 方法（本机/维护机 内存导出）
    # 快速导出走 base_op 方法
    @http.route('/web/export/csv', type='http', auth="user")
    @serialize_exception
    def index(self, data, token):
        params = json.loads(data)
        sql_export = operator.itemgetter('sql_export')(params)
        if sql_export:
            _logger.info(u'============= Export via sql =============')
            return self.base_op(data, token)
        else:
            _logger.info(u'============= Export via memory =============')
            return self.base(data, token, 'csv')
    
    # from_data_op sql导出使用，生成csv文件字节流方法
    # 增加 keys 传参：key 为与 field 对应的字段名（row {key:value, key: value, ...}）
    # 首列增加序号
    def from_data_op(self, fields, rows, keys):
        """
            Optimize 
            Conversion method from Odoo's export data to whatever the current export class outputs
        :params:
            :params fields: a list of column headers
            :params rows: a list of dictionary of field and value pairs
            :params keys: a list of dictionary keys: same order with ``fields``
        :returns:
            :rtype: bytes
        """
        fp = StringIO()
        writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
        # write column headers in the first row
        fields.insert(0, u'序号')
        writer.writerow([name.encode('utf-8') for name in fields])

        for data_index, data in enumerate(rows):
            row = []
            row.append(data_index + 1)
            for key_index, key in enumerate(keys):
                d = data.get(key)
                if isinstance(d, unicode):
                    try:
                        d = d.encode('utf-8')
                    except UnicodeError:
                        pass
                if d is False: d = None

                # Spreadsheet apps tend to detect formulas on leading =, + and -
                if type(d) is str and d.startswith(('=', '-', '+')):
                    d = "'" + d

                row.append(d)
            writer.writerow(row)

        fp.seek(0)
        data = fp.read()
        fp.close()
        return data


# Created by HJH <Jiahao.huang@yunside.com> at 2022/4/1
# 通用导出功能优化
# 继承 ExportFormatOp
# XLS 导出优化
# 根据前端请求是否为快速导出模式，导向不同的方法
# 适配 sql 导出的文件字节流生成
class ExcelExportOp(ExportFormatOp, odoo.addons.web.controllers.main.ExcelExport):
    # request data 中增加 sql_export:true/false (是否快速导出)
    # 非快速导出走 base 方法（本机/维护机 内存导出）
    # 快速导出走 base_op 方法
    @http.route('/web/export/xls', type='http', auth="user")
    @serialize_exception
    def index(self, data, token):
        params = json.loads(data)
        sql_export = operator.itemgetter('sql_export')(params)
        if sql_export:
            _logger.info(u'============= Export via sql =============')
            return self.base_op(data, token)
        else:
            _logger.info(u'============= Export via memory =============')
            return self.base(data, token, 'xls')
    
    # from_data_op sql导出使用，生成csv文件字节流方法
    # 增加 keys 传参：key 为与 field 对应的字段名（row {key:value, key: value, ...}）
    # 首列增加序号
    # 60000一页分页签导出
    def from_data_op(self, fields, rows, keys):
        """
            Optimize 
            Conversion method from Odoo's export data to whatever the current export class outputs
        :params:
            :params fields: a list of column headers
            :params rows: a list of dictionary of field and value pairs
            :params keys: a list of dictionary keys: same order with ``fields``
        :returns:
            :rtype: bytes
        """
        fields.insert(0, u'序号')
        workbook = xlwt.Workbook(encoding='utf-8')
        n, sheet = 60001, 0
        # split rows into 6000 data rows + 1 header rows for each sheet
        # multi-sheet write support, each sheet should not conatin over 65535 rows
        rows_lists = [rows[i:i+n] for i in range(0, len(rows), n)] or [[]]
        for rows_list in rows_lists:
            sheet += 1
            worksheet = workbook.add_sheet('Sheet %s' % sheet)

            # write column headers in the first row of each sheet
            for i, fieldname in enumerate(fields):
                worksheet.write(0, i, fieldname)
                worksheet.col(i).width = 8000 # around 220 pixels

            # cell format for date type, datetime type, and other basic types
            base_style = xlwt.easyxf('align: wrap yes')
            date_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD')
            datetime_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD HH:mm:SS')

            # write data rows
            for row_index, row in enumerate(rows_list):
                # write seq
                worksheet.write(row_index + 1, 0, row_index + 1, base_style)
                for cell_index, key in enumerate(keys):
                    cell_style = base_style
                    cell_value = row.get(key)
                    if isinstance(cell_value, basestring):
                        cell_value = re.sub("\r", " ", cell_value)
                    elif isinstance(cell_value, datetime.datetime):
                        cell_style = datetime_style
                    elif isinstance(cell_value, datetime.date):
                        cell_style = date_style
                    worksheet.write(row_index + 1, cell_index + 1, cell_value, cell_style)

        fp = StringIO()
        workbook.save(fp)
        fp.seek(0)
        data = fp.read()
        fp.close()
        return data