Merge branch 'master' into dev_zhou

# Conflicts:
#	yuthon_will_task/models/yuthon_will_task.py
This commit is contained in:
李鹏宇
2026-04-29 10:31:41 +08:00
33 changed files with 1016 additions and 65 deletions
+1 -1
View File
@@ -18,6 +18,6 @@ def exception_handle(func):
return json.dumps({'data': '', 'code': 500, 'message': str(e)})
except Exception as e:
_logger.exception(e)
return json.dumps({'data': '', 'code': 500, 'message': str(e)})
return json.dumps({'data': '', 'code': 500, 'message': f'Server Error'})
return wrapper
+23
View File
@@ -128,3 +128,26 @@ class RestaurantController(Controller):
}
return json.dumps({'data': catering_data, 'code': 200, 'message': ''})
return json.dumps({'data': '', 'code': 200, 'message': '没有查询到数据'})
@route('/phone/restaurant/get_catering_statistics', methods=['POST'], type='http', auth="none", csrf=False, cors='*')
@exception_handle
def get_catering_statistics(self, **kwargs):
menu_date = kwargs.get('menu_date', date.today())
if not menu_date:
return json.dumps({'data': '', 'code': 500, 'message': '参数错误'})
caterings = request.env['yuthon.catering'].sudo().search([
('date', '=', menu_date),
])
catering_data = {
'breakfast': 0,
'lunch': 0,
}
if caterings:
catering_data = {
'breakfast': len(caterings.filtered(lambda x: x.breakfast)),
'lunch': len(caterings.filtered(lambda x: x.lunch)),
}
return json.dumps({'data': catering_data, 'code': 200, 'message': ''})
return json.dumps({'data': catering_data, 'code': 200, 'message': ''})
+9 -1
View File
@@ -13,7 +13,7 @@ class IssueLibrary(models.Model):
_rec_name = 'name'
name = fields.Char(string='议题名称')
name_text = fields.Text(string='议题名称', compute='_compute_name', readonly=False)
name_text = fields.Text(string='议题名称', compute='_compute_name', readonly=False, store=True)
depart_opinions = fields.Text(string='部门意见')
topic_content = fields.Text(string='事项背景')
will_based = fields.Text(string='上会依据')
@@ -840,6 +840,14 @@ class IssueLibrary(models.Model):
for record in self:
record.name_text = record.name
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
args = args or []
domain = []
if name:
domain = ['|', '|', ('name', operator, name), ('code', operator, name), ('name_text', operator, name)]
return self.search(domain + args, limit=limit).name_get()
@api.onchange('issue_library_room_line_ids')
def _onchange_issue_library_room_line_ids(self):
for record in self.issue_library_room_line_ids:
@@ -331,6 +331,8 @@
<field name="arch" type="xml">
<search string="议题">
<field name="name"/>
<field name="code"/>
<field name="name_text"/>
<filter string="待上会" name="issue_state" domain="[('issue_state', '=', 'draft')]"/>
<filter string="已上会" name="issue_state" domain="[('issue_state', '=', 'on_meet')]"/>
<filter string="待上会审批" name="to_approval" domain="[('issue_state', '=', 'to_approval')]"/>
@@ -343,6 +345,8 @@
</searchpanel>
<header_search class="header-search-inline">
<field name="name"/>
<field name="code"/>
<field name="name_text"/>
<field name="department_id"/>
</header_search>
</search>
@@ -39,6 +39,7 @@ class InheritMaintenanceEquipment(models.Model):
department_id1 = fields.Many2one('hr.department', string="部门", related='employee_id.department_id')
is_jtbb = fields.Boolean(compute='_compute_company_id')
mainten_state = fields.Selection([('ing', '使用中'), ('finish', '暂停')], string="设备状态")
borrow_state = fields.Selection([('available', '可借'), ('borrowed', '已借出')], string="借用状态", default='available')
equipment_number = fields.Char(string="数量")
equipment_unit = fields.Many2one('uom.uom', string="单位")
purchase_date = fields.Date(string="购入日期")
@@ -90,6 +90,7 @@
<xpath expr="//field[@name='name']" position="after">
<field name="mainten_state"/>
<field name="borrow_state"/>
<field name="model" string="型号"/>
<field name="equipment_number"/>
<field name="equipment_unit"/>
@@ -66,11 +66,13 @@ class BatchInvoiceExportWizard(models.TransientModel):
sheet['B2'] = f'{title_year}年{title_month}月'
# 统计最常见公司(用于C2)
# 统计最常见公司(用于C2,取合同出租方公司简称)
company_list = []
for record in receivable_records:
if record.contract_id and record.contract_id.company_id:
company_list.append(record.contract_id.company_id.name)
if record.contract_id and record.contract_id.lessor_id:
referred = record.contract_id.lessor_id.referred
if referred:
company_list.append(referred)
if company_list:
from collections import Counter
@@ -80,6 +82,19 @@ class BatchInvoiceExportWizard(models.TransientModel):
sheet['C2'] = most_common_company
# 统计最常见权属类型(用于文件名)
tenure_type_map = {'self': '自有', 'agency': '代管'}
tenure_list = []
for record in receivable_records:
if record.contract_id and record.contract_id.tenure_type:
tenure_list.append(record.contract_id.tenure_type)
if tenure_list:
from collections import Counter
most_common_tenure = Counter(tenure_list).most_common(1)[0][0]
tenure_type_label = tenure_type_map.get(most_common_tenure, most_common_tenure)
else:
tenure_type_label = ''
# 从第4行开始写入数据
row = 4
template_row_idx = 4
@@ -182,7 +197,7 @@ class BatchInvoiceExportWizard(models.TransientModel):
file_data = base64.b64encode(output.read()).decode('utf-8')
attachment = self.env['ir.attachment'].create({
'name': f'批量开票_{title_year}年{title_month}月.xlsx',
'name': f'{most_common_company}_{tenure_type_label}_{title_year}年{title_month}月批量开票.xlsx',
'type': 'binary',
'datas': file_data,
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+2 -1
View File
@@ -1 +1,2 @@
from . import models
from . import models
from . import report
+1 -1
View File
@@ -6,7 +6,7 @@
'category': 'zhou',
'version': '0.1',
'license': 'LGPL-3',
'depends': ['base', 'soong_workflow', 'hr', 'product', 'documents', 'purchase', 'purchase_stock', 'sun_badge_menu', 'website_sale'],
'depends': ['base', 'soong_workflow', 'hr', 'product', 'documents', 'purchase', 'purchase_stock', 'sun_badge_menu', 'website_sale', 'yuthon_stock'],
'data': [
'security/ir.model.access.csv',
'security/company_rule.xml',
+1
View File
@@ -0,0 +1 @@
from . import purchase_order_print_report
@@ -0,0 +1,15 @@
from odoo import models
class PurchaseOrderPrintReport(models.AbstractModel):
_name = 'report.yuthon_purchase.purchase_order_print_template'
_description = '采购审批单'
def _get_report_values(self, docids, data=None):
docs = self.env['purchase.order'].sudo().browse(docids)
docargs = {
'docs': docs,
}
return docargs
@@ -21,7 +21,6 @@
.text-center { text-align: center !important; }
.text-right { text-align: right !important; }
</style>
<div class="po-wrap">
<div class="po-title">采购审批单</div>
@@ -83,7 +82,6 @@
<td width="8%">税项</td>
<td width="8%">金额</td>
</tr>
<t t-foreach="doc.order_line" t-as="line">
<tr>
<td><span t-esc="line.product_id.name or ''"/></td>
+5
View File
@@ -8,12 +8,17 @@
'license': 'LGPL-3',
'depends': ['base', 'stock', 'purchase_stock', 'documents', 'maintenance', 'funenc_json_widget'],
'data': [
'data/sequence.xml',
'security/ir.model.access.csv',
'security/company_rule.xml',
'wizard/stock_picking_transfer_wizard_views.xml',
'wizard/putaway_rule_batch_update_wizard_views.xml',
'wizard/stock_picking_submit_wizard_views.xml',
'wizard/stock_picking_return_wizard_views.xml',
'views/menu.xml',
'views/inherit_stock_picking.xml',
'views/Inherit_stock_picking_type_views.xml',
'views/inherit_product_template_views.xml',
],
'installable': True,
'application': True,
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="seq_borrow_workflow" model="ir.sequence">
<field name="name">借用领用审批流水号</field>
<field name="code">borrow.workflow.sequence</field>
<field name="prefix">JY%(y)s%(month)s%(day)s</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<!-- 修复旧数据:给借用领用 workflow record 补上 workflow_id -->
<function model="soong.workflow.record" name="fix_borrow_workflow_id"/>
</odoo>
+2 -1
View File
@@ -2,4 +2,5 @@
from . import inherit_stock_picking
from . import Inherit_stock_picking_type
from . import inherit_stock_location
from . import inherit_stock_move
from . import inherit_stock_move
from . import ir_model_patch
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from odoo import models
+58
View File
@@ -27,4 +27,62 @@ class ProductProduct(models.Model):
class StockMove(models.Model):
_inherit = 'stock.move'
product_location_ids_display = fields.Char(
string='所在仓库',
compute='_compute_product_location_ids_display',
store=False,
)
available_location_ids = fields.Many2many(
'stock.location',
string='可选位置',
compute='_compute_available_location_ids',
store=False,
)
borrow_type = fields.Selection(
[('borrow', '借用'), ('requisition', '领用')],
string='借用/领用',
compute='_compute_borrow_type',
store=False,
)
@api.depends('product_id', 'product_id.product_tag_ids')
def _compute_borrow_type(self):
"""根据产品标签判断是借用还是领用"""
for move in self:
if move.product_id:
tag_names = move.product_id.product_tag_ids.mapped('name')
if '固定资产' in tag_names:
move.borrow_type = 'borrow'
else:
move.borrow_type = 'requisition'
else:
move.borrow_type = False
@api.depends('product_id')
def _compute_product_location_ids_display(self):
"""根据产品查找所有有库存的仓库位置,拼接显示"""
for move in self:
if move.product_id:
quants = self.env['stock.quant'].search([
('product_id', '=', move.product_id.id),
('quantity', '>', 0),
('location_id.usage', '=', 'internal'),
])
locations = quants.mapped('location_id')
move.product_location_ids_display = ' / '.join(locations.mapped('complete_name')) if locations else ''
else:
move.product_location_ids_display = ''
@api.depends('product_id')
def _compute_available_location_ids(self):
"""计算该产品有库存的位置,用于 location_id 的 domain 过滤"""
for move in self:
if move.product_id:
quants = self.env['stock.quant'].search([
('product_id', '=', move.product_id.id),
('quantity', '>', 0),
('location_id.usage', '=', 'internal'),
])
move.available_location_ids = quants.mapped('location_id')
else:
move.available_location_ids = self.env['stock.location']
+397 -9
View File
@@ -28,6 +28,23 @@ class StockPicking(models.Model):
acceptance_user_id = fields.Many2one('res.users', string="指定验收人")
certifier_user_id = fields.Many2one('res.users', string="指定证明人")
is_admin = fields.Boolean(string='是否管理员', compute='_compute_is_admin')
approval_state = fields.Selection([
('draft', '草稿'),
('pending', '待审批'),
('approved', '已批准'),
('refused', '已拒绝'),
], string='审批状态', default='draft')
approver_id = fields.Many2one('res.users', string='审批人')
borrower_id = fields.Many2one('res.users', string='借用人')
return_state = fields.Selection([
('none', '未申请'),
('pending', '待审批'),
('approved', '已归还'),
('refused', '已拒绝'),
], string='归还状态', default='none')
return_approver_id = fields.Many2one('res.users', string='归还审批人')
borrow_title = fields.Char(string='标题')
borrow_code = fields.Char(string='流水号', compute='_compute_borrow_code', store=False)
user_id = fields.Many2one('res.users', string="采购人员", compute="_compute_origin", store=True)
receipt_status = fields.Selection([
('acceptance', '验收'),
@@ -155,22 +172,43 @@ class StockPicking(models.Model):
self.certifier_id = current_user.id
self.certification_date = odoo_fields.Datetime.now()
@api.depends('location_id', 'picking_type_code')
@api.depends('picking_type_code', 'move_ids_without_package')
def _compute_available_product_ids(self):
"""根据仓库和作业类型计算可用产品列表"""
"""根据作业类型计算可用产品列表"""
for record in self:
# 如果是入库单,返回所有产品(不限制)
if record.picking_type_code == 'incoming':
record.available_product_ids = self.env['product.product'].search([])
# 如果是借用/领用等,只返回该仓库中有库存的产品
elif record.location_id:
elif record.picking_type_code == 'internal':
# 当前单据已选产品(借出后库存可能为0,仍需可见)
existing_products = record.move_ids_without_package.mapped('product_id')
# 普通产品:有库存
quants = self.env['stock.quant'].search([
('location_id', '=', record.location_id.id),
('quantity', '>', 0) # 只查询有库存的
('quantity', '>', 0),
('location_id.usage', '=', 'internal'),
])
stock_products = quants.mapped('product_id')
# 固定资产产品:有可用设备
available_equip = self.env['maintenance.equipment'].search([
('borrow_state', '=', 'available'),
])
equip_product_names = set()
for equip in available_equip:
base_name = equip.name.rsplit('-', 1)[0] if '-' in equip.name else equip.name
equip_product_names.add(base_name)
if equip_product_names:
fixed_asset_products = self.env['product.product'].search([
('name', 'in', list(equip_product_names)),
('product_tag_ids.name', 'in', ['固定资产']),
])
else:
fixed_asset_products = self.env['product.product']
record.available_product_ids = stock_products | fixed_asset_products | existing_products
else:
quants = self.env['stock.quant'].search([
('quantity', '>', 0),
('location_id.usage', '=', 'internal'),
])
record.available_product_ids = quants.mapped('product_id')
else:
record.available_product_ids = False
@api.depends('picking_type_code')
@@ -215,6 +253,55 @@ class StockPicking(models.Model):
})
self.location_dest_id = location_dest_id
def _get_or_create_dept_location(self, department, company):
"""根据部门查找或自动创建内部位置,返回 location 记录"""
import logging
_logger = logging.getLogger(__name__)
# 优先在当前公司下按名称+内部位置查找
location = self.env['stock.location'].search([
('name', '=', department.name),
('usage', '=', 'internal'),
('company_id', '=', company.id),
], limit=1)
_logger.info('LOCATION DEBUG1: dept=%s, company=%s(%s), found=%s',
department.name, company.name, company.id,
location.complete_name if location else 'None')
# 任何情况都找不到则创建一个属于当前公司的新位置
if not location:
parent_location = self.env['stock.location'].search([
('complete_name', '=', company.name),
], limit=1)
location = self.env['stock.location'].create({
'name': department.name,
'location_id': parent_location.id if parent_location else False,
'usage': 'internal',
'company_id': company.id,
})
_logger.info('LOCATION DEBUG3: created=%s', location.complete_name)
return location
@api.depends('picking_type_id', 'partner_id')
def _compute_location_id(self):
"""\u91cd\u5199\u8ba1\u7b97\u4f4d\u7f6e\uff1ainternal \u7c7b\u578b\u65f6\u81ea\u52a8\u7528\u5f53\u524d\u7528\u6237\u90e8\u95e8\u4f4d\u7f6e\u4f5c\u4e3a\u76ee\u7684\u4f4d\u7f6e"""
super()._compute_location_id()
for picking in self:
if picking.state != 'draft' or picking.return_id:
continue
if picking.picking_type_code != 'internal':
continue
current_user = self.env.user
employee = current_user.employee_id
if not employee or not employee.department_id:
continue
company = current_user.company_id
location = picking._get_or_create_dept_location(employee.department_id, company)
if location:
picking.location_dest_id = location
@api.onchange('picking_type_id')
def _onchange_picking_type_auto_dest(self):
pass # 逻辑已移至 _compute_location_id 重写
@api.depends(
'move_ids_without_package.product_id',
'move_ids_without_package.product_uom_qty',
@@ -251,6 +338,9 @@ class StockPicking(models.Model):
def button_validate(self):
"""重写确认入库方法,改名为证明按鈕"""
for record in self:
# 借用/领用单必须已审批通过才能完成
if record.picking_type_code == 'internal' and record.approval_state != 'approved':
raise UserError('借用/领用单必须审批通过后才能完成')
if record.receipt_status == 'certification':
# 如果已指定证明人,检查当前用户是否是指定证明人
if record.certifier_user_id and record.certifier_user_id.id != self.env.user.id:
@@ -281,7 +371,263 @@ class StockPicking(models.Model):
'assign_date': False,
})
return super(StockPicking, self).button_validate()
def _compute_borrow_code(self):
for rec in self:
wf = self.env['soong.workflow.record'].search([
('res_model', '=', 'stock.picking'),
('res_id', '=', rec.id),
('reclaim_state', '=', False),
], limit=1)
rec.borrow_code = wf.code if wf else ''
def _get_or_create_workflow_record(self):
"""获取或创建当前借用/领用单对应的 soong.workflow.record"""
return self.env['soong.workflow.record'].search([
('res_model', '=', 'stock.picking'),
('res_id', '=', self.id),
('reclaim_state', '=', False),
], limit=1)
def action_submit(self):
"""提交审批:立即扣减库存,审批人后续只影响状态"""
for rec in self:
if rec.picking_type_code != 'internal':
raise UserError('只有借用/领用单才能提交审批')
if not rec.approver_id:
raise UserError('请先设置审批人')
# 立即完成调拨,扣减库存
if rec.state == 'draft':
rec.action_confirm()
if rec.state in ('confirmed', 'waiting'):
rec.action_assign()
if rec.state == 'assigned':
# 临时设为 approved 以绕过 button_validate 拦截,完成后改回 pending
rec.approval_state = 'approved'
rec.button_validate()
rec.approval_state = 'pending'
# 固定资产:标记设备为已借出
if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''):
for move in rec.move_ids_without_package:
product = move.product_id
if '固定资产' not in product.product_tag_ids.mapped('name'):
continue
qty = int(move.product_uom_qty)
if qty <= 0:
continue
available_equipments = self.env['maintenance.equipment'].search([
('name', 'like', product.name + '-'),
('borrow_state', '=', 'available'),
], order='name asc', limit=qty)
if len(available_equipments) < qty:
raise UserError(
f'产品「{product.name}」可用设备不足,'
f'需要 {qty} 台,当前只有 {len(available_equipments)} 台可用'
)
available_equipments.write({'borrow_state': 'borrowed'})
# 创建或更新 soong.workflow.record,只把审批人放入 users_ids(审批人待办)
current_user = self.env.user
employee = current_user.employee_id
department = employee.department_id if employee else self.env['hr.department']
if not department:
department = rec.department_id
approver_employee = rec.approver_id.employee_id if rec.approver_id else self.env['hr.employee']
wf_name = rec.borrow_title or rec.name
existing = rec._get_or_create_workflow_record()
if existing:
existing.write({
'name': wf_name,
'users_ids': [(6, 0, [rec.approver_id.id, current_user.id])],
'employee_ids': [(6, 0, approver_employee.ids)],
'done_by': current_user.id,
'approve_state': 'pending',
'soong_state': 'to_do',
'reclaim_state': False,
})
else:
self.env['soong.workflow.record'].sudo().create({
'name': wf_name,
'code': self.env['ir.sequence'].sudo().next_by_code('borrow.workflow.sequence'),
'res_model': 'stock.picking',
'res_id': rec.id,
'users_ids': [(6, 0, [rec.approver_id.id, current_user.id])],
'employee_ids': [(6, 0, approver_employee.ids)],
'done_by': current_user.id,
'approve_state': 'pending',
'soong_state': 'to_do',
'department_id': department.id if department else False,
'workflow_id': 239,
})
# 申请人提交时立即创建 log:申请人已办(2),审批人待办(0)
self.env['soong.workflow.log'].sudo().create([
{
'model': 'stock.picking',
'res_id': rec.id,
'done_by': current_user.id,
'approval_state': '2',
'lase_click_date': fields.Datetime.now(),
'comment': '已提交审批',
},
{
'model': 'stock.picking',
'res_id': rec.id,
'done_by': rec.approver_id.id,
'approval_state': '0',
'lase_click_date': fields.Datetime.now(),
'comment': '待审批',
},
])
def action_approve(self):
"""审批通过:仅更新审批状态,库存已在提交时扣减"""
for rec in self:
if rec.approver_id and rec.approver_id.id != self.env.user.id:
raise UserError('只有指定的审批人才能批准')
rec.approval_state = 'approved'
# 更新 workflow record 为已办(结束状态)
wf_record = rec._get_or_create_workflow_record()
if wf_record:
wf_record.write({
'approve_state': 'end',
'soong_state': 'done',
})
# 将审批人待办 log 更新为已办结(2)
approver_logs = self.env['soong.workflow.log'].search([
('model', '=', 'stock.picking'),
('res_id', '=', rec.id),
('done_by', '=', self.env.user.id),
('approval_state', '=', '0'),
])
if approver_logs:
approver_logs.write({'approval_state': '2', 'comment': '批准', 'lase_click_date': fields.Datetime.now()})
else:
self.env['soong.workflow.log'].sudo().create({
'model': 'stock.picking',
'res_id': rec.id,
'done_by': self.env.user.id,
'approval_state': '2',
'lase_click_date': fields.Datetime.now(),
'comment': '批准',
})
def action_refuse(self):
"""审批拒绝:创建反向调拨还原库存"""
for rec in self:
if rec.approver_id and rec.approver_id.id != self.env.user.id:
raise UserError('只有指定的审批人才能拒绝')
if rec.state != 'done':
raise UserError('当前单据状态异常,无法拒绝')
# 创建反向调拨单还原库存
reverse_vals = {
'picking_type_id': rec.picking_type_id.id,
'location_id': rec.location_dest_id.id,
'location_dest_id': rec.location_id.id,
'origin': rec.name + '(拒绝还原)',
'approval_state': 'approved',
'move_ids': [(0, 0, {
'name': move.name,
'product_id': move.product_id.id,
'product_uom_qty': move.quantity or move.product_uom_qty,
'product_uom': move.product_uom.id,
'location_id': rec.location_dest_id.id,
'location_dest_id': rec.location_id.id,
}) for move in rec.move_ids if move.state == 'done'],
}
reverse_picking = self.env['stock.picking'].create(reverse_vals)
reverse_picking.action_confirm()
reverse_picking.action_assign()
if reverse_picking.state == 'assigned':
reverse_picking.button_validate()
rec.approval_state = 'refused'
# 固定资产:恢复设备为可用
if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''):
for move in rec.move_ids:
product = move.product_id
if '固定资产' not in product.product_tag_ids.mapped('name'):
continue
qty = int(move.quantity or move.product_uom_qty)
borrowed_equip = self.env['maintenance.equipment'].search([
('name', 'like', product.name + '-'),
('borrow_state', '=', 'borrowed'),
], order='name asc', limit=qty)
borrowed_equip.write({'borrow_state': 'available'})
# 更新 workflow record 为已办(结束状态)
wf_record = rec._get_or_create_workflow_record()
if wf_record:
wf_record.write({
'approve_state': 'end',
'soong_state': 'done',
})
# 将审批人待办 log 更新为已办结(2)
approver_logs = self.env['soong.workflow.log'].search([
('model', '=', 'stock.picking'),
('res_id', '=', rec.id),
('done_by', '=', self.env.user.id),
('approval_state', '=', '0'),
])
if approver_logs:
approver_logs.write({'approval_state': '2', 'comment': '拒绝', 'lase_click_date': fields.Datetime.now()})
else:
self.env['soong.workflow.log'].sudo().create({
'model': 'stock.picking',
'res_id': rec.id,
'done_by': self.env.user.id,
'approval_state': '2',
'lase_click_date': fields.Datetime.now(),
'comment': '拒绝',
})
def action_approve_return(self):
"""归还审批通过:创建反向调拨单并完成"""
for rec in self:
if rec.return_approver_id and rec.return_approver_id.id != self.env.user.id:
raise UserError('只有指定的归还审批人才能批准归还')
if rec.return_state != 'pending':
raise UserError('当前状态不允许批准归还')
# 创建反向调拨单(目标位置 → 源位置)
return_picking_vals = {
'picking_type_id': rec.picking_type_id.id,
'location_id': rec.location_dest_id.id,
'location_dest_id': rec.location_id.id,
'origin': rec.name + '(归还)',
'approval_state': 'approved',
'move_ids': [(0, 0, {
'name': move.name,
'product_id': move.product_id.id,
'product_uom_qty': move.quantity or move.product_uom_qty,
'product_uom': move.product_uom.id,
'location_id': rec.location_dest_id.id,
'location_dest_id': rec.location_id.id,
}) for move in rec.move_ids if move.state == 'done'],
}
return_picking = self.env['stock.picking'].create(return_picking_vals)
return_picking.action_confirm()
return_picking.action_assign()
if return_picking.state == 'assigned':
return_picking.button_validate()
# 更新归还状态及固定资产设备状态
rec.return_state = 'approved'
if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''):
for move in rec.move_ids:
product = move.product_id
if '固定资产' not in product.product_tag_ids.mapped('name'):
continue
qty = int(move.quantity or move.product_uom_qty)
borrowed_equipments = self.env['maintenance.equipment'].search([
('name', 'like', product.name + '-'),
('borrow_state', '=', 'borrowed'),
], order='name asc', limit=qty)
borrowed_equipments.write({'borrow_state': 'available'})
def action_refuse_return(self):
"""归还审批拒绝"""
for rec in self:
if rec.return_approver_id and rec.return_approver_id.id != self.env.user.id:
raise UserError('只有指定的归还审批人才能拒绝归还')
if rec.return_state != 'pending':
raise UserError('当前状态不允许拒绝归还')
rec.return_state = 'refused'
def action_refresh_json_display(self):
"""手动刷新json_display字段"""
for record in self:
@@ -337,6 +683,20 @@ class StockPicking(models.Model):
self.receipt_status = 'certification' # 状态变为证明
return True
def action_reset_to_acceptance(self):
"""管理员退回:清空验收信息,重新回到待转交验收节点"""
self.ensure_one()
if not self.env.user.has_group('base.group_system'):
raise UserError('只有系统管理员才能执行此操作!')
if self.state == 'done':
raise UserError('已完成入库的单据不能退回,如需撤销请联系技术人员!')
self.write({
'receipt_status': 'acceptance',
'acceptance_user_id': False,
'employee_id': False,
'acceptance_date': False,
})
def action_transfer_acceptance(self):
"""转交验收:弹出选人 wizard"""
self.ensure_one()
@@ -405,10 +765,38 @@ class StockPicking(models.Model):
if picking.picking_type_code == 'incoming':
new_name = picking._generate_sh_sequence(picking.scheduled_date)
picking.write({'name': new_name})
# 借用/领用单创建时自动建 workflow record,和申请人关联(草稿待办)
elif picking.picking_type_code == 'internal':
current_user = self.env.user
employee = current_user.employee_id
department = employee.department_id if employee else self.env['hr.department']
if not department:
department = picking.department_id
self.env['soong.workflow.record'].sudo().create({
'name': picking.borrow_title or picking.name,
'code': self.env['ir.sequence'].sudo().next_by_code('borrow.workflow.sequence'),
'res_model': 'stock.picking',
'res_id': picking.id,
'users_ids': [(6, 0, [current_user.id])],
'employee_ids': [(6, 0, employee.ids)],
'done_by': current_user.id,
'approve_state': 'start',
'soong_state': 'to_do',
'department_id': department.id if department else False,
'workflow_id': 239,
})
return pickings
def write(self, vals):
"""重写write方法"""
result = super(StockPicking, self).write(vals)
# 标题变化时同步更新 workflow record 的 name
if 'borrow_title' in vals:
for rec in self:
if rec.picking_type_code != 'internal':
continue
wf = rec._get_or_create_workflow_record()
if wf and vals.get('borrow_title'):
wf.sudo().write({'name': vals['borrow_title']})
return result
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class IrModelPatch(models.Model):
_inherit = 'ir.model'
def _reflect_model_params(self, model):
vals = super(IrModelPatch, self)._reflect_model_params(model)
# 让 stock.picking 在工作流配置界面可被搜索到
if model._name == 'stock.picking':
vals['is_workflow'] = True
return vals
class WorkflowRecordPatch(models.Model):
_inherit = 'soong.workflow.record'
@api.model
def fix_borrow_workflow_id(self):
"""修复借用领用单 workflow record 缺少 workflow_id 的旧数据"""
records = self.search([
('res_model', '=', 'stock.picking'),
('workflow_id', '=', False),
])
if records:
records.write({'workflow_id': 239})
@@ -1,2 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_picking_transfer_wizard,stock.picking.transfer.wizard,model_stock_picking_transfer_wizard,,1,1,1,1
access_putaway_rule_batch_update_wizard,putaway.rule.batch.update.wizard,model_putaway_rule_batch_update_wizard,,1,1,1,1
access_stock_picking_submit_wizard,stock.picking.submit.wizard,model_stock_picking_submit_wizard,,1,1,1,1
access_stock_picking_return_wizard,stock.picking.return.wizard,model_stock_picking_return_wizard,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_picking_transfer_wizard stock.picking.transfer.wizard model_stock_picking_transfer_wizard 1 1 1 1
3 access_putaway_rule_batch_update_wizard putaway.rule.batch.update.wizard model_putaway_rule_batch_update_wizard 1 1 1 1
4 access_stock_picking_submit_wizard stock.picking.submit.wizard model_stock_picking_submit_wizard 1 1 1 1
5 access_stock_picking_return_wizard stock.picking.return.wizard model_stock_picking_return_wizard 1 1 1 1
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- 在产品模板"库存"页签内加上架规则跳转按钮 -->
<record id="inherit_product_template_stock_putaway" model="ir.ui.view">
<field name="name">inherit.product.template.stock.putaway</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.product_template_form_view_procurement_button"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='inventory']" position="inside">
<separator string="上架规则(存储到子位置)"/>
<group>
<button name="action_view_related_putaway_rules"
string="查看/编辑上架规则"
type="object"
class="btn-secondary"
icon="fa-random"
context="{'invisible_handle': True, 'single_product': True}"/>
</group>
</xpath>
</field>
</record>
</odoo>
+169 -39
View File
@@ -5,11 +5,14 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='picking_type_id']" position="before">
<field name="borrow_code" string="流水号" readonly="1" invisible="picking_type_code != 'internal'"/>
<field name="name" string="单据名称" invisible="picking_type_code != 'internal'" placeholder="可修改将显示于待办列表" readonly="0"/>
</xpath>
<xpath expr="//field[@name='picking_type_id']" position="after">
<field name="department_id" options="{'no_create': True}" invisible="1"/>
</xpath>
<xpath expr="//field[@name='location_id'][@groups='stock.group_stock_multi_locations']" position="attributes">
<attribute name="domain">[('id', 'in', available_location_ids)]</attribute>
<field name="picking_type_code" invisible="1"/>
<field name="create_uid" invisible="1"/>
</xpath>
<xpath expr="//field[@name='picking_type_id']" position="attributes">
<attribute name="readonly">state != 'draft' and (receipt_status in ['acceptance', 'certification'] or receipt_status == 'receipt')</attribute>
@@ -18,8 +21,11 @@
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='origin']" position="after">
<field name="purchase_id" string="源单据" readonly="1" options="{'no_create': True, 'no_open': False}"/>
<field name="purchase_id" string="源单据" readonly="1" options="{'no_create': True, 'no_open': False}" invisible="picking_type_code == 'internal'"/>
<field name="user_id" string="采购人员"/>
<field name="borrower_id" string="借用人" invisible="1" options="{'no_create': True}" readonly="1"/>
<field name="return_state" string="归还状态" invisible="1" readonly="1"/>
<field name="return_approver_id" string="归还审批人" invisible="1" options="{'no_create': True}" readonly="1"/>
<field name="available_product_ids" invisible="1"/>
<field name="available_location_ids" invisible="1"/>
<field name="state" invisible="1"/>
@@ -28,37 +34,76 @@
<field name="acceptance_user_id" invisible="1"/>
<field name="certifier_user_id" invisible="1"/>
<field name="is_admin" invisible="1"/>
<field name="approval_state" invisible="1"/>
<field name="approver_id" invisible="1"/>
<field name="return_state" invisible="1"/>
<field name="return_approver_id" invisible="1"/>
</xpath>
<xpath expr="//page[@name='operations']//field[@name='move_ids_without_package']//field[@name='product_id']" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
<attribute name="domain">[('id', 'in', parent.available_product_ids)]</attribute>
</xpath>
<xpath expr="//page[@name='operations']//field[@name='move_ids_without_package']//field[@name='product_uom'][@groups='uom.group_uom']" position="after">
<field name="available_location_ids" invisible="1"/>
<field name="location_id" string="所在仓库" optional="show"
invisible="parent.picking_type_code != 'internal'"
domain="[('id', 'in', available_location_ids)]"
options="{'no_create': True}"/>
<field name="borrow_type" string="借用/领用" readonly="1" optional="show"
invisible="parent.picking_type_code != 'internal'"/>
</xpath>
<xpath expr="//button[@name='%(stock.act_stock_return_picking)d']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='action_confirm']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="invisible">picking_type_code == 'incoming' or picking_type_code == 'internal' or state != 'draft'</attribute>
</xpath>
<xpath expr="//button[@name='action_cancel']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="invisible">picking_type_code == 'incoming' or picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//button[@name='action_assign']" position="attributes">
<attribute name="invisible">picking_type_code == 'internal' or not show_check_availability</attribute>
</xpath>
<xpath expr="//header" position="inside">
<!-- 转交验收:管理员且尚未转交时可见 -->
<!-- 提交审批:借用/领用单且草稿或已拒绝时可见,点击弹窗选审批人和借用人 -->
<button name="%(yuthon_stock.action_stock_picking_submit_wizard)d"
string="提交审批" type="action" class="btn-primary"
invisible="picking_type_code == 'incoming' or approval_state not in ['draft', 'refused']"/>
<!-- 批准:审批人且待审批时可见 -->
<button name="action_approve" string="批准" type="object" class="btn-primary"
invisible="picking_type_code == 'incoming' or approval_state != 'pending' or approver_id != uid"/>
<!-- 拒绝:审批人且待审批时可见 -->
<button name="action_refuse" string="拒绝" type="object" class="btn-secondary"
invisible="picking_type_code == 'incoming' or approval_state != 'pending' or approver_id != uid"/>
<!-- 申请归还:已完成的借用/领用单且未申请归还时可见 -->
<button name="%(yuthon_stock.action_stock_picking_return_wizard)d"
string="申请归还" type="action" class="btn-secondary"
invisible="picking_type_code != 'internal' or state != 'done' or return_state not in ['none', 'refused'] or create_uid != uid"/>
<!-- 批准归还:归还审批人且待审批时可见 -->
<button name="action_approve_return" string="批准归还" type="object" class="btn-primary"
invisible="picking_type_code != 'internal' or return_state != 'pending' or return_approver_id != uid"/>
<!-- 拒绝归还:归还审批人且待审批时可见 -->
<button name="action_refuse_return" string="拒绝归还" type="object" class="btn-secondary"
invisible="picking_type_code != 'internal' or return_state != 'pending' or return_approver_id != uid"/>
<button name="action_transfer_acceptance" string="转交验收" type="object" class="btn-secondary"
invisible="receipt_status != 'acceptance' or acceptance_user_id != False"/>
invisible="receipt_status != 'acceptance' or acceptance_user_id != False or picking_type_code != 'incoming'"/>
<!-- 管理员退回至待验收:仅管理员可见,未完成入库时可用 -->
<button name="action_reset_to_acceptance" string="退回待验收" type="object" class="btn-secondary"
groups="base.group_system"
invisible="picking_type_code != 'incoming' or state == 'done' or receipt_status == 'acceptance'"/>
<!-- 验收按钮:只有指定验收人可见,且当前用户是指定人 -->
<button name="action_acceptance" string="验收" type="object" class="btn-primary"
invisible="receipt_status != 'acceptance' or acceptance_user_id == False or acceptance_user_id != uid"/>
invisible="receipt_status != 'acceptance' or acceptance_user_id == False or acceptance_user_id != uid or picking_type_code != 'incoming'"/>
<!-- 转交证明:验收人才能转交,且尚未转交证明时可见 -->
<button name="action_transfer_certifier" string="转交证明" type="object" class="btn-secondary"
invisible="receipt_status != 'certification' or certifier_user_id != False or acceptance_user_id != uid"/>
invisible="receipt_status != 'certification' or certifier_user_id != False or acceptance_user_id != uid or picking_type_code != 'incoming'"/>
<!-- 撤回入库:管理员且入库状态时可见 -->
<!-- 撤回入库:已隐藏 -->
<button name="action_revoke_receipt" string="撤回入库" type="object" class="btn-secondary"
invisible="receipt_status != 'receipt' or not is_admin"/>
invisible="1"/>
</xpath>
<xpath expr="//page[@name='operations']/field[@name='move_ids_without_package']" position="after">
<group>
<group invisible="picking_type_code != 'incoming'">
<group>
<field name="employee_id" string="验收人" required="1" options="{'no_create': True}" readonly="receipt_status in ['certification', 'receipt']"/>
<field name="document_ids" widget="preview_many2many" readonly="receipt_status in ['certification', 'receipt']"/>
@@ -76,10 +121,16 @@
</xpath>
<xpath expr="//button[@name='button_validate' and @class='oe_highlight']" position="attributes">
<attribute name="string">证明</attribute>
<attribute name="invisible">receipt_status != 'certification' or certifier_user_id == False or certifier_user_id != uid</attribute>
<attribute name="invisible">picking_type_code != 'incoming' or receipt_status != 'certification' or certifier_user_id == False or certifier_user_id != uid</attribute>
</xpath>
<xpath expr="//field[@name='location_dest_id'][@groups='stock.group_stock_multi_locations']" position="attributes">
<attribute name="invisible">1</attribute>
<xpath expr="//button[@name='button_validate' and @class='oe_highlight']" position="after">
<button name="button_validate" string="完成" type="object" class="btn-primary"
invisible="1"/>
</xpath>
<xpath expr="//field[@name='location_dest_id'][@groups='stock.group_stock_multi_locations']" position="replace">
<field name="location_dest_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"
invisible="picking_type_code != 'internal'"
readonly="state == 'done'"/>
</xpath>
<xpath expr="//field[@name='picking_properties']" position="attributes">
<attribute name="invisible">1</attribute>
@@ -92,19 +143,49 @@
</xpath>
<xpath expr="//field[@name='user_id']" position="attributes">
<attribute name="string">采购人员</attribute>
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="string">供应商</attribute>
<attribute name="readonly">state != 'draft' and (receipt_status in ['acceptance', 'certification'] or receipt_status == 'receipt')</attribute>
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//div[@class='o_td_label'][label[@for='partner_id']]" position="attributes">
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//field[@name='origin'][@invisible='1']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//div[@class='oe_title']//field[@name='name']" position="attributes">
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//div[@class='oe_title']//field[@name='priority']" position="attributes">
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="//field[@name='scheduled_date']" position="attributes">
<attribute name="readonly">state != 'draft' and (receipt_status in ['acceptance', 'certification'] or receipt_status == 'receipt')</attribute>
</xpath>
<xpath expr="(//field[@name='state'][@widget='statusbar'])[1]" position="replace">
<field name="receipt_status" widget="statusbar" statusbar_visible="acceptance,certification,receipt" invisible="picking_type_code != 'incoming'"/>
<xpath expr="//label[@for='scheduled_date']" position="replace">
<label for="scheduled_date" invisible="picking_type_code == 'internal'"/>
</xpath>
<xpath expr="//div[field[@name='scheduled_date']]" position="attributes">
<attribute name="invisible">picking_type_code == 'internal'</attribute>
</xpath>
<xpath expr="(//field[@name='state'][@widget='statusbar'])[1]" position="replace">
<field name="receipt_status" widget="statusbar" statusbar_visible="acceptance,certification,receipt" invisible="picking_type_code == 'incoming'"/>
<field name="receipt_status" widget="statusbar" statusbar_visible="acceptance,certification,receipt" invisible="picking_type_code != 'incoming'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done" invisible="picking_type_code != 'internal'"/>
</xpath>
<xpath expr="(//field[@name='state'][@widget='statusbar'])[1]" position="replace">
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done" invisible="picking_type_code != 'outgoing'"/>
</xpath>
<xpath expr="//field[@name='location_id'][@groups='stock.group_stock_multi_locations']" position="replace">
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"
invisible="picking_type_code == 'incoming' or picking_type_code == 'internal'"
readonly="state == 'done'"
domain="[('id', 'in', available_location_ids)]"/>
</xpath>
<xpath expr="//field[@name='location_id'][@groups='!stock.group_stock_multi_locations']" position="replace">
<field name="location_id" groups="!stock.group_stock_multi_locations" invisible="1" readonly="state == 'done'"/>
</xpath>
</field>
</record>
@@ -181,12 +262,7 @@
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<tree string="领用">
<field name="name"/>
<field name="partner_id" string="供应商"/>
<field name="user_id" string="采购人员"/>
<field name="employee_id" string="验收人"/>
<field name="date_done"/>
<field name="origin"/>
<!-- <field name="json_display" widget="json_template"/>-->
<field name="company_id"/>
<field name="state"/>
@@ -194,21 +270,27 @@
</field>
</record>
<!-- 借用出库Tree视图 -->
<!-- 借用/领用 出库Tree视图 -->
<record id="stock_picking_views_tree_borrow" model="ir.ui.view">
<field name="name">stock.picking.tree.borrow</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<tree string="借用">
<field name="name"/>
<field name="partner_id" string="供应商"/>
<field name="user_id" string="采购人员"/>
<field name="employee_id" string="验收人"/>
<field name="date_done"/>
<field name="origin"/>
<!-- <field name="json_display" widget="json_template"/>-->
<field name="borrow_code" string="流水号"/>
<field name="picking_type_id" string="类型"/>
<field name="scheduled_date" string="调拨日期"/>
<field name="borrower_id" string="借用人"/>
<field name="approval_state" string="审批状态"
decoration-info="approval_state == 'draft'"
decoration-warning="approval_state == 'pending'"
decoration-success="approval_state == 'approved'"
decoration-danger="approval_state == 'refused'"/>
<field name="return_state" string="归还状态"
decoration-muted="return_state == 'none'"
decoration-warning="return_state == 'pending'"
decoration-success="return_state == 'approved'"
decoration-danger="return_state == 'refused'"/>
<field name="company_id"/>
<field name="state"/>
</tree>
</field>
</record>
@@ -239,18 +321,63 @@
<field name="view_id" ref="stock_picking_views_tree_ikuku"/>
</record>
<!-- 借用/领用出库 独立 Form 视图 -->
<record id="stock_picking_form_borrow" model="ir.ui.view">
<field name="name">stock.picking.form.borrow</field>
<field name="model">stock.picking</field>
<field name="arch" type="xml">
<form string="出库">
<header>
<field name="approver_id" invisible="1"/>
<button name="action_submit" string="提交审批" type="object" class="btn-primary"
invisible="approval_state not in ['draft', 'refused']"/>
<button name="action_approve" string="批准" type="object" class="btn-primary"
invisible="approval_state != 'pending' or approver_id != uid"/>
<button name="action_refuse" string="拒绝" type="object" class="btn-secondary"
invisible="approval_state != 'pending' or approver_id != uid"/>
<field name="approval_state" widget="statusbar"
statusbar_visible="draft,pending,approved,refused"/>
</header>
<sheet>
<group>
<group string="基本信息">
<field name="borrow_code" string="流水号" readonly="1"/>
<field name="create_uid" string="申请人" readonly="1"/>
<field name="approver_id" string="审批人" options="{'no_create': True}"/>
<field name="picking_type_id" string="作业类型" readonly="1"/>
</group>
</group>
<notebook>
<page string="作业" name="operations">
<field name="move_ids_without_package"
context="{'default_picking_id': id, 'default_picking_type_id': picking_type_id}"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="action_stock_picking_adopted" model="ir.actions.act_window">
<field name="name">领用</field>
<field name="res_model">stock.picking</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="stock_picking_views_tree_adopted"/>
<field name="domain">[('picking_type_id.name', 'ilike', '领用')]</field>
</record>
<record id="action_stock_picking_borrow" model="ir.actions.act_window">
<field name="name">借用</field>
<field name="name">借用/领用</field>
<field name="res_model">stock.picking</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="stock_picking_views_tree_borrow"/>
<field name="domain">['&amp;', '|', ('picking_type_id.name', 'ilike', '借用'), ('picking_type_id.name', 'ilike', '领用'), '|', ('create_uid', '=', uid), ('approver_id', '=', uid)]</field>
<field name="context">{"default_picking_type_id": 72}</field>
</record>
<record id="action_stock_picking_scrap" model="ir.actions.act_window">
@@ -270,17 +397,19 @@
name="出库"
parent="stock.menu_stock_warehouse_mgmt"
action="action_stock_picking_ikuku"
sequence="3"/>
sequence="3"
active="False"/>
<menuitem id="stock_picking_adopted"
parent="stock.menu_stock_warehouse_mgmt"
name="领用"
action="action_stock_picking_adopted"
sequence="5"/>
sequence="5"
active="False"/>
<menuitem id="stock_picking_borrow"
parent="stock.menu_stock_warehouse_mgmt"
name="借用"
name="出库"
action="action_stock_picking_borrow"
sequence="10"/>
sequence="3"/>
<menuitem id="stock_picking_scrap"
parent="stock.menu_stock_warehouse_mgmt"
name="报废"
@@ -289,7 +418,8 @@
<record id="stock.action_picking_tree_incoming" model="ir.actions.act_window">
<field name="name">收货</field>
<field name="context">{'contact_display': 'partner_address', 'default_company_id': allowed_company_ids[0], 'restricted_picking_type_code': 'incoming', 'search_default_reception': 1, 'search_default_my_pickings': 1}</field>
<field name="domain">['&amp;', ('picking_type_code', '=', 'incoming'), '|', '|', ('create_uid', '=', uid), ('acceptance_user_id', '=', uid), ('certifier_user_id', '=', uid)]</field>
<field name="context">{'contact_display': 'partner_address', 'restricted_picking_type_code': 'incoming', 'search_default_reception': 1}</field>
</record>
<record id="inherit_view_picking_internal_search_my" model="ir.ui.view">
+3
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
from . import stock_picking_transfer_wizard
from . import putaway_rule_batch_update_wizard
from . import stock_picking_submit_wizard
from . import stock_picking_return_wizard
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class PutawayRuleBatchUpdateWizard(models.TransientModel):
_name = 'putaway.rule.batch.update.wizard'
_description = '批量修改上架规则目标位置'
location_out_id = fields.Many2one(
'stock.location',
string='存储到子位置',
required=True,
domain=[('usage', '=', 'internal')],
options="{'no_create': True}",
)
def action_confirm(self):
rule_ids = self.env.context.get('active_ids', [])
if rule_ids:
self.env['stock.putaway.rule'].browse(rule_ids).write({
'location_out_id': self.location_out_id.id,
})
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- 向导 Form 视图 -->
<record id="putaway_rule_batch_update_wizard_form" model="ir.ui.view">
<field name="name">putaway.rule.batch.update.wizard.form</field>
<field name="model">putaway.rule.batch.update.wizard</field>
<field name="arch" type="xml">
<form string="批量修改存储到子位置">
<group>
<field name="location_out_id" options="{'no_create': True}"/>
</group>
<footer>
<button name="action_confirm" string="确认" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Server Action:在上架规则列表绑定批量操作 -->
<record id="action_putaway_rule_batch_update" model="ir.actions.server">
<field name="name">批量修改存储到子位置</field>
<field name="model_id" ref="stock.model_stock_putaway_rule"/>
<field name="binding_model_id" ref="stock.model_stock_putaway_rule"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_window',
'name': '批量修改存储到子位置',
'res_model': 'putaway.rule.batch.update.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'active_ids': env.context.get('active_ids', [])},
}
</field>
</record>
</odoo>
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import UserError
class StockPickingReturnWizard(models.TransientModel):
_name = 'stock.picking.return.wizard'
_description = '申请归还向导'
picking_id = fields.Many2one('stock.picking', string='单据', required=True)
return_approver_id = fields.Many2one(
'res.users',
string='归还审批人',
required=True,
options="{'no_create': True}",
)
note = fields.Text(string='归还备注')
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
picking_id = self.env.context.get('active_id')
if picking_id:
picking = self.env['stock.picking'].browse(picking_id)
res['picking_id'] = picking_id
res['return_approver_id'] = picking.return_approver_id.id or False
return res
def action_confirm(self):
picking = self.picking_id
if not picking:
raise UserError('未找到对应单据')
if picking.state != 'done':
raise UserError('只有已完成的借用单才能申请归还')
if picking.return_state == 'approved':
raise UserError('该借用单已归还,不能重复申请')
picking.write({
'return_approver_id': self.return_approver_id.id,
'return_state': 'pending',
})
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 申请归还向导视图 -->
<record id="view_stock_picking_return_wizard_form" model="ir.ui.view">
<field name="name">stock.picking.return.wizard.form</field>
<field name="model">stock.picking.return.wizard</field>
<field name="arch" type="xml">
<form string="申请归还">
<group>
<field name="picking_id" invisible="1"/>
<field name="return_approver_id" options="{'no_create': True}"/>
<field name="note"/>
</group>
<footer>
<button name="action_confirm" string="申请归还" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- 申请归还 Action -->
<record id="action_stock_picking_return_wizard" model="ir.actions.act_window">
<field name="name">申请归还</field>
<field name="res_model">stock.picking.return.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'active_id': active_id}</field>
</record>
</odoo>
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import UserError
class StockPickingSubmitWizard(models.TransientModel):
_name = 'stock.picking.submit.wizard'
_description = '借用/领用提交审批向导'
picking_id = fields.Many2one('stock.picking', string='单据', required=True)
approver_id = fields.Many2one(
'res.users',
string='审批人',
required=True,
options="{'no_create': True}",
)
borrower_id = fields.Many2one(
'res.users',
string='借用人',
required=True,
options="{'no_create': True}",
)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
picking_id = self.env.context.get('active_id')
if picking_id:
picking = self.env['stock.picking'].browse(picking_id)
res['picking_id'] = picking_id
res['approver_id'] = picking.approver_id.id or False
res['borrower_id'] = picking.borrower_id.id or self.env.user.id
return res
def action_confirm(self):
picking = self.picking_id
if not picking:
raise UserError('未找到对应单据')
picking.write({
'approver_id': self.approver_id.id,
'borrower_id': self.borrower_id.id,
})
picking.action_submit()
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 借用/领用提交审批向导视图 -->
<record id="view_stock_picking_submit_wizard_form" model="ir.ui.view">
<field name="name">stock.picking.submit.wizard.form</field>
<field name="model">stock.picking.submit.wizard</field>
<field name="arch" type="xml">
<form string="提交审批">
<group>
<field name="picking_id" invisible="1"/>
<field name="borrower_id" options="{'no_create': True}"/>
<field name="approver_id" options="{'no_create': True}"/>
</group>
<footer>
<button name="action_confirm" string="提交审批" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- 提交审批 Action -->
<record id="action_stock_picking_submit_wizard" model="ir.actions.act_window">
<field name="name">提交审批</field>
<field name="res_model">stock.picking.submit.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'active_id': active_id}</field>
</record>
</odoo>
+5 -3
View File
@@ -10,12 +10,10 @@ import logging
logger = logging.getLogger(__name__)
def get_is_leader_domain(is_leader: bool):
operator = 'in' if is_leader else 'not in'
return [('user_role_ids.name', operator, ['董事长', '分管领导', '总经理', '部门副经理', '副总经理', '司机'])]
class YuthonWillTask(http.Controller):
@http.route('/yuthon/yuthon/will/task', methods=['POST'], type='http', auth='none', csrf=False)
@@ -87,6 +85,9 @@ class YuthonWillTask(http.Controller):
start = int(kwargs.get('start'))
users_id = int(kwargs.get('users_id'))
_filter = kwargs.get('filter')
state = kwargs.get('state')
order = 'start_date desc' if state == 'done' else 'start_date asc'
domain = [('users_ids', 'in', [users_id])]
logger.info(f"{kwargs}")
logger.info(f"{kwargs}")
@@ -104,11 +105,12 @@ class YuthonWillTask(http.Controller):
end_date = start_date + timedelta(days=6)
domain = expression.AND([domain, [('start_date', '>=', start_date), ('start_date', '<=', end_date)]])
domain = expression.AND([domain, [('task_state', '=', state)]])
WillTask = request.env['yuthon.will.task'].sudo()
request_hour_selection = WillTask.fields_get(allfields=['request_hour_from'])['request_hour_from']['selection']
state_dict = dict(WillTask.fields_get(allfields=['task_state'])['task_state']['selection'])
result_list = []
task_ids = request.env['yuthon.will.task'].sudo().search(domain, offset=start, limit=20, order='start_date desc')
task_ids = request.env['yuthon.will.task'].sudo().search(domain, offset=start, limit=20, order=order)
for task in task_ids:
weekday_str = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][task.start_date.weekday()]
result = {
@@ -11,6 +11,7 @@ class YuthonWillTask(models.Model):
_name = 'yuthon.will.task'
_description = '日程'
_rec_name = 'name'
_order = 'task_datetime desc'
time_list = [
('0', '0:00'), ('0.25', '0:15'), ('0.5', '0:30'), ('0.75', '0:45'),
@@ -70,6 +71,7 @@ class YuthonWillTask(models.Model):
start_date = fields.Date(string='开始日期', default=fields.Date.today)
end_date = fields.Date(string='结束日期', default=fields.Date.today)
task_time = fields.Char(string='任务时间', compute='_compute_task_time', store=True)
task_datetime = fields.Datetime(string='任务时间排序', compute='_compute_task_datetime', store=True)
is_this_company = fields.Boolean(string='本公司', default=True)
task_state = fields.Selection([('will', '待办'), ('done', '已办'), ('cancel', '取消')], default='will', string='状态', store=True)
@@ -171,6 +173,27 @@ class YuthonWillTask(models.Model):
for record in will_ids:
record._compute_task_time()
@api.depends('start_date', 'request_hour_from')
def _compute_task_datetime(self):
"""将 start_date + request_hour_from 拼合为 Datetime,用于排序"""
for record in self:
if record.start_date and record.request_hour_from:
try:
hour_float = float(record.request_hour_from)
hours = int(hour_float)
minutes = int(round((hour_float - hours) * 60))
# 存储时转为UTC(界面显示是UTC+8,存储需减8小时)
record.task_datetime = datetime(
record.start_date.year,
record.start_date.month,
record.start_date.day,
hours, minutes, 0
) - timedelta(hours=8)
except Exception:
record.task_datetime = False
else:
record.task_datetime = False
@api.depends('start_date', 'task_week', 'request_hour_from', 'task_type', 'room_booking_id', 'room_booking_id.start_datetime', 'room_booking_id.stop_datetime')
def _compute_task_time(self):
for record in self:
@@ -152,7 +152,8 @@
decoration-danger="priority == 'very_urgent'"
decoration-info="priority == 'urgent'"
decoration-success="priority == 'important'" create="false" edit="false"
js_class="custom_task_tree_button" limit="15">
js_class="custom_task_tree_button" limit="15" default_order="task_datetime desc">
<field name='task_datetime' column_invisible="1"/>
<field name='start_date' column_invisible="1"/>
<field name='task_time'/>
<field name='name' required="1"/>
+1 -2
View File
@@ -38,8 +38,7 @@
<menuitem id="inventory_operation_root" name="作业" sequence="10">
<menuitem id="inventory_operation_in_root" name="入库" action="stock.action_picking_tree_incoming" sequence="10"/>
<!-- <menuitem id="inventory_operation_out_root" name="出库" action="stock.action_picking_tree_outgoing" sequence="20"/>-->
<menuitem id="inventory_operation_borrow_root" name="借用" action="stock.action_picking_tree_internal" sequence="30"/>
<menuitem id="inventory_operation_allocate_root" name="领用" action="stock.action_picking_tree_internal" sequence="40"/>
<menuitem id="inventory_operation_borrow_root" name="出库" action="yuthon_stock.action_stock_picking_borrow" sequence="30"/>
</menuitem>
<menuitem id="inventory_asset_root" name="资产" sequence="30">
<menuitem id="inventory_asset_product_root" name="产品" sequence="10">