420 lines
20 KiB
Python
420 lines
20 KiB
Python
# -*- 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,显示所有产品
|
||
|