Files
gzth/yuthon_stock/models/yuthon_inventory_task.py
T
2026-07-03 19:05:50 +08:00

355 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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