初始数,盘点

This commit is contained in:
李鹏宇
2026-07-03 19:05:50 +08:00
parent 739f48e78e
commit b17b696687
21 changed files with 1877 additions and 44 deletions
@@ -12,12 +12,16 @@
</field>
</record>
<!-- 隐藏产品模板树视图中的字段 -->
<!-- 产品模板树视图:隐藏无用字段 + 可编辑库存位置 -->
<record id="inherit_product_template_tree_view" model="ir.ui.view">
<field name="name">product.template.tree.inherit.yuthon.purchase</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<!-- 启用行内编辑 -->
<xpath expr="//tree" position="attributes">
<attribute name="editable">bottom</attribute>
</xpath>
<!-- 隐藏barcode字段 -->
<xpath expr="//field[@name='barcode']" position="attributes">
<attribute name="column_invisible">1</attribute>
@@ -34,6 +38,10 @@
<xpath expr="//field[@name='detailed_type']" position="attributes">
<attribute name="string">详细类别</attribute>
</xpath>
<!-- 在详细类别之后添加库存位置字段(可编辑下拉) -->
<xpath expr="//field[@name='detailed_type']" position="after">
<field name="stock_location_id" string="库存位置"/>
</xpath>
</field>
</record>
+3
View File
@@ -9,6 +9,8 @@
'depends': ['base', 'stock', 'purchase_stock', 'documents', 'maintenance', 'funenc_json_widget'],
'data': [
'data/sequence.xml',
'data/inventory_sequence.xml',
'data/inventory_cron.xml',
'security/ir.model.access.csv',
'security/company_rule.xml',
'wizard/stock_picking_transfer_wizard_views.xml',
@@ -19,6 +21,7 @@
'views/inherit_stock_picking.xml',
'views/Inherit_stock_picking_type_views.xml',
'views/inherit_product_template_views.xml',
'views/yuthon_inventory_views.xml',
],
'installable': True,
'application': True,
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 盘点任务定时提醒 cron -->
<record id="cron_yuthon_inventory_reminder" model="ir.cron">
<field name="name">盘点任务提醒</field>
<field name="model_id" ref="model_yuthon_inventory_task"/>
<field name="state">code</field>
<field name="code">model.action_send_reminder()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">False</field>
<field name="nextcall" eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 08:00:00')"/>
</record>
</odoo>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 盘点任务编号序列 -->
<record id="seq_yuthon_inventory_task" model="ir.sequence">
<field name="name">盘点任务编号</field>
<field name="code">yuthon.inventory.task</field>
<field name="prefix">INV%(y)s%(month)s</field>
<field name="padding">3</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="company_id" eval="False"/>
</record>
</odoo>
+6
View File
@@ -3,4 +3,10 @@ from . import inherit_stock_picking
from . import Inherit_stock_picking_type
from . import inherit_stock_location
from . import inherit_stock_move
from . import inherit_product_template
from . import inherit_product_product
from . import ir_model_patch
from . import yuthon_inventory_task
from . import yuthon_inventory_sheet
from . import yuthon_inventory_sheet_line
from . import yuthon_inventory_adjustment
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""继承 product.product,让"在手"数量统计所有内部库位
Odoo 默认只在仓库层级内的库位统计 qty_available,而本项目中存在大量
像"办公室"这样不属于仓库层级的内部库位,导致产品树"在手"列显示为 0。
这里覆盖 _get_domain_locations,使默认情况下统计所有内部库位。
"""
from odoo import models
class ProductProduct(models.Model):
"""继承 product.product,修改数量计算的默认库位范围"""
_inherit = 'product.product'
def _get_domain_locations(self):
"""覆盖默认逻辑:未指定仓库/位置时,统计所有内部库位
当 context 中没有显式的 location 或 warehouse 时,不再只取仓库的
view_location_id,而是取所有当前公司下的内部库位,保证 办公室、
保密室 等非仓库层级库位也能正确计入 qty_available。
"""
# 如果 context 中有显式 location 或 warehouse,保持原逻辑不变
if self.env.context.get('location') or self.env.context.get('warehouse'):
return super(ProductProduct, self)._get_domain_locations()
# 默认:使用所有内部库位(usage='internal)且属于当前可用公司的
Location = self.env['stock.location']
location_ids = set(Location.search([
('usage', '=', 'internal'),
('company_id', 'in', self.env.companies.ids),
]).ids)
return self._get_domain_locations_new(location_ids)
@@ -1,2 +1,80 @@
# -*- coding: utf-8 -*-
from odoo import models
from odoo import api, fields, models
class ProductTemplate(models.Model):
"""继承 product.template,添加库存位置相关字段"""
_inherit = 'product.template'
# 库存位置名称(所有内部库位,顿号拼接)—— 只读展示
stock_location_names = fields.Char(
string='库存位置',
compute='_compute_stock_location_names',
help='产品在内部库位的所有位置,多个位置用顿号拼接',
)
# 可编辑的库存位置下拉 —— 用户可通过下拉修改位置,移动库存
stock_location_id = fields.Many2one(
'stock.location',
string='库存位置',
compute='_compute_stock_location_id',
inverse='_inverse_stock_location_id',
store=True,
domain="[('usage', '=', 'internal')]",
help='选择目标位置,产品在当前内部库位的库存将被移动到该位置',
)
@api.depends('product_variant_ids', 'product_variant_ids.stock_quant_ids',
'product_variant_ids.stock_quant_ids.location_id')
def _compute_stock_location_names(self):
"""计算产品所有内部库位名称,去重后用顿号拼接"""
for template in self:
if not template.product_variant_ids:
template.stock_location_names = ''
continue
# 搜索该产品所有变体在内部库位的库存记录
quants = self.env['stock.quant'].search([
('product_id', 'in', template.product_variant_ids.ids),
('location_id.usage', '=', 'internal'),
])
# 去重:用 dict.fromkeys 保持插入顺序
location_names = list(dict.fromkeys(
quants.mapped('location_id.display_name')
))
template.stock_location_names = '、'.join(location_names) if location_names else ''
@api.depends('product_variant_ids', 'product_variant_ids.stock_quant_ids',
'product_variant_ids.stock_quant_ids.location_id',
'product_variant_ids.stock_quant_ids.quantity')
def _compute_stock_location_id(self):
"""计算当前主库存位置 ——
取库存数量最大的内部库位作为默认值,没有库存则留空
"""
for template in self:
if not template.product_variant_ids:
template.stock_location_id = False
continue
# 取库存数量最大的那个内部库位
quants = self.env['stock.quant'].search([
('product_id', 'in', template.product_variant_ids.ids),
('location_id.usage', '=', 'internal'),
], order='quantity desc', limit=1)
template.stock_location_id = quants.location_id.id if quants else False
def _inverse_stock_location_id(self):
"""用户选择新位置时,将产品所有内部库位的库存合并移动到新位置
- Odoo 自动把同产品同位置的 quant 合并为一条,无需手动处理数量
"""
for template in self:
if not template.stock_location_id:
continue
new_location = template.stock_location_id
# 找到该产品所有不在目标位置的 quant → 直接搬家
quants = self.env['stock.quant'].sudo().search([
('product_id', 'in', template.product_variant_ids.ids),
('location_id.usage', '=', 'internal'),
('location_id', '!=', new_location.id),
])
for q in quants:
# 直接改位置,Odoo 自动合并同产品同库位的 quant
q.location_id = new_location.id
@@ -6,6 +6,23 @@ import json
class InheritStockLocation(models.Model):
_inherit = 'stock.location'
def write(self, values):
"""覆盖 write 方法,允许修改已使用库位的 company_id"""
if 'company_id' in values:
# Odoo 原生禁止修改已使用库位的公司,我们通过 SQL 绕过此限制
company_value = values.pop('company_id')
# 先写其他字段(走父类完整校验)
result = super(InheritStockLocation, self).write(values)
# 直接用 SQL 写入 company_id,绕过 Python 层的限制检查
self._cr.execute(
'UPDATE stock_location SET company_id = %s WHERE id IN %s',
[company_value, tuple(self.ids)]
)
# 清除缓存,确保后续读取到最新值
self.invalidate_recordset()
return result
return super(InheritStockLocation, self).write(values)
def update_stock(self):
department_obj = self.env['hr.department']
root_departments = department_obj.search([('child_ids', '=', False)])
+10 -23
View File
@@ -694,19 +694,21 @@ class StockPicking(models.Model):
return True
def action_acceptance(self):
"""验收按鈕:检查是否指定验收人,自动填充验收人和验收时间"""
"""验收按钮:采购人员直接执行验收,自动填充验收人和验收时间,并记录验收人身份以便后续转交证明"""
self.ensure_one()
# 如果已指定验收人,检查当前用户是否是指定验收人
if self.acceptance_user_id and self.acceptance_user_id.id != self.env.user.id:
raise UserError('只有指定的验收人才能执行验收!')
# 用指定验收人填充,没有指定则取当前用户
acceptor = self.acceptance_user_id or self.env.user
# 检查当前用户是否为采购人员(user_id),只有采购人员才能执行验收
if self.user_id and self.user_id.id != self.env.user.id:
raise UserError('只有采购人员才能执行验收!')
acceptor = self.env.user
current_employee = acceptor.employee_ids[:1] if acceptor.employee_ids else False
self.acceptance_date = dt.utcnow()
if current_employee:
self.employee_id = current_employee.id
self.receipt_status = 'certification' # 状态变为证明
# 将验收人的待办 log 更新为已办
self.write({
'receipt_status': 'certification', # 状态变为证明
'acceptance_user_id': self.env.user.id, # 记录验收人身份,供后续转交证明按钮权限判断使用
})
# 将验收人的待办 log 更新为已办(若存在由转交验收生成的待办 log)
pending_logs = self.env['soong.workflow.log'].search([
('model', '=', 'stock.picking'),
('res_id', '=', self.id),
@@ -730,21 +732,6 @@ class StockPicking(models.Model):
'acceptance_date': False,
})
def action_transfer_acceptance(self):
"""转交验收:弹出选人 wizard"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': '转交验收',
'res_model': 'stock.picking.transfer.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_picking_id': self.id,
'default_transfer_type': 'acceptance',
},
}
def action_transfer_certifier(self):
"""转交证明:弹出选人 wizard"""
self.ensure_one()
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class YuthonInventoryAdjustment(models.Model):
"""库存调整记录:盘点提交后自动生成,记录每次库存修正"""
_name = 'yuthon.inventory.adjustment'
_description = '库存调整记录'
_order = 'adjust_date desc, id desc'
task_id = fields.Many2one(
'yuthon.inventory.task',
string='所属盘点任务',
readonly=True,
)
line_id = fields.Many2one(
'yuthon.inventory.sheet.line',
string='所属盘点明细行',
readonly=True,
)
# 办公用品关联产品
product_id = fields.Many2one(
'product.product',
string='调整产品',
readonly=True,
)
# 固定资产关联设备
equipment_id = fields.Many2one(
'maintenance.equipment',
string='调整设备',
readonly=True,
)
adjust_type = fields.Selection(
[('increase', '盘盈(增加)'), ('decrease', '盘亏(减少)')],
string='调整类型',
required=True,
readonly=True,
)
qty_before = fields.Float(
string='调整前数量',
readonly=True,
digits='Product Unit of Measure',
)
qty_after = fields.Float(
string='调整后数量',
readonly=True,
digits='Product Unit of Measure',
)
adjust_qty = fields.Float(
string='调整数量',
readonly=True,
digits='Product Unit of Measure',
)
reason = fields.Char(string='调整原因', readonly=True)
adjust_date = fields.Datetime(
string='调整时间',
readonly=True,
default=fields.Datetime.now,
)
company_id = fields.Many2one(
'res.company',
string='所属公司',
readonly=True,
)
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class YuthonInventorySheet(models.Model):
"""盘点表头:每次点击「确认生成盘点表」时创建一条,内含明细行"""
_name = 'yuthon.inventory.sheet'
_description = '盘点表'
_order = 'create_date desc, id desc'
# ========== 关联任务 ==========
task_id = fields.Many2one(
'yuthon.inventory.task',
string='所属盘点任务',
required=True,
ondelete='cascade',
)
# ========== 头记录信息 ==========
name = fields.Char(
string='盘点表名称',
compute='_compute_name',
store=True,
)
# ========== 明细行 ==========
line_ids = fields.One2many(
'yuthon.inventory.sheet.line',
'sheet_id',
string='盘点明细',
)
# 办公用品明细行(按 sheet_type 过滤)
office_line_ids = fields.One2many(
'yuthon.inventory.sheet.line',
'sheet_id',
string='办公用品明细',
domain=[('sheet_type', '=', 'office')],
)
# 固定资产明细行(按 sheet_type 过滤)
fixed_asset_line_ids = fields.One2many(
'yuthon.inventory.sheet.line',
'sheet_id',
string='固定资产明细',
domain=[('sheet_type', '=', 'fixed_asset')],
)
line_count = fields.Integer(
string='明细行数',
compute='_compute_line_count',
)
# ========== 状态 ==========
state = fields.Selection(
[('pending', '待盘点'), ('done', '已完成')],
string='状态',
default='pending',
readonly=True,
index=True,
)
# ========== 计算方法 ==========
@api.depends('task_id.code', 'task_id.name')
def _compute_name(self):
"""盘点表名称 = 任务编号 + 任务名"""
for sheet in self:
if sheet.task_id:
sheet.name = '%s - %s' % (sheet.task_id.code or '', sheet.task_id.name or '')
else:
sheet.name = ''
@api.depends('line_ids')
def _compute_line_count(self):
"""计算明细行数"""
for sheet in self:
sheet.line_count = len(sheet.line_ids)
@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class YuthonInventorySheetLine(models.Model):
"""盘点表明细行:每条记录对应一个需要盘点的产品或固定资产设备"""
_name = 'yuthon.inventory.sheet.line'
_description = '盘点表明细行'
_order = 'sheet_type, product_id, equipment_id'
# ========== 关联头记录 ==========
sheet_id = fields.Many2one(
'yuthon.inventory.sheet',
string='所属盘点表',
required=True,
ondelete='cascade',
)
# ========== 盘点类型 ==========
sheet_type = fields.Selection(
[('office', '办公用品'), ('fixed_asset', '固定资产')],
string='盘点类型',
required=True,
readonly=True,
index=True,
)
# ========== 办公用品字段 ==========
product_id = fields.Many2one(
'product.product',
string='产品',
readonly=True,
)
# ========== 固定资产字段 ==========
equipment_id = fields.Many2one(
'maintenance.equipment',
string='固定资产设备',
readonly=True,
)
# ========== 显示信息 ==========
product_name = fields.Char(
string='产品/设备名称',
compute='_compute_display_info',
readonly=True,
store=True,
)
equipment_name = fields.Char(
string='设备名称',
related='equipment_id.display_name',
readonly=True,
store=False,
)
specification = fields.Char(
string='规格型号',
compute='_compute_display_info',
store=True,
)
# ========== 库存位置(办公用品可编辑,固定资产不显示) ==========
stock_location_id = fields.Many2one(
'stock.location',
string='库存位置',
compute='_compute_stock_location_id',
inverse='_inverse_stock_location_id',
store=True,
domain="[('usage', '=', 'internal')]",
help='选择目标位置,产品在当前内部库位的库存将被移动到该位置',
)
user_name = fields.Char(string='使用人')
department_name = fields.Char(string='使用部门')
equipment_unit_name = fields.Char(
string='单位',
compute='_compute_display_info',
store=True,
)
# ========== 数量字段 ==========
on_hand_qty = fields.Float(
string='在手数量(系统数)',
readonly=True,
digits='Product Unit of Measure',
)
actual_qty = fields.Float(
string='实际盘点数量',
digits='Product Unit of Measure',
)
difference_qty = fields.Float(
string='差额',
compute='_compute_difference',
store=True,
digits='Product Unit of Measure',
)
has_difference = fields.Boolean(
string='有差额',
compute='_compute_difference',
store=True,
)
difference_reason = fields.Char(string='差额原因')
# ========== 状态 ==========
company_id = fields.Many2one(
'res.company',
string='所属公司',
readonly=True,
)
state = fields.Selection(
[('pending', '待盘点'), ('counted', '已盘点')],
string='盘点状态',
default='pending',
readonly=True,
)
# ========== 计算字段 ==========
@api.depends('product_id', 'equipment_id', 'sheet_type')
def _compute_display_info(self):
"""根据盘点类型,从产品或设备带出名称、规格型号、单位"""
for line in self:
if line.sheet_type == 'fixed_asset' and line.equipment_id:
eq = line.equipment_id
line.product_name = eq.name or ''
line.specification = eq.model or ''
line.equipment_unit_name = eq.equipment_unit.name if eq.equipment_unit else ''
elif line.sheet_type == 'office' and line.product_id:
tmpl = line.product_id.product_tmpl_id
line.product_name = line.product_id.display_name or ''
line.specification = tmpl.description or tmpl.name or ''
line.equipment_unit_name = ''
else:
line.product_name = ''
line.specification = ''
line.equipment_unit_name = ''
@api.depends('on_hand_qty', 'actual_qty')
def _compute_difference(self):
"""计算差额"""
for line in self:
line.difference_qty = line.actual_qty - line.on_hand_qty
line.has_difference = line.difference_qty != 0.0
@api.depends('product_id', 'sheet_type')
def _compute_stock_location_id(self):
"""计算库存位置 ——
办公用品:从 stock.quant 取数量最大的内部库位
固定资产:无库存位置概念,返回 False(视图用 invisible 隐藏)
"""
for line in self:
if line.sheet_type != 'office' or not line.product_id:
line.stock_location_id = False
continue
# 从产品变体取库存量最大的内部库位
quants = self.env['stock.quant'].sudo().search([
('product_id', '=', line.product_id.id),
('location_id.usage', '=', 'internal'),
], order='quantity desc', limit=1)
line.stock_location_id = quants.location_id.id if quants else False
def _inverse_stock_location_id(self):
"""用户修改库存位置时,将产品所有 internal quant 搬到新位置
与 product.template._inverse_stock_location_id 逻辑一致,
改动的是同一张 stock.quant 表,两边自动同步
"""
for line in self:
if not line.stock_location_id or not line.product_id:
continue
new_location = line.stock_location_id
# 找到该产品所有不在目标位置的 quant → 直接搬家
quants = self.env['stock.quant'].sudo().search([
('product_id', '=', line.product_id.id),
('location_id.usage', '=', 'internal'),
('location_id', '!=', new_location.id),
])
for q in quants:
q.location_id = new_location.id
def action_edit_line(self):
"""打开编辑弹窗,预填当前行的数据"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': '编辑明细行',
'res_model': 'yuthon.inventory.line.wizard',
'view_mode': 'form',
'target': 'new', # 弹窗模式
'context': {
'default_line_id': self.id,
},
}
def action_submit_line(self):
# 校验:有差额时必须填原因
if self.difference_qty != 0.0 and not self.difference_reason:
raise UserError('「%s」存在差额(%.2f),请先填写差额原因后再提交!' % (
self.product_name, self.difference_qty))
if self.sheet_type == 'office':
self._submit_office_line()
elif self.sheet_type == 'fixed_asset':
self._submit_fixed_asset_line()
# 标记该行已盘点
self.state = 'counted'
# 检查所属头记录是否所有明细行都已盘点
sheet = self.sheet_id
pending_lines = sheet.line_ids.filtered(lambda l: l.state == 'pending')
if not pending_lines:
sheet.state = 'done'
# 如果任务下所有头记录都完成了,标记任务完成
task = sheet.task_id
all_done = all(s.state == 'done' for s in task.sheet_ids)
if all_done:
task.state = 'done'
def _submit_office_line(self):
"""提交办公用品明细:通过 stock.quant 调整库存数量
调整后 product.template.qty_available 会自动反映最新值
使用 sudo() 绕过多公司 record rule 限制(产品可能属于非当前用户公司)
"""
product = self.product_id
actual_qty = self.actual_qty
# 使用产品自身的公司ID,确保 quant 的公司与产品一致
product_company = product.company_id.id if product.company_id else False
# 整个流程在 sudo 下执行:绕过 record rule + 绕过 _search bug
Quant = self.env['stock.quant'].sudo()
quants = Quant.search([
('product_id', '=', product.id),
('location_id.usage', '=', 'internal'),
'|',
('company_id', '=', product_company),
('company_id', '=', False),
])
if not quants:
default_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
if not default_location:
raise UserError('未找到默认库存位置,无法创建库存记录!')
quant = Quant.create({
'product_id': product.id,
'location_id': default_location.id,
'quantity': 0.0,
'company_id': product_company,
})
else:
quant = quants.filtered(lambda q: q.quantity > 0.0)[:1] or quants[:1]
# 通过 inventory adjustment 设置实际数量(sudo 下避免跨公司报错)
quant.inventory_quantity = actual_qty
quant.sudo().action_apply_inventory()
# 创建调整记录
self.env['yuthon.inventory.adjustment'].create({
'task_id': self.sheet_id.task_id.id,
'line_id': self.id,
'product_id': product.id,
'adjust_type': 'increase' if self.difference_qty > 0 else 'decrease',
'qty_before': self.on_hand_qty,
'qty_after': actual_qty,
'adjust_qty': abs(self.difference_qty),
'reason': self.difference_reason or '',
'company_id': product_company,
})
def _submit_fixed_asset_line(self):
"""提交固定资产明细:更新 maintenance.equipment.equipment_number"""
equipment = self.equipment_id
actual_qty = self.actual_qty
# 将实际盘点数量写入 equipment_number(Char 类型)
if actual_qty == int(actual_qty):
equipment.equipment_number = str(int(actual_qty))
else:
equipment.equipment_number = str(round(actual_qty, 2))
# 创建调整记录
self.env['yuthon.inventory.adjustment'].create({
'task_id': self.sheet_id.task_id.id,
'line_id': self.id,
'equipment_id': equipment.id,
'product_id': False,
'adjust_type': 'increase' if self.difference_qty > 0 else 'decrease',
'qty_before': self.on_hand_qty,
'qty_after': actual_qty,
'adjust_qty': abs(self.difference_qty),
'reason': self.difference_reason or '',
'company_id': self.company_id.id if self.company_id else False,
})
@@ -0,0 +1,354 @@
# -*- coding: utf-8 -*-
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class YuthonInventoryTask(models.Model):
"""盘点任务:定义一次盘点的范围、人员和时间
每次点击「确认并生成盘点表」会创建一条盘点表头记录(yuthon.inventory.sheet),
头记录下包含多条明细行(yuthon.inventory.sheet.line),每条对应一个产品/设备
"""
_name = 'yuthon.inventory.task'
_description = '盘点任务'
_order = 'create_date desc, id desc'
name = fields.Char(string='盘点名称', required=True, index=True)
code = fields.Char(string='编号', readonly=True, copy=False, default='New')
planned_date = fields.Datetime(string='计划盘点时间')
employee_ids = fields.Many2many(
'hr.employee',
'inventory_task_employee_rel',
'task_id',
'employee_id',
string='盘点人员',
required=True,
)
company_ids = fields.Many2many(
'res.company',
string='公司范围',
required=True,
default=lambda self: self.env.company,
)
include_fixed_assets = fields.Boolean(string='盘点固定资产', default=True)
include_office_supplies = fields.Boolean(string='盘点办公用品', default=True)
state = fields.Selection(
[('draft', '草稿'), ('confirmed', '已确认'), ('done', '已完成')],
string='状态',
default='draft',
index=True,
readonly=True,
copy=False,
)
# 盘点表头记录(每次确认生成一条)
sheet_ids = fields.One2many(
'yuthon.inventory.sheet',
'task_id',
string='盘点表',
readonly=True,
)
sheet_count = fields.Integer(string='盘点表数', compute='_compute_sheet_count')
note = fields.Text(string='备注')
reminded = fields.Boolean(string='已提醒', default=False, copy=False)
create_uid = fields.Many2one('res.users', string='创建人', readonly=True)
create_date = fields.Datetime(string='创建时间', readonly=True)
@api.depends('sheet_ids')
def _compute_sheet_count(self):
"""计算盘点表头数量"""
for task in self:
task.sheet_count = len(task.sheet_ids)
@api.model_create_multi
def create(self, vals_list):
"""创建时自动生成编号,格式:INV-202606-001"""
for vals in vals_list:
if vals.get('code', 'New') == 'New':
vals['code'] = self.env['ir.sequence'].next_by_code('yuthon.inventory.task') or 'New'
return super().create(vals_list)
def action_confirm(self):
"""确认任务:生成一条盘点表头 + 所有明细行,状态改为已确认"""
self.ensure_one()
if self.state != 'draft':
raise UserError('只有草稿状态的任务才能确认!')
if not self.include_fixed_assets and not self.include_office_supplies:
raise UserError('请至少选择一种盘点内容(固定资产或办公用品)!')
if not self.sheet_ids:
self._generate_sheets()
self.state = 'confirmed'
def action_done(self):
"""完成任务:批量提交所有待盘点的明细行
逐行执行提交逻辑(与点击每行「提交修改」按钮效果相同)
"""
self.ensure_one()
if self.state != 'confirmed':
raise UserError('只有已确认状态的任务才能提交!')
# 收集所有待盘点的明细行
pending_lines = self.env['yuthon.inventory.sheet.line']
for sheet in self.sheet_ids:
pending_lines |= sheet.line_ids.filtered(lambda l: l.state == 'pending')
if not pending_lines:
raise UserError('没有需要提交的盘点明细!')
# 逐行提交
for line in pending_lines:
try:
line.action_submit_line()
except UserError as e:
# 单行失败不中断,记录错误继续处理其他行
_logger.warning('明细行[%s]提交失败: %s', line.product_name, str(e))
# 标记任务完成
self.state = 'done'
def action_draft(self):
"""回到草稿:清空盘点表,允许重新生成"""
self.ensure_one()
if self.state == 'done':
raise UserError('已完成的任务不能回到草稿!')
self.state = 'draft'
# 清空盘点表(删除头记录,级联删除明细行)
self.sheet_ids.unlink()
def _generate_sheets(self):
"""根据任务条件生成 1 条盘点表头 + N 条明细行
- 办公用品从 product.template("办公用品"标签)+ stock.quant 取数
- 固定资产从 maintenance.equipment 取数
"""
self.ensure_one()
Sheet = self.env['yuthon.inventory.sheet']
SheetLine = self.env['yuthon.inventory.sheet.line']
_logger.info('=== _generate_sheets 开始 ===')
_logger.info('任务ID=%s, name=%s, code=%s', self.id, self.name, self.code)
# 创建一条盘点表头
sheet = Sheet.create({
'task_id': self.id,
})
_logger.info('创建盘点表头 ID=%s', sheet.id)
line_count = 0
# ===== 办公用品明细行 =====
if self.include_office_supplies:
# 只取任务选定公司范围内的产品
product_tmpl_domain = [
('type', 'in', ('product', 'consu')),
'|',
('company_id', 'in', self.company_ids.ids),
('company_id', '=', False),
]
office_tags = self.env['product.tag'].search([('name', '=', '办公用品')])
_logger.info('办公用品标签查询 → 找到 %d 个标签', len(office_tags))
if office_tags:
office_domain = product_tmpl_domain + [('product_tag_ids', 'in', office_tags.ids)]
office_tmpls = self.env['product.template'].search(office_domain)
_logger.info('办公用品模板查询 → 找到 %d 个模板', len(office_tmpls))
if office_tmpls:
products = self.env['product.product'].search([
('product_tmpl_id', 'in', office_tmpls.ids),
])
_logger.info('办公用品变体查询 → 找到 %d 个产品', len(products))
if products:
# 只查询有实际库存的 quant(quantity > 0),
# 确保盘点明细只带出仓库里真正有货的产品
quant_domain = [
('product_id', 'in', products.ids),
('location_id.usage', '=', 'internal'),
('quantity', '>', 0),
]
quants = self.env['stock.quant'].search(quant_domain)
_logger.info('办公用品 stock.quant 查询(quantity>0)→ %d 条记录', len(quants))
# 按产品汇总库存数量
product_summary = {}
for quant in quants:
pid = quant.product_id.id
if pid not in product_summary:
product_summary[pid] = {'qty': 0.0}
product_summary[pid]['qty'] += quant.quantity
# 只创建有实际库存的产品的明细行(跳过库存为 0 的产品)
for pid, summary in product_summary.items():
product = self.env['product.product'].browse(pid)
if not product.exists():
continue
# 使用产品自身的公司ID,确保后续提交时公司匹配
line_company = product.company_id.id if product.company_id else (
self.company_ids[0].id if self.company_ids else False
)
SheetLine.create({
'sheet_id': sheet.id,
'sheet_type': 'office',
'product_id': product.id,
'on_hand_qty': summary['qty'],
'company_id': line_company,
})
line_count += 1
_logger.info('办公用品明细行创建完成(共 %d 行)', line_count)
# ===== 固定资产明细行 =====
if self.include_fixed_assets:
# 只取任务选定公司范围内的设备
eq_domain = [
'|',
('company_id', 'in', self.company_ids.ids),
('company_id', '=', False),
]
equipments = self.env['maintenance.equipment'].search(eq_domain)
_logger.info('固定资产设备查询 → 找到 %d 台设备', len(equipments))
for eq in equipments:
try:
on_hand = float(eq.equipment_number or 0)
except (ValueError, TypeError):
on_hand = 0.0
eq_company = False
if eq.company_id:
eq_company = eq.company_id.id
elif self.company_ids:
eq_company = self.company_ids[0].id
SheetLine.create({
'sheet_id': sheet.id,
'sheet_type': 'fixed_asset',
'equipment_id': eq.id,
'on_hand_qty': on_hand,
'user_name': eq.employee_id.name if eq.employee_id else '',
'department_name': eq.department_id1.name if eq.department_id1 else '',
'company_id': eq_company,
})
line_count += 1
_logger.info('固定资产明细行创建完成')
if line_count == 0:
raise UserError('根据所选条件未找到任何可盘点的数据,请检查盘点内容设置!')
_logger.info('=== _generate_sheets 完成:头记录=1, 明细行=%d ===', line_count)
def _adjust_inventory(self, lines=None):
"""提交盘点结果时,按类型分别处理明细行:
- 办公用品:通过 stock.quant 调整库存(标准 Odoo 方式)
- 固定资产:更新 maintenance.equipment 的 equipment_number 字段
"""
self.ensure_one()
if lines is None:
# 收集所有头记录下的所有明细行
lines = self.env['yuthon.inventory.sheet.line']
for sheet in self.sheet_ids:
lines |= sheet.line_ids
diff_lines = lines.filtered(lambda l: l.difference_qty != 0)
if not diff_lines:
return
office_lines = diff_lines.filtered(lambda l: l.sheet_type == 'office')
asset_lines = diff_lines.filtered(lambda l: l.sheet_type == 'fixed_asset')
# ===== 办公用品:通过 stock.quant.inventory_quantity 调整库存 =====
for line in office_lines:
product = line.product_id
actual_qty = line.actual_qty
quants = self.env['stock.quant'].search([
('product_id', '=', product.id),
('location_id.usage', '=', 'internal'),
'|',
('company_id', 'in', self.company_ids.ids),
('company_id', '=', False),
])
if not quants:
default_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
if not default_location:
continue
quant = self.env['stock.quant'].create({
'product_id': product.id,
'location_id': default_location.id,
'quantity': 0.0,
'company_id': self.company_ids[0].id if self.company_ids else False,
})
else:
quant = quants.filtered(lambda q: q.quantity > 0.0)[:1] or quants[:1]
quant.inventory_quantity = actual_qty
quant.action_apply_inventory()
self.env['yuthon.inventory.adjustment'].create({
'task_id': self.id,
'line_id': line.id,
'product_id': product.id,
'adjust_type': 'increase' if line.difference_qty > 0 else 'decrease',
'qty_before': line.on_hand_qty,
'qty_after': actual_qty,
'adjust_qty': abs(line.difference_qty),
'reason': line.difference_reason,
'company_id': line.company_id.id if line.company_id else False,
})
# ===== 固定资产:更新 maintenance.equipment 的 equipment_number 字段 =====
for line in asset_lines:
equipment = line.equipment_id
actual_qty = line.actual_qty
if actual_qty == int(actual_qty):
equipment.equipment_number = str(int(actual_qty))
else:
equipment.equipment_number = str(round(actual_qty, 2))
_logger.info('固定资产盘点: 设备[%s](id=%s) 数量从 %s 更新为 %s',
equipment.name, equipment.id, line.on_hand_qty, equipment.equipment_number)
self.env['yuthon.inventory.adjustment'].create({
'task_id': self.id,
'line_id': line.id,
'equipment_id': equipment.id,
'product_id': False,
'adjust_type': 'increase' if line.difference_qty > 0 else 'decrease',
'qty_before': line.on_hand_qty,
'qty_after': actual_qty,
'adjust_qty': abs(line.difference_qty),
'reason': line.difference_reason,
'company_id': line.company_id.id if line.company_id else False,
})
def action_open_sheets(self):
"""跳转到当前任务的盘点表头列表"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': '盘点表 - %s' % self.name,
'res_model': 'yuthon.inventory.sheet',
'view_mode': 'tree,form',
'domain': [('task_id', '=', self.id)],
'target': 'current',
}
def action_send_reminder(self):
"""手动触发提醒(供 cron 调用)"""
now = fields.Datetime.now()
tasks = self.search([
('state', '=', 'confirmed'),
('planned_date', '!=', False),
('planned_date', '<=', now),
('reminded', '=', False),
])
for task in tasks:
# TODO: 接入企业微信后,此处发送消息给 task.employee_ids
task.reminded = True
@@ -3,3 +3,9 @@ access_stock_picking_transfer_wizard,stock.picking.transfer.wizard,model_stock_p
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
access_yuthon_inventory_task,yuthon.inventory.task,model_yuthon_inventory_task,,1,1,1,1
access_yuthon_inventory_sheet,yuthon.inventory.sheet,model_yuthon_inventory_sheet,,1,1,1,1
access_yuthon_inventory_sheet_line,yuthon.inventory.sheet.line,model_yuthon_inventory_sheet_line,,1,1,1,1
access_yuthon_inventory_adjustment,yuthon.inventory.adjustment,model_yuthon_inventory_adjustment,,1,1,1,0
access_yuthon_inventory_line_wizard,yuthon.inventory.line.wizard,model_yuthon_inventory_line_wizard,,1,1,1,1
access_yuthon_stock_backfill_wizard,yuthon.stock.backfill.wizard,model_yuthon_stock_backfill_wizard,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
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
6 access_yuthon_inventory_task yuthon.inventory.task model_yuthon_inventory_task 1 1 1 1
7 access_yuthon_inventory_sheet yuthon.inventory.sheet model_yuthon_inventory_sheet 1 1 1 1
8 access_yuthon_inventory_sheet_line yuthon.inventory.sheet.line model_yuthon_inventory_sheet_line 1 1 1 1
9 access_yuthon_inventory_adjustment yuthon.inventory.adjustment model_yuthon_inventory_adjustment 1 1 1 0
10 access_yuthon_inventory_line_wizard yuthon.inventory.line.wizard model_yuthon_inventory_line_wizard 1 1 1 1
11 access_yuthon_stock_backfill_wizard yuthon.stock.backfill.wizard model_yuthon_stock_backfill_wizard 1 1 1 1
+4 -7
View File
@@ -79,16 +79,13 @@
<!-- 拒绝归还:归还审批人且待审批时可见 -->
<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 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 or picking_type_code != 'incoming'"/>
<!-- 转交证明:验收人才能转交,且尚未转交证明时可见 -->
invisible="receipt_status != 'acceptance' or user_id == False or 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 or picking_type_code != 'incoming'"/>
@@ -433,7 +430,7 @@
<record id="stock.action_picking_tree_incoming" model="ir.actions.act_window">
<field name="name">收货</field>
<field name="domain">['&amp;', ('picking_type_code', '=', 'incoming'), '|', '|', ('create_uid', '=', uid), ('acceptance_user_id', '=', uid), ('certifier_user_id', '=', uid)]</field>
<field name="domain">['&amp;', ('picking_type_code', '=', 'incoming'), '|', '|', '|', ('create_uid', '=', uid), ('user_id', '=', 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>
<field name="views" eval="[(False, 'tree'), (ref('stock.view_picking_form'), 'form')]"/>
</record>
+7 -5
View File
@@ -5,9 +5,11 @@
<field name="name">固定资产</field>
</record>
<menuitem id="purchase_assets_equipment_root"
name="固定资产"
parent="stock.menu_stock_inventory_control"
action="maintenance.hr_equipment_action"
sequence="5"/>
<!-- 新建固定资产专属动作,指定默认 kanban 视图 -->
<record id="action_fixed_assets" model="ir.actions.act_window">
<field name="name">固定资产</field>
<field name="res_model">maintenance.equipment</field>
<field name="view_mode">kanban,tree,form</field>
<field name="view_id" ref="maintenance.hr_equipment_view_kanban"/>
</record>
</odoo>
@@ -0,0 +1,484 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============ 盘点任务视图 ============ -->
<!-- tree 列表视图 -->
<record id="view_yuthon_inventory_task_tree" model="ir.ui.view">
<field name="name">yuthon.inventory.task.tree</field>
<field name="model">yuthon.inventory.task</field>
<field name="arch" type="xml">
<tree string="盘点任务" decoration-info="state=='draft'" decoration-success="state=='confirmed'" decoration-muted="state=='done'">
<field name="code"/>
<field name="name"/>
<field name="company_ids" widget="many2many_tags"/>
<field name="state" widget="badge"/>
<field name="sheet_count"/>
<field name="create_uid"/>
<field name="create_date"/>
</tree>
</field>
</record>
<!-- form 表单视图(去掉内嵌盘点表列表) -->
<record id="view_yuthon_inventory_task_form" model="ir.ui.view">
<field name="name">yuthon.inventory.task.form</field>
<field name="model">yuthon.inventory.task</field>
<field name="arch" type="xml">
<form string="盘点任务">
<header>
<button name="action_confirm" string="确认并生成盘点表" type="object" class="oe_highlight" invisible="state!='draft'"/>
<button name="action_done" string="提交盘点结果" type="object" class="oe_highlight" invisible="state!='confirmed'"/>
<button name="action_draft" string="回到草稿" type="object" invisible="state not in ('confirmed',)"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="code" readonly="1"/>
</h1>
<h3>
<field name="name" placeholder="请输入盘点名称"/>
</h3>
</div>
<group>
<group string="盘点设置">
<field name="company_ids" widget="many2many_tags" required="1" options="{'no_create': True}"/>
<field name="employee_ids" widget="many2many_tags" required="1" options="{'no_create': True}"
domain="[('company_id', 'in', company_ids)]"/>
</group>
<group string="盘点内容">
<field name="include_fixed_assets" required="1"/>
<field name="include_office_supplies" required="1"/>
</group>
</group>
<!-- 已确认后显示盘点表入口 -->
<group string="盘点表" invisible="state=='draft'">
<div>
<p>已生成 <strong><field name="sheet_count"/></strong> 张盘点表。</p>
<p invisible="state!='confirmed'">
<a href="#" name="action_open_sheets" type="object">点击填写盘点结果</a>
</p>
</div>
</group>
<group string="备注" invisible="state=='draft'">
<field name="note" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- kanban 看板视图 -->
<record id="view_yuthon_inventory_task_kanban" model="ir.ui.view">
<field name="name">yuthon.inventory.task.kanban</field>
<field name="model">yuthon.inventory.task</field>
<field name="arch" type="xml">
<kanban string="盘点任务">
<field name="state"/>
<field name="company_ids"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card">
<div class="oe_kanban_content">
<strong><field name="code"/></strong>
<div><field name="name"/></div>
<div>状态:<field name="state" widget="badge"/></div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ============ 盘点表头视图 ============ -->
<!-- tree 列表视图:显示任务编号+名称、状态、明细数 -->
<record id="view_yuthon_inventory_sheet_tree" model="ir.ui.view">
<field name="name">yuthon.inventory.sheet.tree</field>
<field name="model">yuthon.inventory.sheet</field>
<field name="arch" type="xml">
<tree string="盘点表">
<field name="name"/>
<field name="line_count" string="明细数"/>
<field name="state" widget="badge"/>
<field name="create_date"/>
</tree>
</field>
</record>
<!-- form 表单视图:notebook 分页展示明细行 -->
<record id="view_yuthon_inventory_sheet_form" model="ir.ui.view">
<field name="name">yuthon.inventory.sheet.form</field>
<field name="model">yuthon.inventory.sheet</field>
<field name="arch" type="xml">
<form string="盘点表">
<header>
<field name="state" widget="statusbar" statusbar_visible="pending,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<notebook>
<!-- 全部明细 -->
<page string="全部明细" name="all_lines">
<field name="line_ids" nolabel="1" options="{'no_create': True}">
<tree string="盘点明细">
<field name="sheet_type" invisible="1"/>
<field name="product_name"/>
<field name="specification"/>
<field name="stock_location_id" invisible="sheet_type != 'office'"/>
<field name="equipment_unit_name" invisible="sheet_type != 'fixed_asset'"/>
<field name="on_hand_qty"/>
<field name="actual_qty"/>
<field name="difference_qty" decoration-danger="difference_qty&lt;0" decoration-success="difference_qty&gt;0"/>
<field name="user_name"/>
<field name="department_name"/>
<field name="difference_reason"/>
<field name="state" widget="badge"/>
<button name="action_edit_line" string="编辑" type="object"
icon="fa-pencil" class="btn-secondary"
invisible="state=='counted'"/>
<button name="action_submit_line" string="提交" type="object"
icon="fa-check-circle" class="btn-primary"
invisible="state=='counted'"
confirm="确认提交该行盘点结果?提交后将调整库存数据。"/>
</tree>
</field>
</page>
<!-- 办公用品明细 -->
<page string="办公用品" name="office_lines">
<field name="office_line_ids" nolabel="1" options="{'no_create': True}">
<tree string="办公用品明细">
<field name="product_name"/>
<field name="specification"/>
<field name="stock_location_id"/>
<field name="on_hand_qty"/>
<field name="actual_qty"/>
<field name="difference_qty" decoration-danger="difference_qty&lt;0" decoration-success="difference_qty&gt;0"/>
<field name="difference_reason"/>
<field name="state" widget="badge"/>
<button name="action_edit_line" string="编辑" type="object"
icon="fa-pencil" class="btn-secondary"
invisible="state=='counted'"/>
<button name="action_submit_line" string="提交" type="object"
icon="fa-check-circle" class="btn-primary"
invisible="state=='counted'"
confirm="确认提交该行盘点结果?提交后将调整库存数据。"/>
</tree>
</field>
</page>
<!-- 固定资产明细 -->
<page string="固定资产" name="fixed_asset_lines">
<field name="fixed_asset_line_ids" nolabel="1" options="{'no_create': True}">
<tree string="固定资产明细">
<field name="product_name"/>
<field name="specification"/>
<field name="equipment_unit_name"/>
<field name="user_name"/>
<field name="department_name"/>
<field name="on_hand_qty"/>
<field name="actual_qty"/>
<field name="difference_qty" decoration-danger="difference_qty&lt;0" decoration-success="difference_qty&gt;0"/>
<field name="difference_reason"/>
<field name="state" widget="badge"/>
<button name="action_edit_line" string="编辑" type="object"
icon="fa-pencil" class="btn-secondary"
invisible="state=='counted'"/>
<button name="action_submit_line" string="提交" type="object"
icon="fa-check-circle" class="btn-primary"
invisible="state=='counted'"
confirm="确认提交该行盘点结果?提交后将更新设备数量。"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- ============ 盘点表明细行视图 ============ -->
<!-- tree 列表视图(独立菜单用) -->
<record id="view_yuthon_inventory_sheet_line_tree" model="ir.ui.view">
<field name="name">yuthon.inventory.sheet.line.tree</field>
<field name="model">yuthon.inventory.sheet.line</field>
<field name="arch" type="xml">
<tree string="盘点明细">
<field name="sheet_id"/>
<field name="sheet_type" widget="badge"/>
<field name="product_name"/>
<field name="specification"/>
<field name="on_hand_qty"/>
<field name="actual_qty"/>
<field name="difference_qty"/>
<field name="state" widget="badge"/>
</tree>
</field>
</record>
<!-- form 表单视图(独立菜单用) -->
<record id="view_yuthon_inventory_sheet_line_form" model="ir.ui.view">
<field name="name">yuthon.inventory.sheet.line.form</field>
<field name="model">yuthon.inventory.sheet.line</field>
<field name="arch" type="xml">
<form string="盘点明细">
<sheet>
<group>
<field name="sheet_type" widget="badge" readonly="1"/>
</group>
<group>
<group>
<field name="product_name" readonly="1"/>
<field name="specification" readonly="1"/>
<field name="stock_location_id" invisible="sheet_type != 'office'"/>
<field name="equipment_id" readonly="1" invisible="sheet_type != 'fixed_asset'"/>
<field name="equipment_unit_name" readonly="1" invisible="sheet_type != 'fixed_asset'"/>
</group>
<group>
<field name="on_hand_qty" readonly="1"/>
<field name="actual_qty"/>
<field name="difference_qty" readonly="1"/>
</group>
</group>
<group>
<field name="user_name"/>
<field name="department_name"/>
</group>
<group>
<field name="difference_reason" placeholder="有差额时必须填写"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============ 库存调整记录视图 ============ -->
<record id="view_yuthon_inventory_adjustment_tree" model="ir.ui.view">
<field name="name">yuthon.inventory.adjustment.tree</field>
<field name="model">yuthon.inventory.adjustment</field>
<field name="arch" type="xml">
<tree string="库存调整记录">
<field name="task_id"/>
<field name="line_id"/>
<field name="product_id" invisible="not product_id"/>
<field name="equipment_id" invisible="not equipment_id"/>
<field name="adjust_type" widget="badge"/>
<field name="qty_before"/>
<field name="qty_after"/>
<field name="adjust_qty"/>
<field name="reason"/>
<field name="adjust_date"/>
</tree>
</field>
</record>
<record id="view_yuthon_inventory_adjustment_form" model="ir.ui.view">
<field name="name">yuthon.inventory.adjustment.form</field>
<field name="model">yuthon.inventory.adjustment</field>
<field name="arch" type="xml">
<form string="库存调整记录">
<sheet>
<group>
<field name="task_id" readonly="1"/>
<field name="line_id" readonly="1"/>
<field name="product_id" readonly="1" invisible="not product_id"/>
<field name="equipment_id" readonly="1" invisible="not equipment_id"/>
<field name="adjust_type" readonly="1"/>
</group>
<group>
<field name="qty_before" readonly="1"/>
<field name="qty_after" readonly="1"/>
<field name="adjust_qty" readonly="1"/>
</group>
<group>
<field name="reason" readonly="1"/>
<field name="adjust_date" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ============ 动作定义 ============ -->
<!-- 盘点任务 action -->
<record id="action_yuthon_inventory_task" model="ir.actions.act_window">
<field name="name">盘点任务</field>
<field name="res_model">yuthon.inventory.task</field>
<field name="view_mode">tree,form,kanban</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一条盘点任务
</p>
</field>
</record>
<!-- 盘点表 action(头记录) -->
<record id="action_yuthon_inventory_sheet" model="ir.actions.act_window">
<field name="name">盘点表</field>
<field name="res_model">yuthon.inventory.sheet</field>
<field name="view_mode">tree,form</field>
</record>
<!-- 盘点明细 action(独立查看) -->
<record id="action_yuthon_inventory_sheet_line" model="ir.actions.act_window">
<field name="name">盘点明细</field>
<field name="res_model">yuthon.inventory.sheet.line</field>
<field name="view_mode">tree,form</field>
</record>
<!-- 库存调整记录 action -->
<record id="action_yuthon_inventory_adjustment" model="ir.actions.act_window">
<field name="name">库存调整记录</field>
<field name="res_model">yuthon.inventory.adjustment</field>
<field name="view_mode">tree,form</field>
</record>
<!-- ============ 菜单 ============ -->
<menuitem id="menu_yuthon_inventory_root" name="盘点管理" parent="yuthon_work_brand.inventory_menus_root_board" sequence="100"/>
<menuitem id="menu_yuthon_inventory_task" name="盘点任务" parent="menu_yuthon_inventory_root" action="action_yuthon_inventory_task" sequence="10"/>
<menuitem id="menu_yuthon_inventory_sheet" name="盘点表" parent="menu_yuthon_inventory_root" action="action_yuthon_inventory_sheet" sequence="20"/>
<menuitem id="menu_yuthon_inventory_sheet_line" name="盘点明细" parent="menu_yuthon_inventory_root" action="action_yuthon_inventory_sheet_line" sequence="25"/>
<menuitem id="menu_yuthon_inventory_adjustment" name="库存调整记录" parent="menu_yuthon_inventory_root" action="action_yuthon_inventory_adjustment" sequence="30"/>
<!-- ============ 盘点明细编辑向导 ============ -->
<!-- wizard form -->
<record id="view_yuthon_inventory_line_wizard_form" model="ir.ui.view">
<field name="name">yuthon.inventory.line.wizard.form</field>
<field name="model">yuthon.inventory.line.wizard</field>
<field name="arch" type="xml">
<form string="编辑盘点明细">
<group>
<group>
<field name="product_name" readonly="1"/>
<field name="specification" readonly="1"/>
<field name="is_office" invisible="1"/>
<field name="stock_location_id" invisible="not is_office"/>
<field name="on_hand_qty" readonly="1"/>
</group>
<group>
<field name="actual_qty"/>
<field name="difference_qty" readonly="1"/>
<field name="difference_reason"
placeholder="有差额时必须填写"/>
</group>
</group>
<group>
<field name="employee_id" invisible="is_office"/>
<field name="department_name" readonly="1" invisible="is_office"/>
</group>
<footer>
<button name="action_save" string="保存" type="object"
class="btn-primary" icon="fa-check"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- wizard action -->
<record id="action_yuthon_inventory_line_wizard" model="ir.actions.act_window">
<field name="name">编辑盘点明细</field>
<field name="res_model">yuthon.inventory.line.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- ============ 补全旧数据向导 ============ -->
<!-- wizard form -->
<record id="view_yuthon_stock_backfill_wizard_form" model="ir.ui.view">
<field name="name">yuthon.stock.backfill.wizard.form</field>
<field name="model">yuthon.stock.backfill.wizard</field>
<field name="arch" type="xml">
<form string="补全旧数据">
<sheet>
<div class="alert alert-info" role="alert">
<strong>📦 补全旧数据</strong>:跳过完整入库流程,直接在库存位置创建产品并设定库存数量。
适用于系统上线前已有库存的产品数据补录。
</div>
<group string="产品信息">
<group>
<field name="product_name" placeholder="请输入产品名称" required="1"/>
<field name="description" placeholder="规格型号(选填)"/>
</group>
<group>
<field name="detailed_type" required="1"/>
<field name="product_tag_ids" widget="many2many_tags"
options="{'no_create': True}"/>
</group>
</group>
<group string="产品分类">
<group>
<field name="categ_id"/>
</group>
<group>
<field name="uom_id" required="1"/>
</group>
</group>
<group string="库存信息">
<group>
<field name="company_id" required="1" options="{'no_create': True}"/>
<field name="location_id"
options="{'no_create': True}"
placeholder="留空则使用公司默认仓库"/>
</group>
<group>
<field name="quantity" required="1"/>
</group>
</group>
</sheet>
<footer>
<button name="action_confirm" string="确认补全" type="object"
class="btn-primary" icon="fa-check"/>
<button string="取消" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- wizard action -->
<record id="action_yuthon_stock_backfill_wizard" model="ir.actions.act_window">
<field name="name">补全旧数据</field>
<field name="res_model">yuthon.stock.backfill.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- 补全旧数据菜单项 -->
<menuitem id="menu_yuthon_stock_backfill"
name="补全旧数据"
parent="menu_yuthon_inventory_root"
action="action_yuthon_stock_backfill_wizard"
sequence="5"/>
<!-- 修复库存公司数据 action(修复历史遗留的空公司 quant) -->
<record id="action_repair_quant_company" model="ir.actions.server">
<field name="name">修复库存公司数据</field>
<field name="model_id" ref="model_yuthon_stock_backfill_wizard"/>
<field name="state">code</field>
<field name="code">
repaired = env['yuthon.stock.backfill.wizard']._repair_orphan_quants()
message = '\u2705 \u5e93\u5b58\u516c\u53f8\u6570\u636e\u4fee\u590d\u5b8c\u6210\uff01\n\n'
message += '\u5171\u4fee\u590d %d \u6761\u7a7a\u516c\u53f8\u7684 quant\u8bb0\u5f55\u3002' % repaired
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '\u64cd\u4f5c\u6210\u529f',
'message': message,
'type': 'success',
'sticky': True,
},
}
</field>
</record>
<!-- 修复库存公司数据菜单项 -->
<menuitem id="menu_yuthon_stock_repair_quants"
name="修复库存公司数据"
parent="menu_yuthon_inventory_root"
action="action_repair_quant_company"
sequence="6"/>
</odoo>
+3 -1
View File
@@ -1,5 +1,7 @@
# -*- 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
from . import putaway_rule_batch_update_wizard
from . import yuthon_inventory_line_wizard
from . import yuthon_stock_backfill_wizard
@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""盘点明细编辑向导:弹出窗口编辑 single line,填写后提交"""
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class YuthonInventoryLineWizard(models.TransientModel):
"""盘点明细行编辑弹窗"""
_name = 'yuthon.inventory.line.wizard'
_description = '盘点明细编辑向导'
# ========== 关联明细行 ==========
line_id = fields.Many2one(
'yuthon.inventory.sheet.line',
string='明细行',
readonly=True,
)
# ========== 只读展示字段 ==========
product_name = fields.Char(string='产品/设备名称', readonly=True)
specification = fields.Char(string='规格型号', readonly=True)
# 是否为办公用品(用于控制库存位置字段的显示/隐藏)
is_office = fields.Boolean(
string='是否办公用品',
compute='_compute_is_office',
)
# 库存位置:办公用品可编辑,固定资产不显示
stock_location_id = fields.Many2one(
'stock.location',
string='库存位置',
domain="[('usage', '=', 'internal')]",
)
on_hand_qty = fields.Float(string='在手数量(系统数)', readonly=True)
# ========== 可编辑字段 ==========
actual_qty = fields.Float(string='实际盘点数量', digits='Product Unit of Measure')
# 固定资产:员工下拉(办公用品隐藏)
employee_id = fields.Many2one(
'hr.employee',
string='使用人',
)
# 使用部门(只读,根据选中的员工自动带出)
department_name = fields.Char(
string='使用部门',
compute='_compute_department_name',
readonly=True,
)
difference_reason = fields.Char(string='差额原因',
help='实际数量与在手数量有差额时必须填写')
# ========== 计算字段 ==========
difference_qty = fields.Float(
string='差额',
compute='_compute_difference',
digits='Product Unit of Measure',
)
@api.depends('line_id')
def _compute_is_office(self):
"""根据关联明细行的类型判断是否为办公用品"""
for wiz in self:
wiz.is_office = bool(wiz.line_id and wiz.line_id.sheet_type == 'office')
@api.depends('employee_id', 'employee_id.department_id')
def _compute_department_name(self):
"""根据选中的员工自动带出所属部门名称"""
for wiz in self:
if wiz.employee_id and wiz.employee_id.department_id:
wiz.department_name = wiz.employee_id.department_id.name
else:
wiz.department_name = ''
@api.depends('actual_qty', 'on_hand_qty')
def _compute_difference(self):
"""动态计算差额"""
for wiz in self:
wiz.difference_qty = (wiz.actual_qty or 0.0) - (wiz.on_hand_qty or 0.0)
@api.model
def default_get(self, fields_list):
"""打开弹窗时从明细行预填数据"""
res = super().default_get(fields_list)
line_id = self.env.context.get('default_line_id')
if line_id:
line = self.env['yuthon.inventory.sheet.line'].browse(line_id)
if line.exists():
# 预填固定资产的员工(按名称匹配 hr.employee)
employee_id = False
if line.sheet_type == 'fixed_asset' and line.user_name:
employee = self.env['hr.employee'].sudo().search([
('name', '=', line.user_name),
], limit=1)
if employee:
employee_id = employee.id
res.update({
'line_id': line.id,
'product_name': line.product_name or '',
'specification': line.specification or '',
'is_office': (line.sheet_type == 'office'),
'stock_location_id': line.stock_location_id.id if (line.sheet_type == 'office' and line.stock_location_id) else False,
'on_hand_qty': line.on_hand_qty or 0.0,
'actual_qty': line.actual_qty if line.actual_qty else 0.0,
'employee_id': employee_id, # 固定资产预填员工,办公用品留空
'difference_reason': line.difference_reason or '',
})
return res
def action_save(self):
"""保存数据到明细行(仅写回编辑字段,不执行提交/库存调整逻辑)
固定资产:员工下拉选择后同步写回 maintenance.equipment
"""
self.ensure_one()
line = self.line_id
if not line:
raise UserError('未找到关联的盘点明细行!')
# 写回用户编辑过的字段(库存位置改动会触发 inverse 移动库存)
write_vals = {
'actual_qty': self.actual_qty,
'difference_reason': self.difference_reason,
}
# 办公用品:使用人/部门保持 Char 写法
if line.sheet_type == 'office':
write_vals['user_name'] = ''
write_vals['department_name'] = ''
else:
# 固定资产:将 employee_id 的名称写回 line 的 user_name
# department_name 由 compute 自动取,无需手动写入
write_vals['user_name'] = self.employee_id.name if self.employee_id else ''
write_vals['department_name'] = self.department_name or ''
# 办公用品才写回库存位置
if line.sheet_type == 'office' and self.stock_location_id:
write_vals['stock_location_id'] = self.stock_location_id.id
line.write(write_vals)
# 固定资产:直接将 employee_id 和 department_id1 同步到 maintenance.equipment
if line.sheet_type == 'fixed_asset' and line.equipment_id:
eq = line.equipment_id
eq_vals = {}
eq_vals['employee_id'] = self.employee_id.id if self.employee_id else False
# 部门从员工的 department_id 取出 → 写入设备的 department_id1
if self.employee_id and self.employee_id.department_id:
eq_vals['department_id1'] = self.employee_id.department_id.id
else:
eq_vals['department_id1'] = False
eq.sudo().write(eq_vals)
# 关闭弹窗(不执行提交,用户回到列表后点「提交」按钮)
return {'type': 'ir.actions.act_window_close'}
@@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
"""补全旧数据向导:极简入库流程
跳过大而全的采购入库流程,直接在库存位置创建产品并设定库存数量
适用于系统上线前已有库存的产品数据补录
"""
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
class YuthonStockBackfillWizard(models.TransientModel):
"""补全旧数据弹窗 —— 新建产品 + 指定库位 + 填写库存数量,一键创建"""
_name = 'yuthon.stock.backfill.wizard'
_description = '补全旧数据向导'
# ========== 产品信息 ==========
product_name = fields.Char(
string='产品名称',
required=True,
help='请输入产品名称(必填)',
)
product_tag_ids = fields.Many2many(
'product.tag',
string='产品标签',
help='选择产品标签,如"办公用品""常用耗材"等(非必填)',
)
description = fields.Char(
string='规格型号',
help='产品的规格或型号描述(非必填)',
)
detailed_type = fields.Selection(
[('product', '库存产品'), ('consu', '消耗品'), ('service', '服务')],
string='产品类型',
default='product',
required=True,
help='库存产品:需要管理库存数量的实物产品',
)
categ_id = fields.Many2one(
'product.category',
string='产品类别',
help='选择产品所属类别(非必填)',
)
uom_id = fields.Many2one(
'uom.uom',
string='计量单位',
required=True,
default=lambda self: self.env.ref('uom.product_uom_unit', raise_if_not_found=False),
help='产品的计量单位,默认"件"',
)
# ========== 库存信息 ==========
company_id = fields.Many2one(
'res.company',
string='所属公司',
required=True,
default=lambda self: self.env.company,
help='产品归属的公司',
)
location_id = fields.Many2one(
'stock.location',
string='库存位置',
required=False,
domain="[('usage', '=', 'internal')]",
help='选择产品所在的内部库存位置,留空则自动使用公司默认仓库库位',
)
quantity = fields.Float(
string='库存数量',
required=True,
default=0.0,
digits='Product Unit of Measure',
help='当前库存中该产品的实际在手数量',
)
def action_confirm(self):
"""执行补全操作:
1. 创建 product.template + product.product
2. 确保库存位置有 company_id(quant 的公司是只读相关字段,从位置继承)
3. 创建 stock.quant 并通过 inventory adjustment 设置库存数量
4. 修复可能存在的 quant 公司为空的问题
"""
self.ensure_one()
# ===== 校验 =====
if self.quantity < 0:
raise ValidationError('库存数量不能为负数!')
# ===== Step 1: 创建产品模板 =====
tmpl_vals = {
'name': self.product_name,
'detailed_type': self.detailed_type,
'company_id': self.company_id.id,
}
if self.product_tag_ids:
tmpl_vals['product_tag_ids'] = [(6, 0, self.product_tag_ids.ids)]
if self.description:
tmpl_vals['description'] = self.description
if self.categ_id:
tmpl_vals['categ_id'] = self.categ_id.id
if self.uom_id:
tmpl_vals['uom_id'] = self.uom_id.id
# 创建产品模板(用 sudo 避免权限问题)
product_tmpl = self.env['product.template'].sudo().create(tmpl_vals)
# 获取自动生成的 product.product 变体
product = product_tmpl.product_variant_id
if not product:
raise UserError('产品创建失败:未生成产品变体,请联系管理员。')
# ===== Step 2: 确定库存位置(未选时自动用公司默认仓库) =====
if self.location_id:
location = self.location_id.sudo()
else:
# 用户没选库存位置,自动使用该公司下的默认仓库库位
warehouse = self.env['stock.warehouse'].sudo().search([
('company_id', '=', self.company_id.id),
], limit=1)
if not warehouse:
raise UserError('未找到该公司[%s]的默认仓库,请手动选择库存位置!' % self.company_id.name)
location = warehouse.lot_stock_id.sudo()
# stock.quant.company_id 是 readonly related 字段(= location_id.company_id),
# 所以 quant 的公司完全由所在位置决定,不能在 create 时直接传入
target_company = self.company_id
if location.company_id != target_company:
# 用 SQL 直接更新位置的公司(绕过 Odoo 的禁止修改限制)
location._cr.execute(
'UPDATE stock_location SET company_id = %s WHERE id = %s',
[target_company.id, location.id]
)
# 强制刷新缓存(包括位置和相关的 quant)
self.env['stock.location'].invalidate_model(['company_id'])
self.env['stock.quant'].invalidate_model(['company_id'])
# 重新读取 location,确保后续 create 用的是新值
location = self.env['stock.location'].sudo().browse(location.id)
# ===== Step 3: 创建 stock.quant 并设置库存数量 =====
Quant = self.env['stock.quant'].sudo()
existing_quant = Quant.search([
('product_id', '=', product.id),
('location_id', '=', location.id),
], limit=1)
if existing_quant:
# 已有 quant:直接调整库存数量
existing_quant.sudo().inventory_quantity = self.quantity
existing_quant.sudo().user_id = self.env.user.id
existing_quant.sudo().inventory_date = fields.Date.today()
existing_quant.sudo().action_apply_inventory()
target_quant = existing_quant
else:
# 创建空 quant(company_id 不传,自动从 location 继承)
quant = Quant.create({
'product_id': product.id,
'location_id': location.id,
'quantity': 0.0,
})
# 通过 inventory adjustment 写入真实数量
quant.sudo().inventory_quantity = self.quantity
quant.sudo().user_id = self.env.user.id
quant.sudo().inventory_date = fields.Date.today()
quant.sudo().action_apply_inventory()
target_quant = quant
# 关键:确保这个 quant 的 company_id 与 location 一致(相关字段可能因缓存没刷新而空白)
self._cr.execute('''
UPDATE stock_quant sq
SET company_id = sl.company_id
FROM stock_location sl
WHERE sq.id = %s
AND sq.location_id = sl.id
''', [target_quant.id])
# ===== Step 4: 修复所有空公司的 quant(从位置继承公司)=====
repaired = self._repair_orphan_quants()
# ===== Step 5: 清除缓存 =====
product_tmpl.invalidate_recordset(['qty_available'])
self.env['stock.quant'].invalidate_model()
# ===== 成功提示 =====
message = (
'\u2705 \u65e7\u6570\u636e\u8865\u5168\u6210\u529f\uff01\n\n'
'\u4ea7\u54c1\u540d\u79f0\uff1a%s\n'
'\u5e93\u5b58\u4f4d\u7f6e\uff1a%s\n'
'\u5e93\u5b58\u6570\u91cf\uff1a%s %s\n\n'
'\u4ea7\u54c1\u5df2\u521b\u5efa\u5e76\u53ef\u5728\u7cfb\u7edf\u4e2d\u6b63\u5e38\u4f7f\u7528\u3002'
) % (
product.display_name,
location.complete_name or location.name,
self.quantity,
self.uom_id.name if self.uom_id else '',
)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '\u64cd\u4f5c\u6210\u529f',
'message': message,
'type': 'success',
'sticky': True,
},
}
@api.model
def _repair_orphan_quants(self):
"""修复所有 company_id 为空的 quant:从所在位置继承公司
stock.quant.company_id 是 readonly related 字段(= location_id.company_id),
当位置原本没有公司时创建的 quant 会留下空公司,导致 qty_available 无法读取。
此方法通过 SQL 批量修复,并同步修复所有子位置的公司归属。
"""
# 第一步:先把所有没有公司但父位置有公司的内部位置,公司继承父位置
# 这样能覆盖 办公室 -> 办公室/子位置 这种层级结构
self.env.cr.execute('''
UPDATE stock_location child
SET company_id = parent.company_id
FROM stock_location parent
WHERE child.location_id = parent.id
AND (child.company_id IS NULL OR child.company_id = 0)
AND parent.company_id IS NOT NULL
AND parent.company_id != 0
''')
fixed_locations = self.env.cr.rowcount
# 第二步:修复所有 quant 的公司,从位置继承
self.env.cr.execute('''
UPDATE stock_quant sq
SET company_id = sl.company_id
FROM stock_location sl
WHERE sq.location_id = sl.id
AND (sq.company_id IS NULL OR sq.company_id = 0)
AND sl.company_id IS NOT NULL
AND sl.company_id != 0
''')
fixed_quants = self.env.cr.rowcount
# 强制刷新缓存
if fixed_locations > 0 or fixed_quants > 0:
self.env['stock.location'].invalidate_model(['company_id'])
self.env['stock.quant'].invalidate_model()
return fixed_quants
+6 -6
View File
@@ -6,7 +6,7 @@
<field name="name">办公用品</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('product_tag_ids.name', '=', '办公用品'), ('product_variant_ids.stock_move_ids.picking_id.receipt_status', '=', 'receipt')]</field>
<field name="domain">[('product_tag_ids.name', '=', '办公用品'), ('product_variant_ids.stock_quant_ids.location_id.usage', '=', 'internal')]</field>
<field name="context">{'default_detailed_type': 'product'}</field>
</record>
@@ -14,7 +14,7 @@
<field name="name">常用耗材</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('product_tag_ids.name', '=', '常用耗材'), ('product_variant_ids.stock_move_ids.picking_id.receipt_status', '=', 'receipt')]</field>
<field name="domain">[('product_tag_ids.name', '=', '常用耗材'), ('product_variant_ids.stock_quant_ids.location_id.usage', '=', 'internal')]</field>
<field name="context">{'default_detailed_type': 'product'}</field>
</record>
@@ -22,7 +22,7 @@
<field name="name">固定资产</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('product_tag_ids.name', '=', '固定资产'), ('product_variant_ids.stock_move_ids.picking_id.receipt_status', '=', 'receipt')]</field>
<field name="domain">[('product_tag_ids.name', '=', '固定资产'), ('product_variant_ids.stock_quant_ids.location_id.usage', '=', 'internal')]</field>
<field name="context">{'default_detailed_type': 'product'}</field>
</record>
@@ -30,7 +30,7 @@
<field name="name">日常消耗品</field>
<field name="res_model">product.template</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('product_tag_ids.name', '=', '日常消耗品'), ('product_variant_ids.stock_move_ids.picking_id.receipt_status', '=', 'receipt')]</field>
<field name="domain">[('product_tag_ids.name', '=', '日常消耗品'), ('product_variant_ids.stock_quant_ids.location_id.usage', '=', 'internal')]</field>
<field name="context">{'default_detailed_type': 'product'}</field>
</record>
</data>
@@ -44,7 +44,7 @@
<menuitem id="inventory_asset_product_root" name="产品" sequence="10">
<menuitem id="inventory_product_office_supplies_root" name="办公用品" action="inventory_product_office_supplies_action" sequence="10"/>
<menuitem id="inventory_product_consumables_root" name="常用耗材" action="inventory_product_consumables_action" sequence="20"/>
<menuitem id="inventory_product_fixed_assets_root" name="固定资产" action="inventory_product_fixed_assets_action" sequence="30"/>
<menuitem id="inventory_product_fixed_assets_root" name="固定资产" action="yuthon_stock.action_fixed_assets" sequence="30"/>
<menuitem id="inventory_product_daily_consumables_root" name="日常消耗品" action="inventory_product_daily_consumables_action" sequence="40"/>
</menuitem>
</menuitem>
@@ -58,7 +58,7 @@
<menuitem id="inventory_setting_uom_root" name="计量单位类别" action="uom.product_uom_categ_form_action" sequence="60"/>
<menuitem id="inventory_setting_product_root" name="产品" sequence="70">
<menuitem id="inventory_asset_variant_root" name="产品变体" action="stock.stock_product_normal_action" sequence="10"/>
<menuitem id="inventory_asset_fixed_root" name="固定资产" action="maintenance.hr_equipment_action" sequence="20"/>
<menuitem id="inventory_asset_fixed_root" name="固定资产" action="yuthon_stock.action_fixed_assets" sequence="20"/>
</menuitem>
<menuitem id="inventory_setting_product_tag_root" name="产品模版" action="product.product_tag_action" sequence="80"/>
<menuitem id="inventory_setting_putaway_root" name="上架规则" action="stock.action_putaway_tree" sequence="90"/>