Files
gzth/yuthon_purchase/models/inherit_purchase_order.py
T
2026-06-26 16:10:42 +08:00

420 lines
20 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 -*-
from odoo import api, fields, models, SUPERUSER_ID
import json
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare
class InheritPurchaseOrder(models.Model):
# _inherit = 'purchase.order'
_name = 'purchase.order'
_inherit = ['purchase.order', 'soong.workflow.mixin']
active = fields.Boolean('Active', default=True, tracking=True)
# 重写 picking_type_id,只显示入库类型
picking_type_id = fields.Many2one(
'stock.picking.type',
'Deliver To',
required=True,
domain="[('code', '=', 'incoming')]",
default=lambda self: self.env['stock.picking.type'].search([
('code', '=', 'incoming'),
('company_id', '=', self.env.company.id)
], limit=1)
)
json_display = fields.Char(
string="物品详情",
compute='_compute_json_display',
store=True
)
name_title = fields.Char(string="默认标题", default='采购申请单', readonly=True)
name = fields.Char(string="标题", default=False)
code = fields.Char(string="流水号", default=lambda self: self.env['ir.sequence'].next_by_code('purchase_workflow_approval_code'))
state = fields.Selection(selection_add=[('申请人', '申请人'), ('部门经理', '部门经理'), ('工会主席', '工会主席'),
('协办', '协办'),
('财务', '财务'),
('复核', '复核'),
('分管领导', '分管领导'),
('总经理', '总经理'), ('归档', '归档')], default='申请人')
urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度',
default='normal')
work_end = fields.Boolean(string="结束")
user_id = fields.Many2one('res.users', string="申请人", default=lambda self: self.env.user)
company_id = fields.Many2one(related='user_id.employee_ids.company_id', string="公司")
department_id = fields.Many2one(related='user_id.employee_ids.department_id', string="部门")
document_ids = fields.Many2many('ir.attachment', string="附件")
start_date = fields.Date(string="申请日期", default=fields.Date.today())
category_id = fields.Many2one('product.category', string="采购标的类型")
self_users_ids = fields.Many2many('res.users', string="当前经办人",
default=lambda self: [(6, 0, [self.env.user.id])])
pay_way = fields.Selection([('onetime', '一次性'), ('installment', '分期')], string="付款方式", default='onetime')
reason_text = fields.Text(string="申请事由")
purchase_type = fields.Selection([('fixed_asset', '固定资产采购'), ('sporadic', '零星采购'), ('comprehensive', '综合类')], string="采购类型", default='comprehensive')
is_modify = fields.Boolean(string="可修改")
pay_line_ids = fields.One2many('purchase.order.pay.line', 'order_id', string="付款详情")
is_company = fields.Boolean(string="本公司", default=True, compute='_compute_is_company',search='_search_part_of_company')
# 审批状态字段
purchase_state = fields.Selection([
('draft', '草稿'),
('in_progress', '审批中'),
('done', '完成')
], string="审批状态", compute='_compute_purchase_state', store=True)
# 显示用的办结时间(使用write_date作为办结时间)
display_end_time = fields.Datetime(
string="办结时间",
compute='_compute_display_end_time',
store=True
)
# 从 soong.workflow.record 读取的流程审批状态(用于"确认订单"按钮判断)
workflow_approve_state = fields.Selection([
('start', '草稿'),
('pending', '审批中'),
('end', '结束')
], string='流程审批状态', compute='_compute_workflow_approve_state')
@api.depends('log_ids', 'work_end')
def _compute_workflow_approve_state(self):
"""从关联的 soong.workflow.record 获取 approve_state
# 流程记录通过 res_model='purchase.order' + res_id=当前PO + reclaim_state=False 关联
# 取值:start(草稿)、pending(审批中)、end(结束)
# 只有 end 状态才允许"确认订单"操作
"""
for record in self:
workflow_record = self.env['soong.workflow.record'].search([
('res_model', '=', 'purchase.order'),
('res_id', '=', record.id),
('reclaim_state', '=', False)
], limit=1)
record.workflow_approve_state = workflow_record.approve_state if workflow_record else False
# 入库进度(根据关联入库单的 receipt_status 聚合)
picking_receipt_state = fields.Selection([
('waiting', '待验收'),
('certification', '证明入库'),
('done', '已入库'),
], string="入库状态", compute='_compute_picking_receipt_state', store=False)
@api.depends('log_ids', 'work_end')
def _compute_purchase_state(self):
"""
计算审批状态
- log_ids为空:草稿
- log_ids有值且work_end=False:审批中
- work_end=True:完成
"""
for record in self:
if record.work_end:
record.purchase_state = 'done'
elif record.log_ids:
record.purchase_state = 'in_progress'
else:
record.purchase_state = 'draft'
@api.depends('log_ids', 'write_date')
def _compute_display_end_time(self):
"""
计算显示用的办结时间
逻辑:如果有审批日志,取最后一条日志的时间;否则使用write_date
"""
for record in self:
if record.log_ids:
# 按ID排序,取最后一条日志的时间
sorted_logs = record.log_ids.sorted(lambda l: l.id)
if sorted_logs and hasattr(sorted_logs[-1], 'lase_click_date') and sorted_logs[-1].lase_click_date:
record.display_end_time = sorted_logs[-1].lase_click_date
else:
record.display_end_time = record.write_date
else:
record.display_end_time = record.write_date
@api.depends('picking_ids', 'picking_ids.receipt_status', 'picking_ids.picking_type_code')
def _compute_picking_receipt_state(self):
"""根据关联入库单的 receipt_status 计算入库状态。
只考虑入库类型的调拨(picking_type_code == 'incoming'):
- 任一为 receipt → 已入库
- 否则任一为 certification → 证明入库
- 否则任一为 acceptance → 待验收
- 否则为空
"""
for order in self:
state = False
incoming_pickings = order.picking_ids.filtered(lambda p: p.picking_type_code == 'incoming')
if incoming_pickings:
statuses = set(incoming_pickings.mapped('receipt_status'))
if 'receipt' in statuses:
state = 'done'
elif 'certification' in statuses:
state = 'certification'
elif 'acceptance' in statuses:
state = 'waiting'
order.picking_receipt_state = state
@api.depends_context('uid', 'company')
@api.depends('company_id')
def _compute_is_company(self):
active_company_ids = self.env.companies
for employee in self:
employee.is_company = employee.company_id in active_company_ids
def _search_part_of_company(self, operator, value):
company_ids = self.env.companies.ids
if not value:
operator = '!=' if operator == '=' else '='
if operator == '=':
return [('company_id', 'in', company_ids)]
else:
return [('company_id', 'not in', company_ids)]
def update_state(self):
order_ids = self.env["purchase.order"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for order in order_ids:
order.button_cancel()
@api.onchange('category_id')
def _onchange_category_id(self):
for record in self:
if record.pay_line_ids:
for line in record.pay_line_ids:
line.category_id = record.category_id.id
def _create_picking(self):
"""重写_create_picking方法,移除产品类型和state=='purchase'的限制,
但保留入库单已存在时不重复创建移动行的保护逻辑"""
StockPicking = self.env['stock.picking']
for order in self:
order = order.with_company(order.company_id)
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
if not pickings:
# 没有入库单,创建新的
res = order._prepare_picking()
picking = StockPicking.with_user(SUPERUSER_ID).create(res)
pickings = picking
moves = order.order_line._create_stock_moves(picking)
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
seq = 0
for move in sorted(moves, key=lambda move: move.date):
seq += 5
move.sequence = seq
moves._action_assign()
forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves)
(pickings | forward_pickings).action_confirm()
picking.message_post_with_source(
'mail.message_origin_link',
render_values={'self': picking, 'origin': order},
subtype_xmlid='mail.mt_note',
)
# 入库单已存在时不再重复追加移动行
return True
def button_confirm(self):
"""重写button_confirm方法:用流程记录的 approve_state 判断,而非原生 ['draft','sent']
# 只有流程状态为 end(审批结束)时才执行确认
# 执行内容与原生命 button_confirm 一致:校验分析账户、添加供应商、调用 button_approve
# button_approve 内部会调用 _create_picking 生成入库单
"""
for order in self:
# 查找关联的流程记录,检查审批是否结束
workflow_record = self.env['soong.workflow.record'].search([
('res_model', '=', 'purchase.order'),
('res_id', '=', order.id),
('reclaim_state', '=', False)
], limit=1)
# 流程未结束则跳过,不生成入库单
if not workflow_record or workflow_record.approve_state != 'end':
continue
# 以下逻辑与原生命 button_confirm 一致
order.order_line._validate_analytic_distribution()
order._add_supplier_to_product()
if order._approval_allowed():
order.button_approve()
else:
order.write({'state': 'to approve'})
if order.partner_id not in order.message_partner_ids:
order.message_subscribe([order.partner_id.id])
return True
def end_approval(self):
self.button_confirm()
self.state = 'purchase'
# button_confirm → button_approve 已经调用过 _create_picking,不需要再调
self.write({'work_end': True})
def action_sync_to_picking(self):
"""将当前明细行同步到入库单
# — 重建策略:取消并删除入库单中所有未完成的 move,再按当前明细行重新生成
# — 这样避免了增量创建时 push rule 生成独立新入库单的问题
"""
self.ensure_one()
# 取得订单的入库单(非取消、非完成,最新的优先)
picking = self.picking_ids.filtered(
lambda p: p.picking_type_code == 'incoming' and p.state not in ('done', 'cancel')
)
if not picking:
raise UserError('没有可同步的入库单!请先确认订单生成入库单。')
picking = picking.sorted(lambda p: p.id, reverse=True)[0] # 取 ID 最大的(最新)
current_lines = self.order_line
# —— 1. 删除所有非 done 状态的 move(含 cancel 孤儿,不含已收货的 done) ——
moves_to_remove = picking.move_ids.filtered(lambda m: m.state != 'done')
if moves_to_remove:
# 先 cancel 再删:cancel 状态的 move 再调 _action_cancel 不会有副作用
moves_to_remove._action_cancel()
moves_to_remove.unlink()
# —— 2. 按当前明细行重新生成全部 move(复用原生 _create_picking 后半段逻辑) ——
if current_lines:
new_moves = current_lines._create_stock_moves(picking)
new_moves = new_moves.filtered(lambda m: m.state not in ('done', 'cancel'))._action_confirm()
# 设置排序序号
seq = 0
for move in new_moves.sorted(lambda m: m.date):
seq += 5
move.sequence = seq
new_moves._action_assign()
# push rule 可能把某些 move 拆分到新入库单,全部移回原入库单
forward_pickings = self.env['stock.picking']._get_impacted_pickings(new_moves) - picking
for fp in forward_pickings:
fp.move_ids.write({'picking_id': picking.id}) # 移回原入库单
# 取消空的分流入库单
forward_pickings.filtered(lambda p: not p.move_ids).action_cancel()
(picking | forward_pickings).action_confirm()
return True
@api.depends(
'order_line.product_id',
'order_line.product_qty',
'order_line.product_uom',
'order_line.price_unit',
'order_line.price_subtotal'
)
def _compute_json_display(self):
for record in self:
json_data = {
"template": "json_widget_purchase.table_purchase",
"title": f"{record.name}的明细数据",
"lines": [
{
'product_id': line.product_id.name or '',
'product_qty': line.product_uom_qty or '',
'product_uom': line.product_uom.name or '',
'price_unit': line.price_unit or '',
'price_subtotal': line.price_subtotal or ''
}
for idx, line in enumerate(record.order_line)
]
}
record.json_display = json.dumps(json_data, ensure_ascii=False)
def action_refresh_json_display(self):
"""手动刷新json_display字段"""
for record in self:
record._compute_json_display()
return True
def get_no_read_count(self):
domain = [
# ('company_id', '=', self.env.user.company_id.id),
('state','=','draft')
]
return self.search_count(domain)
def action_view_picking(self):
# 注释掉创建人限制,允许所有人查看收货单
# if self.user_id == self.env.user:
# raise UserError("您不能接收自己创建的采购订单产品")
return super(InheritPurchaseOrder, self).action_view_picking()
def action_print_purchase_order(self):
"""打印采购审批单"""
self.ensure_one()
report_ref = self.env.ref('yuthon_purchase.action_purchase_order_print_report')
return report_ref.sudo().report_action(self)
class InheritPurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
category_id = fields.Many2one('product.category', string="产品分类")
brand_spec = fields.Char(string="品牌/规格")
category_ids = fields.Many2many('product.category', string="产品分类", compute='_compute_category_id')
purchase_type = fields.Selection(related='order_id.purchase_type', string="采购类型")
def _unlink_except_purchase_or_done(self):
"""重写原生限制:只允许 state='purchase' 时删行,done 仍拦截
# — 用户删行后通过"同步到入库单"按钮清理对应的 stock.move
"""
pass # 不做任何拦截,用户自行负责,按钮里会清理孤儿 move
def _create_or_update_picking(self):
"""重写为空:禁止产品新增/修改时自动同步入库单
# 原因:_create_stock_moves → stock.move.create() 内置自动确认 → 触发 push rule → 生成新入库单
# 所有同步统一由"同步到入库单"按钮处理
"""
pass
@api.model
def create(self, vals_list):
"""重写:禁用添加产品时自动同步入库单,统一由按钮处理"""
lines = super().create(vals_list)
# 不调 _create_or_update_picking:stock.move.create() 内置自动确认会触发 push rule 生成新入库单
# 所有同步逻辑统一走 action_sync_to_picking 按钮
return lines
@api.depends('category_id')
def _compute_category_id(self):
for record in self:
matching_categories = self.env['product.category']
if record.category_id:
matching_categories = self.env['product.category'].search([
'|',
('id', '=', record.category_id.id),
'|',
('parent_id', '=', record.category_id.id),
('parent_id.parent_id', '=', record.category_id.id),
])
record.category_ids = [(6, 0, matching_categories.ids)] if matching_categories else [(5,)]
@api.onchange('product_id')
def _onchange_product_id_check_tag(self):
"""检查产品标签是否符合采购类型要求"""
if self.product_id and self.order_id.purchase_type:
tag_names = self.product_id.product_tag_ids.mapped('name')
if self.order_id.purchase_type == 'fixed_asset':
# 固定资产采购:只能选择有固定资产标签的产品
if '固定资产' not in tag_names:
warning = {
'title': '产品不符合要求',
'message': '固定资产采购只能选择产品标签为“固定资产”的产品!'
}
return {'warning': warning, 'value': {'product_id': False}}
elif self.order_id.purchase_type == 'sporadic':
# 零星采购:只能选择没有固定资产标签的产品
if '固定资产' in tag_names:
warning = {
'title': '产品不符合要求',
'message': '零星采购不能选择产品标签为“固定资产”的产品!'
}
return {'warning': warning, 'value': {'product_id': False}}
@api.onchange('order_id')
def _onchange_order_id_set_product_domain(self):
"""根据采购类型设置产品domain"""
if self.order_id:
if self.order_id.purchase_type == 'fixed_asset':
# 固定资产采购:只能选择有固定资产标签的产品
return {'domain': {'product_id': [('product_tag_ids.name', 'in', ['固定资产'])]}}
elif self.order_id.purchase_type == 'sporadic':
# 零星采购:只能选择没有固定资产标签的产品
return {'domain': {'product_id': [('product_tag_ids.name', 'not in', ['固定资产'])]}}
# 综合类:不设置domain,显示所有产品