提交 b4e351d5 authored 作者: 伍姿英's avatar 伍姿英

Merge branch 'release/2.9.0'

......@@ -20,6 +20,7 @@
'wizard/batch_input_ship_package_statu_wizard.xml',
'wizard/update_bl_status_wizard.xml',
'wizard/export_bl_big_package_xlsx_wizard.xml',
'wizard/batch_update_transfer_bl_no_wizard_view.xml',
'wizard/associate_pallet_wizard_views.xml',
'wizard/add_exception_info_wizard_views.xml',
'wizard/email_template.xml',
......@@ -57,9 +58,11 @@
'ccs_base/static/css/base.scss',
],
'web.assets_backend': [
'ccs_base/static/src/mixins/*.js',
'ccs_base/static/src/views/*.js',
'ccs_base/static/src/views/*.xml',
'ccs_base/static/src/mixins/link_pallet.js',
'ccs_base/static/src/mixins/link_transfer_bl_no.js',
'ccs_base/static/src/views/big_package_list_controller.js',
'ccs_base/static/src/views/bl_list_controller.js',
'ccs_base/static/src/views/list.xml',
],
},
'license': 'AGPL-3',
......
......@@ -133,8 +133,8 @@ class ExportBlAndPackageXlsx(http.Controller):
return sheet1
# 运单导出包裹清关数据 每个运单导出一个文件 生成压缩包
@http.route(['/export/bl/package/xls/<string:arr>/<string:select_type>'], type='http', auth="public")
def export_bl_package_xls(self, arr, select_type):
@http.route(['/export/bl/package/xls/<string:arr>/<string:select_type>/<string:file_name_type>'], type='http', auth="public")
def export_bl_package_xls(self, arr, select_type, file_name_type):
"""
是否分大包,如果不分,就是提单一个文件,命名 提单号;如果分,一个提单一个大包一个文件,命名 提单号➕大包号
"""
......@@ -144,6 +144,7 @@ class ExportBlAndPackageXlsx(http.Controller):
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
if select_type == 'yes':
for bl_item in bl_obj:
order_no = bl_item.bl_no if file_name_type == 'bl_no' else bl_item.transfer_bl_no
for bag_item in bl_item.big_package_ids.filtered(lambda pack: not pack.is_cancel):
if len(bag_item.mapped('ship_package_ids').mapped('good_ids').filtered(
lambda good: not good.is_cancel)) > 0:
......@@ -152,9 +153,10 @@ class ExportBlAndPackageXlsx(http.Controller):
excel_file = io.BytesIO()
worksheet.save(excel_file)
excel_file.seek(0)
zip_file.writestr(f"{bl_item.bl_no}-{bag_item.big_package_no}.xls", excel_file.read()) # 压缩
zip_file.writestr(f"{order_no}-{bag_item.big_package_no}.xls", excel_file.read()) # 压缩
else:
for bl_item in bl_obj:
order_no = bl_item.bl_no if file_name_type == 'bl_no' else bl_item.transfer_bl_no
big_bag_obj = bl_item.big_package_ids.filtered(lambda pack: not pack.is_cancel)
if len(big_bag_obj.mapped('ship_package_ids').mapped('good_ids').filtered(
lambda good: not good.is_cancel)) > 0:
......@@ -163,7 +165,7 @@ class ExportBlAndPackageXlsx(http.Controller):
excel_file = io.BytesIO()
worksheet.save(excel_file)
excel_file.seek(0)
zip_file.writestr(f"{bl_item.bl_no}.xls", excel_file.read()) # 压缩
zip_file.writestr(f"{order_no}.xls", excel_file.read()) # 压缩
zip_buffer.seek(0)
headers = [
('Content-Type', 'application/x-zip-compressed'),
......@@ -172,19 +174,20 @@ class ExportBlAndPackageXlsx(http.Controller):
response = http.request.make_response(zip_buffer.getvalue(), headers=headers)
return response
@http.route(['/export/flight_png/xls/<string:arr>'], type='http', auth="public")
def export_flight_png_xls(self, arr):
@http.route(['/export/flight_png/xls/<string:arr>/<string:file_name_type>'], type='http', auth="public")
def export_flight_png_xls(self, arr, file_name_type):
arr = json.loads(arr)
flight_objs = http.request.env['cc.bl'].sudo().search([('id', 'in', arr)], order='id')
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for flight in flight_objs:
order_no = flight.bl_no if file_name_type == 'bl_no' else flight.transfer_bl_no
bl_attachment_objs = flight.bl_attachment_ids
for bl_attachment_obj in bl_attachment_objs:
# 获取图片数据
image_data = base64.b64decode(bl_attachment_obj.datas)
# 创建文件名,前缀为单据号
file_name = f"{flight.bl_no}.png"
file_name = f"{order_no}.png"
# 将图片添加到 ZIP 文件
zip_file.writestr(file_name, image_data)
zip_buffer.seek(0)
......
差异被折叠。
......@@ -16,6 +16,32 @@ class CommonCommon(models.Model):
_name = 'common.common'
_description = u'公用基础类'
def process_num(self, input_str):
"""
处理导入
:param input_str:
:return:
"""
if input_str:
return str(input_str).replace('\\', '-').replace('、、', '').replace('&',
' ').replace(
'>',
'').replace('<',
'').replace(
'?', '').replace('!', '')
return input_str
def process_str(self, input_str):
"""
处理导入的字符串
:param input_str:
:return:
"""
if input_str:
return self.process_num(str(input_str)).replace('.0', '').strip()
return input_str
# 去杠去空格转大写
def process_match_str(self, input_str):
......
......@@ -4,6 +4,7 @@ export_bl_big_package_xlsx_wizard_group_user,export_bl_big_package_xlsx_wizard_g
associate_pallet_wizard_group_user,associate_pallet_wizard_group_user,ccs_base.model_associate_pallet_wizard,base.group_user,1,1,1,1
add_exception_info_wizard_group_user,add_exception_info_wizard_group_user,ccs_base.model_add_exception_info_wizard,base.group_user,1,1,1,1
update_bl_status_wizard_group_user,update_bl_status_wizard_group_user,ccs_base.model_update_bl_status_wizard,base.group_user,1,1,1,1
batch_update_transfer_bl_no_wizard_group_user,batch_update_transfer_bl_no_wizard_group_user,ccs_base.model_batch_update_transfer_bl_no_wizard,base.group_user,1,1,1,1
access_group_user_common_common,access_group_user_common_common,model_common_common,base.group_user,1,1,1,1
......
......@@ -13,18 +13,8 @@ export const BigPackageLinkPallet = {
this.http = useService('http');
this.fileInput = useRef('fileInput');
this.root = useRef("root");
// this.isBigPackage = this.model.rootParams.resModel === "cc.big.package";
// this.is_link = this.user.hasGroup("ccs_base.group_clearance_of_customs_manager");
// console.log('ccs isBigPackage:' + this.isBigPackage)
// console.log('ccs is_link:' + this.is_link)
},
// displayLink() {
// console.log('ccs flag:' + this.isBigPackage && this.is_link)
// // 是大包的对象以及有清关清理的权限才显示按钮
// return this.isBigPackage && this.is_link;
// },
async onLinkPalletClick() {
// 点击按钮弹出关联托盘的向导
const records = this.model.root.selection;
......
/** @odoo-module */
import {useService} from '@web/core/utils/hooks';
const {useRef, useEffect, useState} = owl;
export const LinkTransferBlNo = {
setup() {
this._super();
this.actionService = useService('action');
this.notification = useService('notification');
this.orm = useService('orm');
this.http = useService('http');
this.fileInput = useRef('fileInput');
this.root = useRef("root");
},
async onTransferBlNoClick() {
// 点击按钮弹出批量关联转单号的向导
const records = this.model.root.selection;
const recordIds = records.map((a) => a.resId);
const action = await this.orm.call('cc.bl', 'action_link_transfer_bl_no', [recordIds]);
this.actionService.doAction(action);
},
};
/** @odoo-module */
import {BigPackageLinkPallet} from '../mixins/link_pallet';
import {registry} from '@web/core/registry';
import {patch} from '@web/core/utils/patch';
import {useService} from '@web/core/utils/hooks';
import {listView} from "@web/views/list/list_view";
import {ListController} from "@web/views/list/list_controller";
const {onWillStart} = owl;
export class BigPackageListController extends ListController {
setup() {
super.setup();
console.log('----------引用成功')
this.orm = useService('orm');
this.actionService = useService('action');
this.rpc = useService("rpc");
this.user = useService("user");
this.isBigPackage = this.model.rootParams.resModel === "cc.big.package";
console.log('ccs isBigPackage:' + this.isBigPackage)
onWillStart(async () => {
this.is_link = await this.user.hasGroup("ccs_base.group_clearance_of_customs_manager");
console.log('ccs is_link:' + this.is_link)
});
}
displayLink() {
console.log('ccs flag:' + this.isBigPackage && this.is_link)
// 是大包的对象以及有清关清理的权限才显示按钮
return this.isBigPackage && this.is_link;
}
displayTransferBlNo() {
// 大包页面永远不显示“关联转单号”按钮
return false;
}
}
patch(BigPackageListController.prototype, 'big_package_list_controller_link_pallet', BigPackageLinkPallet);
registry.category('views').add('cc_big_package_tree', {
...listView,
buttonTemplate: 'ccs_base.ListButtons',
Controller: BigPackageListController
});
\ No newline at end of file
/** @odoo-module */
import {LinkTransferBlNo} from '../mixins/link_transfer_bl_no';
import {registry} from '@web/core/registry';
import {patch} from '@web/core/utils/patch';
import {useService} from '@web/core/utils/hooks';
import {listView} from "@web/views/list/list_view";
import {ListController} from "@web/views/list/list_controller";
const {onWillStart} = owl;
export class BlListController extends ListController {
setup() {
super.setup();
console.log('----------引用成功')
this.orm = useService('orm');
this.actionService = useService('action');
this.rpc = useService("rpc");
this.user = useService("user");
this.isBl = this.model.rootParams.resModel === "cc.bl";
console.log('ccs isBl:' + this.isBl)
onWillStart(async () => {
this.can_link_transfer_bl_no = await this.user.hasGroup("ccs_base.group_clearance_of_customs_user");
console.log('ccs can_link_transfer_bl_no:' + this.can_link_transfer_bl_no)
});
}
displayLink() {
// 提单页面永远不显示“关联托盘”按钮
return false;
}
displayTransferBlNo() {
console.log('ccs flag:' + this.isBl && this.can_link_transfer_bl_no)
// 是提单的对象以及有清关用户的权限才显示按钮
return this.isBl && this.can_link_transfer_bl_no;
}
}
patch(BlListController.prototype, 'link_transfer_bl_no', LinkTransferBlNo);
registry.category('views').add('cc_bl_tree', {
...listView,
buttonTemplate: 'ccs_base.ListButtons',
Controller: BlListController
});
\ No newline at end of file
/** @odoo-module */
import {LinkTransferBlNo} from '../mixins/link_transfer_bl_no';
import {BigPackageLinkPallet} from '../mixins/link_pallet';
import {registry} from '@web/core/registry';
......@@ -10,7 +11,7 @@ import {ListController} from "@web/views/list/list_controller";
const {onWillStart} = owl;
export class BigPackageListController extends ListController {
export class BlListController extends ListController {
setup() {
super.setup();
console.log('----------引用成功')
......@@ -18,14 +19,24 @@ export class BigPackageListController extends ListController {
this.actionService = useService('action');
this.rpc = useService("rpc");
this.user = useService("user");
this.isBl = this.model.rootParams.resModel === "cc.bl";
this.isBigPackage = this.model.rootParams.resModel === "cc.big.package";
console.log('ccs isBigPackage:' + this.isBigPackage)
console.log('ccs isBl:' + this.isBl)
onWillStart(async () => {
this.can_link_transfer_bl_no = await this.user.hasGroup("ccs_base.group_clearance_of_customs_user");
this.is_link = await this.user.hasGroup("ccs_base.group_clearance_of_customs_manager");
console.log('ccs can_link_transfer_bl_no:' + this.can_link_transfer_bl_no)
console.log('ccs is_link:' + this.is_link)
});
}
displayTransferBlNo() {
console.log('ccs flag:' + this.isBl && this.can_link_transfer_bl_no)
// 是提单的对象以及有清关用户的权限才显示按钮
return this.isBl && this.can_link_transfer_bl_no;
}
displayLink() {
console.log('ccs flag:' + this.isBigPackage && this.is_link)
// 是大包的对象以及有清关清理的权限才显示按钮
......@@ -34,11 +45,19 @@ export class BigPackageListController extends ListController {
}
patch(BlListController.prototype, 'link_transfer_bl_no', LinkTransferBlNo);
registry.category('views').add('cc_bl_tree', {
...listView,
buttonTemplate: 'ccs_base.ListButtons',
Controller: BlListController
});
patch(BigPackageListController.prototype, 'big_package_list_controller_link_pallet', BigPackageLinkPallet);
registry.category('views').add('cc_big_package_tree', {
...listView,
buttonTemplate: 'ccs_base.ListButtons',
Controller: BigPackageListController
});
});
\ No newline at end of file
......@@ -7,5 +7,10 @@
Link Pallet
</button>
</xpath>
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
<button t-if="displayTransferBlNo()" type="button" class="d-none d-md-inline o_button_transfer_bl_no btn btn-primary mx-1" t-on-click.prevent="onTransferBlNoClick">
Link Transfer B/L No
</button>
</xpath>
</t>
</templates>
......@@ -7,12 +7,13 @@
<field name="name">tree.cc.bl</field>
<field name="model">cc.bl</field>
<field name="arch" type="xml">
<tree string="Bill of Loading" decoration-warning="is_cancel==True">
<tree string="Bill of Loading" decoration-warning="is_cancel==True" js_class="cc_bl_tree">
<field optional="show" name="state" string="Status" widget="badge" decoration-info="state=='draft'"
decoration-primary="state=='ccing'" decoration-success="state=='done'"/>
<field optional="show" name="customs_clearance_status" string="Customs Clearance Status"/>
<field optional="show" name="bl_no" string="Bill of Loading No."/>
<field optional="show" name="bl_date" string="B/L Date"/>
<field optional="hide" name="transfer_bl_no" string="Transfer Bill of Loading No."/>
<field optional="show" name="customer_id" string="Customer"/>
<field optional="show" name="customs_bl_no" string="Customs Bill of Loading No."/>
<field optional="hide" name="big_package_sell_country" string="Sell Country"/>
......@@ -120,6 +121,7 @@
<group>
<group>
<field name="bl_date" string="B/L Date"/>
<field name="transfer_bl_no"/>
<field name="customer_id" string="Customer"/>
<field name="customs_bl_no" string="Customs Bill of Loading No."/>
<field name="big_package_sell_country" string="Sell Country"/>
......@@ -163,7 +165,7 @@
<!-- </page>-->
<page string="Attachments">
<group>
<field name="bl_attachment_ids" string="B/L Attachments" widget="many2many_binary"
<field name="bl_attachment_ids" string="B/L Attachments" widget="many2many_attachment_preview"
readonly="1"/>
<field name="cc_attachment_ids" string="CC Attachments"/>
</group>
......@@ -405,7 +407,7 @@
<field name="state">code</field>
<field name="code">
if records:
action = records.export_bl_attachment_png()
action = records.action_export_bl_attachment_png()
</field>
</record>
......
......@@ -146,7 +146,7 @@
</page>
<page string="Invoice Attachments">
<field name="invoice_attachment_ids" string="Invoice Attachments"
widget="many2many_binary"/>
widget="many2many_attachment_preview"/>
</page>
<page string="Sender Info">
<group>
......
......@@ -149,7 +149,7 @@
</page>
<page string="Invoice Attachments">
<field name="invoice_attachment_ids" string="Invoice Attachments"
widget="many2many_binary"/>
widget="many2many_attachment_preview"/>
</page>
<page string="Sender Info">
<group>
......
......@@ -5,4 +5,5 @@ from . import export_bl_big_package_xlsx_wizard
from . import associate_pallet_wizard
from . import add_exception_info_wizard
from . import update_bl_status_wizard
from . import batch_update_transfer_bl_no_wizard
import base64
import io
from odoo import models, fields, _
from odoo.exceptions import ValidationError
try:
import xlrd
import xlwt
except ImportError:
xlrd = None
xlwt = None
class BatchUpdateTransferBlNoWizard(models.TransientModel):
_name = 'batch.update.transfer.bl.no.wizard'
_description = 'Batch Update Transfer B/L No Wizard'
report_file_ids = fields.Many2many('ir.attachment', 'batch_update_transfer_bl_no_attachment_rel',
string='B/L List') # 提单清单
error_file_ids = fields.Many2many('ir.attachment', 'batch_update_transfer_bl_no_error_attachment_rel', 'wizard_id',
'error_file_id',
string='Error Data') # 异常数据
file_name = fields.Char('File Name') # 文件名称
def get_file_path(self, report_file_ids):
"""
Get the content of the excel file
:return:
"""
if report_file_ids.name[-3:] == 'xls' or report_file_ids.name[-4:] == 'xlsx':
data = False
report_path = report_file_ids._full_path(report_file_ids.store_fname)
if report_file_ids.name[-3:] == 'xls':
data = xlrd.open_workbook(report_path, formatting_info=True)
elif report_file_ids.name[-4:] == 'xlsx':
data = xlrd.open_workbook(report_path)
return data
else:
raise ValidationError(_('Only excel files can be uploaded!')) # 只能上传excel文件!
def check_import_template(self, report_file_ids):
"""
Check if the template header is correct
:return:
"""
if self.report_file_ids:
try:
data = self.get_file_path(report_file_ids[0])
first_table = data.sheets()[0]
excel_header = first_table.row_values(0)
except Exception as e:
raise ValidationError(
_('Please check if the import file and content are correct, and import according to the template file!')) # 请检查导入文件和内容是否正确,请根据模板文件导入!
header = [_('B/L No'), _('Transfer B/L No')]
str_header = ','.join(header)
if not excel_header == header:
raise ValidationError(_('File error, the content order should be: %s') % str_header) # 文件错误,内容顺序为:%s
def init_importfile(self):
"""
Initialize the imported file and convert it into a unified array format
:return:
"""
common_obj = self.env['common.common'].sudo()
data = self.get_file_path(self.report_file_ids[0])
order_list = []
first_table = data.sheets()[0]
for i in range(1, first_table.nrows):
line = first_table.row_values(i)
bl_no = common_obj.process_str(line[0]) # 提单号
transfer_bl_no = common_obj.process_str(line[1]) # 转单号
order_list.append({
'bl_no': bl_no, # 提单号
'bl_id': False, # 提单对象
'transfer_bl_no': transfer_bl_no, # 转单号
'error_remark': '' # 错误信息
})
return order_list
def check_list_import(self, order_list):
"""
Check normal and abnormal data
:param order_list:
:return:
"""
error_list = [] # 异常数据
pass_list = [] # 正常数据
# Collect all B/L No and Transfer B/L No
all_bl_no = set(item['bl_no'] for item in order_list if item.get('bl_no'))
all_transfer_bl_no = set(item['transfer_bl_no'] for item in order_list if item.get('transfer_bl_no'))
is_error = False
for order_item in order_list:
cause_arr = []
bl_no = self.env['common.common'].sudo().process_match_str(order_item.get('bl_no'))
transfer_bl_no = self.env['common.common'].sudo().process_match_str(order_item.get('transfer_bl_no'))
if not bl_no:
cause_arr.append(_('B/L No is required')) # 提单号必填
else:
bl_obj = self.env['cc.bl'].sudo().deal_bl_no(bl_no)
if bl_obj:
order_item['bl_id'] = bl_obj.id
else:
cause_arr.append(_('This B/L No does not exist in the system')) # 该提单号在系统不存在
if not transfer_bl_no:
cause_arr.append(_('Transfer B/L No is required')) # 转单号必填
else:
if bl_no == transfer_bl_no:
cause_arr.append(_('Transfer B/L No. cannot be the same as B/L No.')) # 转单号不能与提单号重复
# Check if Transfer B/L No already exists in the system
bl_obj = self.env['cc.bl'].sudo().deal_bl_no_and_transfer_bl_no(transfer_bl_no)
if bl_obj and self.env['common.common'].sudo().process_match_str(bl_obj.bl_no) != bl_no:
cause_arr.append(
_('Transfer B/L No. cannot be the same as B/L No. or Transfer B/L No.')) # 转单号不能与提单号或转单号重复
# Check if any B/L No equals any Transfer B/L No, or vice versa
if (bl_no and bl_no in all_transfer_bl_no) or (transfer_bl_no and transfer_bl_no in all_bl_no):
cause_arr.append(_('B/L No and Transfer B/L No are duplicated')) # 提单号和转单号有重复
if len(cause_arr) > 0:
is_error = True
order_item['error_remark'] = ','.join(cause_arr)
error_list.append(order_item)
else:
pass_list.append(order_item)
error_list.append(order_item)
return pass_list, error_list, is_error
def error_export(self, error_msg):
"""
Export error records
:return:
"""
# Generate excel file for error data
wb = xlwt.Workbook(encoding='utf-8')
# Initialize style
style = xlwt.XFStyle()
style.num_format_str = 'yyyy/mm/dd'
# Create font for style
font = xlwt.Font()
font.name = 'Times New Roman'
font.bold = True
style.font = font
ws = wb.add_sheet('sheet1', cell_overwrite_ok=True)
header = [_('B/L No'), _('Transfer B/L No'), _('Error Reason')] # ['提单号', '转单号', '异常原因']
col_index = 0
for title in header:
ws.write(0, col_index, title, style)
ws.col(len(header) - col_index - 1).width = 256 * 20 # around 256 pixels
col_index += 1
index = 0
for item in error_msg:
ws.write(1 + index, 0, item['bl_no'])
ws.write(1 + index, 1, item['transfer_bl_no'])
ws.write(1 + index, 2, item['error_remark'])
index += 1
buf = io.BytesIO()
wb.save(buf)
buf.seek(0)
data = base64.encodebytes(buf.read())
buf.close()
self.file_name = '%s.%s' % (_('Error Data'), "xls")
# 将base64编码的字符串保存到Odoo的binary字段
attachment = self.env['ir.attachment'].sudo().create({
'datas': data,
'type': 'binary',
'name': self.file_name,
'store_fname': self.file_name,
'res_model': self._name, # 用 wizard 的模型名
'res_id': self.id, # 关联到当前 wizard 记录
})
self.error_file_ids = [(6, 0, attachment.ids)]
return {
'type': 'ir.actions.act_window',
'name': _('Error Data'),
'res_model': self._name,
'view_mode': 'form',
'res_id': self.id,
'context': {'default_error_file_ids': [(6, 0, attachment.ids)],
'default_report_file_ids': [(6, 0, self.report_file_ids.ids)]},
'target': 'new',
}
def submit(self):
report_file_ids = self.report_file_ids
if report_file_ids and len(report_file_ids) > 0:
if len(report_file_ids) > 1:
raise ValidationError(u'Only one template file can be uploaded at a time!') # 一次只能上传一个模板文件!
self.check_import_template(report_file_ids) # Check template header
# Convert to array
order_list = self.init_importfile()
# Check normal and error data
pass_list, error_list, is_error = self.check_list_import(order_list)
# Export error data
if len(error_list) > 0 and is_error:
return self.error_export(error_list)
if len(pass_list) > 0 and not is_error:
for item in pass_list:
item.pop('error_remark') if item.get('error_remark') or item.get(
'error_remark') == '' else False
if item.get('bl_id'):
self.env['cc.bl'].sudo().browse(item['bl_id']).write({'transfer_bl_no': item['transfer_bl_no']})
else:
raise ValidationError(
_('Please upload the B/L data file to be imported first!')) # 请先上传需要更新的提单数据文件!
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Batch Link Transfer B/L No Wizard View -->
<record id="view_batch_update_transfer_bl_no_wizard" model="ir.ui.view">
<field name="name">batch.update.transfer.bl.no.wizard.form</field>
<field name="model">batch.update.transfer.bl.no.wizard</field>
<field name="arch" type="xml">
<form string="Batch Link Transfer B/L No"> <!-- 批量关联转单号 -->
<group>
<field name="report_file_ids" widget="many2many_attachment_preview"/>
<label for="error_file_ids" attrs="{'invisible': [('error_file_ids', '=', [])]}"/>
<div attrs="{'invisible': [('error_file_ids', '=', [])]}">
<field name="error_file_ids" widget="many2many_attachment_preview" readonly="1"
attrs="{'invisible': [('error_file_ids', '=', [])]}"/>
<br/>
<!-- 有异常数据时,请下载异常数据文件,根据提示处理好数据,再导入! -->
<span class="label label-warning">
If there is abnormal data, please download the error file, fix the data as prompted, and re-import!
</span>
</div>
</group>
<group>
<!-- 模板下载 -->
<a href="/ccs_base/static/template/transfer_bl_no_template.xlsx?v=20250715001">
Download Template
</a>
<!-- 提示:请务必按照模板填写信息,否则系统无法识别。 -->
<span style="color:red;font-size:15px;">Tip: Please fill in the information strictly according to the template, otherwise the system will not recognize it.
</span>
</group>
<footer>
<button name="submit" type="object"
string="Import" class="oe_highlight"/> <!-- 导入 -->
<button special="cancel" string="Close"/> <!-- 关闭 -->
</footer>
</form>
</field>
</record>
</data>
</odoo>
\ No newline at end of file
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api, fields
from odoo.exceptions import Warning, ValidationError
from odoo import models, fields
class ExportBlBigPackageXlsxWizard(models.TransientModel):
......@@ -20,7 +19,12 @@ class ExportBlBigPackageXlsxWizard(models.TransientModel):
return self.env['cc.bl'].sudo().browse(order_id)
# 增加选择类型字段, 是否分大包导出
select_type = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Whether to export in Big packages')
select_type = fields.Selection([('yes', 'YES'), ('no', 'NO')], string='Whether to export in Big packages',
default='no')
# 增加文件命名类型字段。导出的表格命名,可勾选。默认为提单号命名(现在就是按提单号命名),若勾选了转单号,则按转单号命名
file_name_type = fields.Selection([('bl_no', 'B/L No'), ('transfer_bl_no', 'Transfer B/L No')],
string='File Name Type', default='bl_no')
action_type = fields.Char(string='Action Type', default='报关文件')
def submit(self):
"""
......@@ -28,8 +32,15 @@ class ExportBlBigPackageXlsxWizard(models.TransientModel):
"""
order_obj = self.get_order()
arr = [item.id for item in order_obj]
return {
'type': 'ir.actions.act_url',
'url': '/export/bl/package/xls/%s/%s' % (arr, self.select_type),
'target': 'new',
}
if self.action_type == '报关文件':
return {
'type': 'ir.actions.act_url',
'url': '/export/bl/package/xls/%s/%s/%s' % (arr, self.select_type, self.file_name_type),
'target': 'new',
}
else:
return {
'type': 'ir.actions.act_url',
'url': '/export/flight_png/xls/%s/%s' % (arr, self.file_name_type),
'target': 'new',
}
......@@ -12,8 +12,10 @@
<form string="导出报关文件">
<sheet>
<group>
<field name="select_type" required="1"/>
<field name="select_type"
attrs="{'invisible': [('action_type', '!=', '报关文件')], 'required': [('action_type', '=', '报关文件')]}"/>
<field name="file_name_type" required="1"/>
<field name="action_type" invisible="1"/>
</group>
<footer>
<button name="submit" type="object" string="Submit" class="oe_highlight"/>
......
......@@ -102,8 +102,8 @@ class OrderController(http.Controller):
bl_no = kwargs['bl_no']
# bl_obj = request.env['cc.bl'].sudo().search([('bl_no', '=', bl_no)]) # 提单
state_arr = ['draft', 'ccing']
bl_obj = request.env['cc.bl'].sudo().deal_bl_no(
bl_no) # 提单号去掉杠和空格,并转换为小写
bl_obj = request.env['cc.bl'].sudo().deal_bl_no_and_transfer_bl_no(
bl_no) # 提单号去掉杠和空格,并转换为小写,优先匹配提单号,匹配不到则匹配转单号
if bl_obj:
if bl_obj.state in state_arr:
res['bl_info'] = bl_obj.search_bl_info(
......@@ -149,8 +149,8 @@ class OrderController(http.Controller):
kwargs.get('big_package_arr') or kwargs.get('ship_package_arr') or kwargs.get('pallet_arr')):
bl_no = kwargs['bl_no']
state_arr = ['draft', 'ccing']
bl_obj = request.env['cc.bl'].sudo().deal_bl_no(
bl_no) # 提单号去掉杠和空格,并转换为小写
bl_obj = request.env['cc.bl'].sudo().deal_bl_no_and_transfer_bl_no(
bl_no) # 提单号去掉杠和空格,并转换为小写,优先匹配提单号,匹配不到则匹配转单号
if bl_obj and bl_obj.state in state_arr:
ship_packages = []
big_package_exception_arr = {}
......
......@@ -799,20 +799,6 @@ class CcBl(models.Model):
return [{'pallet_number': k, 'pallet_usage_time': v} for k, v in pallet_info.items()]
def deal_bl_no(self, bl_no, state_arr=[]):
"""
处理提单号:去掉杠和空格,并转换为小写
:param bl_no:
:return:
"""
processed_bl_no = bl_no.replace('-', '').replace(' ', '').lower()
# 查询所有提单并处理它们的 bl_no
domain = [('state', 'in', state_arr)] if state_arr else []
all_bl_obj = self.env['cc.bl'].sudo().search(domain)
bl_obj = all_bl_obj.filtered(
lambda r: r.bl_no.replace('-', '').replace(' ', '').lower() == processed_bl_no) # 提单
return bl_obj
def try_callback_track(self, max_retries=3, ship_package_ids=[], user_obj=False):
""" 封装的重试逻辑 """
for i in range(max_retries):
......
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
Many2Many Attachment Preview
============================
This Module will help to preview the attachments in Many2Many fields.
Configuration
=============
* No additional configurations needed
Company
-------
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__
License
=======
GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3)
(https://www.gnu.org/licenses/agpl-3.0-standalone.html)
Credits
-------
* Developer: (V16) Ajith V,
(V15) Shonima, Contact: odoo@cybrosys.com
Contacts
--------
* Mail Contact : odoo@cybrosys.com
* Website : https://cybrosys.com
Bug Tracker
-----------
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your
issue has already been reported.
Maintainer
==========
.. image:: https://cybrosys.com/images/logo.png
:target: https://cybrosys.com
This module is maintained by Cybrosys Technologies.
For support and more information, please visit `Our Website <https://cybrosys.com/>`__
Further information
===================
HTML Description: `<static/description/index.html>`__
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Ajith(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from . import models
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Ajith(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
{
'name': "Many2Many Attachment Preview",
'version': '16.0.1.0.0',
'summary': """Preview the attachment in Many2Many field""",
'description': """"This module helps you to preview the attachments in
Many2Many fields.""",
'category': 'Uncategorized',
'author': 'Cybrosys Techno Solutions',
'company': 'Cybrosys Techno Solutions',
'maintainer': 'Cybrosys Techno Solutions',
'website': "http://www.cybrosys.com",
'depends': ['base', 'sale_management'],
'data': ['views/sale_order_views.xml'],
'assets': {
'web.assets_backend': [
'many2many_attachment_preview/static/src/js/attachment_preview.js',
'many2many_attachment_preview/static/src/xml/attachment_preview.xml',
'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.6/dist/jquery.fancybox.min.css',
'https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.6/dist/jquery.fancybox.min.js'
],
},
'license': 'AGPL-3',
'images': ['static/description/banner.png'],
'installable': True,
'auto_install': False,
'application': False,
}
## Module <many2many_attachment_preview>
#### 29.08.2024
#### Version 16.0.1.0.0
#### ADD
- Initial Commit for Many2Many Attachment Preview
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Ajith(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from . import sale_order
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>).
# Author: Ajith(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU AFFERO
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class SaleOrder(models.Model):
"""Inherited sale. order class to add a new field for attachments"""
_inherit = 'sale.order'
attachment_ids = fields.Many2many(
comodel_name='ir.attachment',
string="Attachments", help="Add Multiple attachment file")
/** @odoo-module **/
import {registry} from "@web/core/registry";
import {Component, useState} from "@odoo/owl";
import {FileInput} from "@web/core/file_input/file_input";
import {standardFieldProps} from "@web/views/fields/standard_field_props";
import {useService} from "@web/core/utils/hooks";
import {useX2ManyCrud} from "@web/views/fields/relational_utils";
/**
* The Many2ManyAttachmentPreview component is designed to manage the preview and handling
* of many2many fields that contain file attachments. It allows users to upload, preview,
* and remove files associated with a record.
*
* @class
* @extends Component
*
* @prop {Object} props - The props object includes all the standard field properties,
* as well as additional properties such as acceptedFileExtensions,
* className, and numberOfFiles.
* @prop {String} props.acceptedFileExtensions - (Optional) A string defining the accepted file
* extensions for uploads.
* @prop {String} props.className - (Optional) A string defining any additional CSS classes
* to be applied.
* @prop {Number} props.numberOfFiles - (Optional) A number representing the maximum number of
* files allowed.
*
* @setup
* @method setup - Initializes services and state for the component, including ORM service,
* notification service, and operations for managing many2many CRUD.
*
* @state {Object} state - Contains component state, including a flag for internal logic handling.
*
* @getter uploadText - Retrieves the label for the upload button, typically the field's label.
*
* @getter files - Returns a list of files associated with the record, including their IDs
* and other relevant data.
*
* @method getUrl(id) - Constructs the URL to access a file attachment by its ID.
* @param {Number} id - The ID of the file attachment.
* @returns {String} - The URL for the file attachment.
*
* @method getExtension(file) - Extracts the file extension from a file's name.
* @param {Object} file - The file object.
* @returns {String} - The file extension.
*
* @method onFileUploaded(files) - Handles the logic after files are uploaded,
* including error handling and saving the records.
* @param {Array} files - An array of uploaded files.
* @returns {Promise<void>}
*
* @method onFileRemove(deleteId) - Handles the logic for removing a file by its ID,
* including updating the records.
* @param {Number} deleteId - The ID of the file to be removed.
* @returns {Promise<void>}
*
* @supportedTypes - Specifies that this component supports "many2many" fields.
*
* @fieldsToFetch - Defines the fields to be fetched from the related records, such as
* 'name' and 'mimetype'.
*
* @registry.category("fields").add("many2many_attachment_preview", Many2ManyAttachmentPreview)
* - Registers the component in the Odoo registry.
*/
export class Many2ManyAttachmentPreview extends Component {
static template = 'many2many_attachment_preview.Many2ManyImageField'
static components = {
FileInput,
};
static props = {
...standardFieldProps,
acceptedFileExtensions: {type: String, optional: true},
className: {type: String, optional: true},
numberOfFiles: {type: Number, optional: true},
};
setup() {
this.orm = useService("orm");
this.notification = useService("notification");
this.operations = useX2ManyCrud(() => this.props.value, true);
this.state = useState({
flag: false,
});
}
get uploadText() {
return this.props.record.fields[this.props.name].string;
}
get files() {
return this.props.record.data[this.props.name].records.map((record) => {
return {
...record.data,
id: record.resId,
};
});
}
getUrl(id) {
return "/web/content/ir.attachment/" + id + "/datas";
}
getExtension(file) {
return file.name.replace(/^.*\./, "");
}
async onFileUploaded(files) {
for (const file of files) {
if (file.error) {
return this.notification.add(file.error, {
title: this.env._t("Uploading error"),
type: "danger",
});
}
await this.operations.saveRecord([file.id]);
}
}
async onFileRemove(deleteId) {
const record = this.props.value.records.find((record) => record.data.id === deleteId);
this.operations.removeRecord(record);
}
}
Many2ManyAttachmentPreview.supportedTypes = ["many2many"];
Many2ManyAttachmentPreview.fieldsToFetch = {
name: {
type: 'char'
},
mimetype: {
type: 'char'
},
}
registry.category("fields").add("many2many_attachment_preview", Many2ManyAttachmentPreview)
<template>
<!--
This template defines a Many2ManyImageField component used to display a list of image attachments
with an option to upload additional files. The component utilizes the Odoo Owl framework.
Structure:
- The outermost `div` has a dynamic class that includes `oe_fileupload` and optionally
a custom class if provided in `props.className`.
- Inside this `div`, there's a container `div` with the class `o_attachments o_attachments_widget`
which holds the list of attached files.
- The files are iterated over using `t-foreach`, where each `file` is rendered using the
`many2many_attachment_preview.image_preview` template.
- If the `readonly` property is not set, an upload section is rendered, allowing the user to
attach more files. The upload button triggers the `FileInput` component with the following attributes:
- `acceptedFileExtensions`: Defines the types of files allowed for upload.
- `multiUpload`: Enables multiple file uploads at once.
- `onUpload.bind`: Binds the `onFileUploaded` method for handling the upload process.
- `resModel`: Specifies the model associated with the uploaded files.
- `resId`: Specifies the record ID. If no ID is provided, it defaults to 0.
- The upload button has a tooltip "Attach" and displays a paperclip icon with a label and upload text.
-->
<t t-name="many2many_attachment_preview.Many2ManyImageField" owl="1">
<div t-attf-class="oe_fileupload {{ props.className ? props.className : ''}}"
aria-atomic="true">
<div class="o_attachments o_attachments_widget">
<t t-foreach="files" t-as="file" t-key="file_index">
<t t-call="many2many_attachment_preview.image_preview"/>
</t>
</div>
<div t-if="!props.readonly" class="oe_add">
<FileInput
acceptedFileExtensions="props.acceptedFileExtensions"
multiUpload="true"
onUpload.bind="onFileUploaded"
resModel="props.record.resModel"
resId="props.record.data.id or 0">
<button class="btn btn-secondary o_attach o_attach_wiget"
data-tooltip="Attach">
<span class="fa fa-paperclip" aria-label="Attach"/>
<t t-esc="uploadText"/>
</button>
</FileInput>
</div>
</div>
</t>
<!--
This template defines the `image_preview` component used to display individual file attachments
in a many-to-many relationship field within Odoo. It handles various file types such as images,
PDFs, and videos, and provides functionalities for viewing, downloading, and deleting files.
Structure:
- A variable `editable` is set based on the `readonly` property from `props`, determining whether the file is editable.
- The outer `div` uses dynamic classes based on the `editable` state and upload status to provide appropriate styling.
- The inner `div` wraps the file attachment and displays the content based on the file extension:
- For image files (`png`, `jpg`, `jpeg`), an image preview is displayed. The image is wrapped in a link that uses Fancybox to allow zooming.
- For PDF files, the link is set to open in an iframe using Fancybox.
- For video files (`mkv`), the link is configured to play the video in an HTML5 video player using Fancybox.
- The `caption` section displays the file name and extension. Clicking the file name triggers a download.
- If the file is editable, a progress bar is shown during the upload process, and a delete button is provided to remove the file.
- Once the file is uploaded, a checkmark icon indicates successful upload.
- The delete button allows the user to remove the file, triggering the `onFileRemove` function with the file's ID.
-->
<t t-name="many2many_attachment_preview.image_preview" owl="1">
<t t-set="editable" t-value="!props.readonly"/>
<div t-attf-class="o_attachment_widget o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}"
t-att-title="file.name">
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}"
t-att-title="file.name">
<div class="o_attachment_wrap">
<t t-set="ext" t-value="getExtension(file)"/>
<t t-if="ext=='png' or ext=='jpg' or ext=='jpeg'">
<div class="o_image_box float-start"
t-att-data-tooltip="'Download ' + file.name">
<a t-att-href="getUrl(file.id)"
aria-label="Download"
style="cursor: zoom-in;" data-fancybox="gallery"
data-options="Toolbar">
<span class="o_image o_hover"
t-att-data-mimetype="file.mimetype"
t-att-data-ext="ext" role="img"
t-attf-data-src="/web/content/{{file.id}}"/>
</a>
</div>
</t>
<t t-if="ext=='pdf'">
<div class="o_image_box float-start"
t-att-data-tooltip="'Download ' + file.name">
<a t-att-href="getUrl(file.id)"
aria-label="Download"
style="cursor: zoom-in;" data-fancybox=""
data-type="iframe">
<span class="o_image o_hover"
t-att-data-mimetype="file.mimetype"
t-att-data-ext="ext" role="img"
t-attf-data-src="/web/content/{{file.id}}"/>
</a>
</div>
</t>
<t t-if="ext=='mkv'">
<div class="o_image_box float-start"
t-att-data-tooltip="'Download ' + file.name">
<a t-att-href="getUrl(file.id)"
aria-label="Download"
style="cursor: zoom-in;" data-fancybox=""
data-type="html5video" data-width="640"
data-height="360">
<span class="o_image o_hover"
t-att-data-mimetype="file.mimetype"
t-att-data-ext="ext" role="img"
t-attf-data-src="/web/content/{{file.id}}"/>
</a>
</div>
</t>
<div class="caption">
<a class="ml4"
t-att-data-tooltip="'Download ' + file.name"
t-att-href="getUrl(file.id)">
<t t-esc='file.name'/>
</a>
</div>
<div class="caption small">
<a class="ml4 small text-uppercase"
t-att-href="getUrl(file.id)">
<b>
<t t-esc='ext'/>
</b>
</a>
<div t-if="editable"
class="progress o_attachment_progress_bar">
<div class="progress-bar progress-bar-striped active"
style="width: 100%">Uploading
</div>
</div>
</div>
<div class="o_attachment_uploaded">
<i class="text-success fa fa-check" role="img"
aria-label="Uploaded" title="Uploaded"/>
</div>
<div t-if="editable" class="o_attachment_delete"
t-on-click.stop="() => this.onFileRemove(file.id)">
<span class="text-white" role="img" aria-label="Delete"
title="Delete">×
</span>
</div>
</div>
</div>
</div>
</t>
</template>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
This XML file inherits the sale order form view to add a new field,
`attachment_ids`, which uses the `many2many_attachment_preview` widget.
The new field is placed right after the `payment_term_id` field.
This customization allows users to preview attachments directly in the
sale order form.
-->
<record id="view_order_form" model="ir.ui.view">
<field name="name">
sale.order.view.form.inherit.many2many.attachment.preview
</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<field name="payment_term_id" position="after">
<field name="attachment_ids" widget="many2many_attachment_preview"/>
</field>
</field>
</record>
</odoo>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论