初始数,盘点
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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">['&', ('picking_type_code', '=', 'incoming'), '|', '|', ('create_uid', '=', uid), ('acceptance_user_id', '=', uid), ('certifier_user_id', '=', uid)]</field>
|
||||
<field name="domain">['&', ('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>
|
||||
|
||||
@@ -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<0" decoration-success="difference_qty>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<0" decoration-success="difference_qty>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<0" decoration-success="difference_qty>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>
|
||||
@@ -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,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"/>
|
||||
|
||||
Reference in New Issue
Block a user