diff --git a/yuthon_catering/controllers/decorators.py b/yuthon_catering/controllers/decorators.py index 41e336993..c84fc4dfa 100644 --- a/yuthon_catering/controllers/decorators.py +++ b/yuthon_catering/controllers/decorators.py @@ -18,6 +18,6 @@ def exception_handle(func): return json.dumps({'data': '', 'code': 500, 'message': str(e)}) except Exception as e: _logger.exception(e) - return json.dumps({'data': '', 'code': 500, 'message': str(e)}) + return json.dumps({'data': '', 'code': 500, 'message': f'Server Error'}) return wrapper diff --git a/yuthon_catering/controllers/restaurant.py b/yuthon_catering/controllers/restaurant.py index 9fb187856..ba49ca1b1 100644 --- a/yuthon_catering/controllers/restaurant.py +++ b/yuthon_catering/controllers/restaurant.py @@ -128,3 +128,26 @@ class RestaurantController(Controller): } return json.dumps({'data': catering_data, 'code': 200, 'message': ''}) return json.dumps({'data': '', 'code': 200, 'message': '没有查询到数据'}) + + @route('/phone/restaurant/get_catering_statistics', methods=['POST'], type='http', auth="none", csrf=False, cors='*') + @exception_handle + def get_catering_statistics(self, **kwargs): + menu_date = kwargs.get('menu_date', date.today()) + + if not menu_date: + return json.dumps({'data': '', 'code': 500, 'message': '参数错误'}) + + caterings = request.env['yuthon.catering'].sudo().search([ + ('date', '=', menu_date), + ]) + catering_data = { + 'breakfast': 0, + 'lunch': 0, + } + if caterings: + catering_data = { + 'breakfast': len(caterings.filtered(lambda x: x.breakfast)), + 'lunch': len(caterings.filtered(lambda x: x.lunch)), + } + return json.dumps({'data': catering_data, 'code': 200, 'message': ''}) + return json.dumps({'data': catering_data, 'code': 200, 'message': ''}) diff --git a/yuthon_issue/models/issue_library.py b/yuthon_issue/models/issue_library.py index 81c121293..7ed37f3b4 100644 --- a/yuthon_issue/models/issue_library.py +++ b/yuthon_issue/models/issue_library.py @@ -13,7 +13,7 @@ class IssueLibrary(models.Model): _rec_name = 'name' name = fields.Char(string='议题名称') - name_text = fields.Text(string='议题名称', compute='_compute_name', readonly=False) + name_text = fields.Text(string='议题名称', compute='_compute_name', readonly=False, store=True) depart_opinions = fields.Text(string='部门意见') topic_content = fields.Text(string='事项背景') will_based = fields.Text(string='上会依据') @@ -840,6 +840,14 @@ class IssueLibrary(models.Model): for record in self: record.name_text = record.name + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + args = args or [] + domain = [] + if name: + domain = ['|', '|', ('name', operator, name), ('code', operator, name), ('name_text', operator, name)] + return self.search(domain + args, limit=limit).name_get() + @api.onchange('issue_library_room_line_ids') def _onchange_issue_library_room_line_ids(self): for record in self.issue_library_room_line_ids: diff --git a/yuthon_issue/views/issue_library_views.xml b/yuthon_issue/views/issue_library_views.xml index 6f297c134..065419a02 100644 --- a/yuthon_issue/views/issue_library_views.xml +++ b/yuthon_issue/views/issue_library_views.xml @@ -331,6 +331,8 @@ + + @@ -343,6 +345,8 @@ + + diff --git a/yuthon_maintain/models/inherit_maintenance_equipment.py b/yuthon_maintain/models/inherit_maintenance_equipment.py index 2174d7fbc..d0b817a5d 100644 --- a/yuthon_maintain/models/inherit_maintenance_equipment.py +++ b/yuthon_maintain/models/inherit_maintenance_equipment.py @@ -39,6 +39,7 @@ class InheritMaintenanceEquipment(models.Model): department_id1 = fields.Many2one('hr.department', string="部门", related='employee_id.department_id') is_jtbb = fields.Boolean(compute='_compute_company_id') mainten_state = fields.Selection([('ing', '使用中'), ('finish', '暂停')], string="设备状态") + borrow_state = fields.Selection([('available', '可借'), ('borrowed', '已借出')], string="借用状态", default='available') equipment_number = fields.Char(string="数量") equipment_unit = fields.Many2one('uom.uom', string="单位") purchase_date = fields.Date(string="购入日期") diff --git a/yuthon_maintain/views/maintenance_equipment_views.xml b/yuthon_maintain/views/maintenance_equipment_views.xml index f10362509..53f26d530 100644 --- a/yuthon_maintain/views/maintenance_equipment_views.xml +++ b/yuthon_maintain/views/maintenance_equipment_views.xml @@ -90,6 +90,7 @@ + diff --git a/yuthon_property/wizard/batch_invoice_export_wizard.py b/yuthon_property/wizard/batch_invoice_export_wizard.py index a11045647..d268a6b40 100644 --- a/yuthon_property/wizard/batch_invoice_export_wizard.py +++ b/yuthon_property/wizard/batch_invoice_export_wizard.py @@ -66,11 +66,13 @@ class BatchInvoiceExportWizard(models.TransientModel): sheet['B2'] = f'{title_year}年{title_month}月' - # 统计最常见公司(用于C2) + # 统计最常见公司(用于C2,取合同出租方公司简称) company_list = [] for record in receivable_records: - if record.contract_id and record.contract_id.company_id: - company_list.append(record.contract_id.company_id.name) + if record.contract_id and record.contract_id.lessor_id: + referred = record.contract_id.lessor_id.referred + if referred: + company_list.append(referred) if company_list: from collections import Counter @@ -80,6 +82,19 @@ class BatchInvoiceExportWizard(models.TransientModel): sheet['C2'] = most_common_company + # 统计最常见权属类型(用于文件名) + tenure_type_map = {'self': '自有', 'agency': '代管'} + tenure_list = [] + for record in receivable_records: + if record.contract_id and record.contract_id.tenure_type: + tenure_list.append(record.contract_id.tenure_type) + if tenure_list: + from collections import Counter + most_common_tenure = Counter(tenure_list).most_common(1)[0][0] + tenure_type_label = tenure_type_map.get(most_common_tenure, most_common_tenure) + else: + tenure_type_label = '' + # 从第4行开始写入数据 row = 4 template_row_idx = 4 @@ -182,7 +197,7 @@ class BatchInvoiceExportWizard(models.TransientModel): file_data = base64.b64encode(output.read()).decode('utf-8') attachment = self.env['ir.attachment'].create({ - 'name': f'批量开票_{title_year}年{title_month}月.xlsx', + 'name': f'{most_common_company}_{tenure_type_label}_{title_year}年{title_month}月批量开票.xlsx', 'type': 'binary', 'datas': file_data, 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', diff --git a/yuthon_purchase/__init__.py b/yuthon_purchase/__init__.py index 9a7e03ede..bf588bc8b 100644 --- a/yuthon_purchase/__init__.py +++ b/yuthon_purchase/__init__.py @@ -1 +1,2 @@ -from . import models \ No newline at end of file +from . import models +from . import report diff --git a/yuthon_purchase/__manifest__.py b/yuthon_purchase/__manifest__.py index 0241174be..57283d0c4 100644 --- a/yuthon_purchase/__manifest__.py +++ b/yuthon_purchase/__manifest__.py @@ -6,7 +6,7 @@ 'category': 'zhou', 'version': '0.1', 'license': 'LGPL-3', - 'depends': ['base', 'soong_workflow', 'hr', 'product', 'documents', 'purchase', 'purchase_stock', 'sun_badge_menu', 'website_sale'], + 'depends': ['base', 'soong_workflow', 'hr', 'product', 'documents', 'purchase', 'purchase_stock', 'sun_badge_menu', 'website_sale', 'yuthon_stock'], 'data': [ 'security/ir.model.access.csv', 'security/company_rule.xml', diff --git a/yuthon_purchase/report/__init__.py b/yuthon_purchase/report/__init__.py new file mode 100644 index 000000000..05d7e4ee8 --- /dev/null +++ b/yuthon_purchase/report/__init__.py @@ -0,0 +1 @@ +from . import purchase_order_print_report diff --git a/yuthon_purchase/report/purchase_order_print_report.py b/yuthon_purchase/report/purchase_order_print_report.py new file mode 100644 index 000000000..b2a76bb6e --- /dev/null +++ b/yuthon_purchase/report/purchase_order_print_report.py @@ -0,0 +1,15 @@ +from odoo import models + + +class PurchaseOrderPrintReport(models.AbstractModel): + _name = 'report.yuthon_purchase.purchase_order_print_template' + _description = '采购审批单' + + def _get_report_values(self, docids, data=None): + docs = self.env['purchase.order'].sudo().browse(docids) + + docargs = { + 'docs': docs, + } + + return docargs diff --git a/yuthon_purchase/report/purchase_order_print_templates.xml b/yuthon_purchase/report/purchase_order_print_templates.xml index b66a08c8d..5e2f1b345 100644 --- a/yuthon_purchase/report/purchase_order_print_templates.xml +++ b/yuthon_purchase/report/purchase_order_print_templates.xml @@ -21,7 +21,6 @@ .text-center { text-align: center !important; } .text-right { text-align: right !important; } -
采购审批单
@@ -83,7 +82,6 @@ 税项 金额 - diff --git a/yuthon_stock/__manifest__.py b/yuthon_stock/__manifest__.py index 6dfce5be3..f44f479b1 100644 --- a/yuthon_stock/__manifest__.py +++ b/yuthon_stock/__manifest__.py @@ -8,12 +8,17 @@ 'license': 'LGPL-3', 'depends': ['base', 'stock', 'purchase_stock', 'documents', 'maintenance', 'funenc_json_widget'], 'data': [ + 'data/sequence.xml', 'security/ir.model.access.csv', 'security/company_rule.xml', 'wizard/stock_picking_transfer_wizard_views.xml', + 'wizard/putaway_rule_batch_update_wizard_views.xml', + 'wizard/stock_picking_submit_wizard_views.xml', + 'wizard/stock_picking_return_wizard_views.xml', 'views/menu.xml', 'views/inherit_stock_picking.xml', 'views/Inherit_stock_picking_type_views.xml', + 'views/inherit_product_template_views.xml', ], 'installable': True, 'application': True, diff --git a/yuthon_stock/data/sequence.xml b/yuthon_stock/data/sequence.xml new file mode 100644 index 000000000..aaa1da864 --- /dev/null +++ b/yuthon_stock/data/sequence.xml @@ -0,0 +1,14 @@ + + + + 借用领用审批流水号 + borrow.workflow.sequence + JY%(y)s%(month)s%(day)s + 4 + + + + + + + diff --git a/yuthon_stock/models/__init__.py b/yuthon_stock/models/__init__.py index fd8656311..28f787203 100644 --- a/yuthon_stock/models/__init__.py +++ b/yuthon_stock/models/__init__.py @@ -2,4 +2,5 @@ from . import inherit_stock_picking from . import Inherit_stock_picking_type from . import inherit_stock_location -from . import inherit_stock_move \ No newline at end of file +from . import inherit_stock_move +from . import ir_model_patch diff --git a/yuthon_stock/models/inherit_product_template.py b/yuthon_stock/models/inherit_product_template.py new file mode 100644 index 000000000..e03ecd9a8 --- /dev/null +++ b/yuthon_stock/models/inherit_product_template.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from odoo import models diff --git a/yuthon_stock/models/inherit_stock_move.py b/yuthon_stock/models/inherit_stock_move.py index 8561887ed..bf8e139c4 100644 --- a/yuthon_stock/models/inherit_stock_move.py +++ b/yuthon_stock/models/inherit_stock_move.py @@ -27,4 +27,62 @@ class ProductProduct(models.Model): class StockMove(models.Model): _inherit = 'stock.move' + product_location_ids_display = fields.Char( + string='所在仓库', + compute='_compute_product_location_ids_display', + store=False, + ) + available_location_ids = fields.Many2many( + 'stock.location', + string='可选位置', + compute='_compute_available_location_ids', + store=False, + ) + borrow_type = fields.Selection( + [('borrow', '借用'), ('requisition', '领用')], + string='借用/领用', + compute='_compute_borrow_type', + store=False, + ) + @api.depends('product_id', 'product_id.product_tag_ids') + def _compute_borrow_type(self): + """根据产品标签判断是借用还是领用""" + for move in self: + if move.product_id: + tag_names = move.product_id.product_tag_ids.mapped('name') + if '固定资产' in tag_names: + move.borrow_type = 'borrow' + else: + move.borrow_type = 'requisition' + else: + move.borrow_type = False + + @api.depends('product_id') + def _compute_product_location_ids_display(self): + """根据产品查找所有有库存的仓库位置,拼接显示""" + for move in self: + if move.product_id: + quants = self.env['stock.quant'].search([ + ('product_id', '=', move.product_id.id), + ('quantity', '>', 0), + ('location_id.usage', '=', 'internal'), + ]) + locations = quants.mapped('location_id') + move.product_location_ids_display = ' / '.join(locations.mapped('complete_name')) if locations else '' + else: + move.product_location_ids_display = '' + + @api.depends('product_id') + def _compute_available_location_ids(self): + """计算该产品有库存的位置,用于 location_id 的 domain 过滤""" + for move in self: + if move.product_id: + quants = self.env['stock.quant'].search([ + ('product_id', '=', move.product_id.id), + ('quantity', '>', 0), + ('location_id.usage', '=', 'internal'), + ]) + move.available_location_ids = quants.mapped('location_id') + else: + move.available_location_ids = self.env['stock.location'] diff --git a/yuthon_stock/models/inherit_stock_picking.py b/yuthon_stock/models/inherit_stock_picking.py index 04f357e4d..7fa27d59b 100644 --- a/yuthon_stock/models/inherit_stock_picking.py +++ b/yuthon_stock/models/inherit_stock_picking.py @@ -28,6 +28,23 @@ class StockPicking(models.Model): acceptance_user_id = fields.Many2one('res.users', string="指定验收人") certifier_user_id = fields.Many2one('res.users', string="指定证明人") is_admin = fields.Boolean(string='是否管理员', compute='_compute_is_admin') + approval_state = fields.Selection([ + ('draft', '草稿'), + ('pending', '待审批'), + ('approved', '已批准'), + ('refused', '已拒绝'), + ], string='审批状态', default='draft') + approver_id = fields.Many2one('res.users', string='审批人') + borrower_id = fields.Many2one('res.users', string='借用人') + return_state = fields.Selection([ + ('none', '未申请'), + ('pending', '待审批'), + ('approved', '已归还'), + ('refused', '已拒绝'), + ], string='归还状态', default='none') + return_approver_id = fields.Many2one('res.users', string='归还审批人') + borrow_title = fields.Char(string='标题') + borrow_code = fields.Char(string='流水号', compute='_compute_borrow_code', store=False) user_id = fields.Many2one('res.users', string="采购人员", compute="_compute_origin", store=True) receipt_status = fields.Selection([ ('acceptance', '验收'), @@ -155,22 +172,43 @@ class StockPicking(models.Model): self.certifier_id = current_user.id self.certification_date = odoo_fields.Datetime.now() - @api.depends('location_id', 'picking_type_code') + @api.depends('picking_type_code', 'move_ids_without_package') def _compute_available_product_ids(self): - """根据仓库和作业类型计算可用产品列表""" + """根据作业类型计算可用产品列表""" for record in self: - # 如果是入库单,返回所有产品(不限制) if record.picking_type_code == 'incoming': record.available_product_ids = self.env['product.product'].search([]) - # 如果是借用/领用等,只返回该仓库中有库存的产品 - elif record.location_id: + elif record.picking_type_code == 'internal': + # 当前单据已选产品(借出后库存可能为0,仍需可见) + existing_products = record.move_ids_without_package.mapped('product_id') + # 普通产品:有库存 quants = self.env['stock.quant'].search([ - ('location_id', '=', record.location_id.id), - ('quantity', '>', 0) # 只查询有库存的 + ('quantity', '>', 0), + ('location_id.usage', '=', 'internal'), + ]) + stock_products = quants.mapped('product_id') + # 固定资产产品:有可用设备 + available_equip = self.env['maintenance.equipment'].search([ + ('borrow_state', '=', 'available'), + ]) + equip_product_names = set() + for equip in available_equip: + base_name = equip.name.rsplit('-', 1)[0] if '-' in equip.name else equip.name + equip_product_names.add(base_name) + if equip_product_names: + fixed_asset_products = self.env['product.product'].search([ + ('name', 'in', list(equip_product_names)), + ('product_tag_ids.name', 'in', ['固定资产']), + ]) + else: + fixed_asset_products = self.env['product.product'] + record.available_product_ids = stock_products | fixed_asset_products | existing_products + else: + quants = self.env['stock.quant'].search([ + ('quantity', '>', 0), + ('location_id.usage', '=', 'internal'), ]) record.available_product_ids = quants.mapped('product_id') - else: - record.available_product_ids = False @api.depends('picking_type_code') @@ -215,6 +253,55 @@ class StockPicking(models.Model): }) self.location_dest_id = location_dest_id + def _get_or_create_dept_location(self, department, company): + """根据部门查找或自动创建内部位置,返回 location 记录""" + import logging + _logger = logging.getLogger(__name__) + # 优先在当前公司下按名称+内部位置查找 + location = self.env['stock.location'].search([ + ('name', '=', department.name), + ('usage', '=', 'internal'), + ('company_id', '=', company.id), + ], limit=1) + _logger.info('LOCATION DEBUG1: dept=%s, company=%s(%s), found=%s', + department.name, company.name, company.id, + location.complete_name if location else 'None') + # 任何情况都找不到则创建一个属于当前公司的新位置 + if not location: + parent_location = self.env['stock.location'].search([ + ('complete_name', '=', company.name), + ], limit=1) + location = self.env['stock.location'].create({ + 'name': department.name, + 'location_id': parent_location.id if parent_location else False, + 'usage': 'internal', + 'company_id': company.id, + }) + _logger.info('LOCATION DEBUG3: created=%s', location.complete_name) + return location + + @api.depends('picking_type_id', 'partner_id') + def _compute_location_id(self): + """\u91cd\u5199\u8ba1\u7b97\u4f4d\u7f6e\uff1ainternal \u7c7b\u578b\u65f6\u81ea\u52a8\u7528\u5f53\u524d\u7528\u6237\u90e8\u95e8\u4f4d\u7f6e\u4f5c\u4e3a\u76ee\u7684\u4f4d\u7f6e""" + super()._compute_location_id() + for picking in self: + if picking.state != 'draft' or picking.return_id: + continue + if picking.picking_type_code != 'internal': + continue + current_user = self.env.user + employee = current_user.employee_id + if not employee or not employee.department_id: + continue + company = current_user.company_id + location = picking._get_or_create_dept_location(employee.department_id, company) + if location: + picking.location_dest_id = location + + @api.onchange('picking_type_id') + def _onchange_picking_type_auto_dest(self): + pass # 逻辑已移至 _compute_location_id 重写 + @api.depends( 'move_ids_without_package.product_id', 'move_ids_without_package.product_uom_qty', @@ -251,6 +338,9 @@ class StockPicking(models.Model): def button_validate(self): """重写确认入库方法,改名为证明按鈕""" for record in self: + # 借用/领用单必须已审批通过才能完成 + if record.picking_type_code == 'internal' and record.approval_state != 'approved': + raise UserError('借用/领用单必须审批通过后才能完成') if record.receipt_status == 'certification': # 如果已指定证明人,检查当前用户是否是指定证明人 if record.certifier_user_id and record.certifier_user_id.id != self.env.user.id: @@ -281,7 +371,263 @@ class StockPicking(models.Model): 'assign_date': False, }) return super(StockPicking, self).button_validate() + + def _compute_borrow_code(self): + for rec in self: + wf = self.env['soong.workflow.record'].search([ + ('res_model', '=', 'stock.picking'), + ('res_id', '=', rec.id), + ('reclaim_state', '=', False), + ], limit=1) + rec.borrow_code = wf.code if wf else '' + def _get_or_create_workflow_record(self): + """获取或创建当前借用/领用单对应的 soong.workflow.record""" + return self.env['soong.workflow.record'].search([ + ('res_model', '=', 'stock.picking'), + ('res_id', '=', self.id), + ('reclaim_state', '=', False), + ], limit=1) + + def action_submit(self): + """提交审批:立即扣减库存,审批人后续只影响状态""" + for rec in self: + if rec.picking_type_code != 'internal': + raise UserError('只有借用/领用单才能提交审批') + if not rec.approver_id: + raise UserError('请先设置审批人') + # 立即完成调拨,扣减库存 + if rec.state == 'draft': + rec.action_confirm() + if rec.state in ('confirmed', 'waiting'): + rec.action_assign() + if rec.state == 'assigned': + # 临时设为 approved 以绕过 button_validate 拦截,完成后改回 pending + rec.approval_state = 'approved' + rec.button_validate() + rec.approval_state = 'pending' + # 固定资产:标记设备为已借出 + if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''): + for move in rec.move_ids_without_package: + product = move.product_id + if '固定资产' not in product.product_tag_ids.mapped('name'): + continue + qty = int(move.product_uom_qty) + if qty <= 0: + continue + available_equipments = self.env['maintenance.equipment'].search([ + ('name', 'like', product.name + '-'), + ('borrow_state', '=', 'available'), + ], order='name asc', limit=qty) + if len(available_equipments) < qty: + raise UserError( + f'产品「{product.name}」可用设备不足,' + f'需要 {qty} 台,当前只有 {len(available_equipments)} 台可用' + ) + available_equipments.write({'borrow_state': 'borrowed'}) + # 创建或更新 soong.workflow.record,只把审批人放入 users_ids(审批人待办) + current_user = self.env.user + employee = current_user.employee_id + department = employee.department_id if employee else self.env['hr.department'] + if not department: + department = rec.department_id + approver_employee = rec.approver_id.employee_id if rec.approver_id else self.env['hr.employee'] + wf_name = rec.borrow_title or rec.name + existing = rec._get_or_create_workflow_record() + if existing: + existing.write({ + 'name': wf_name, + 'users_ids': [(6, 0, [rec.approver_id.id, current_user.id])], + 'employee_ids': [(6, 0, approver_employee.ids)], + 'done_by': current_user.id, + 'approve_state': 'pending', + 'soong_state': 'to_do', + 'reclaim_state': False, + }) + else: + self.env['soong.workflow.record'].sudo().create({ + 'name': wf_name, + 'code': self.env['ir.sequence'].sudo().next_by_code('borrow.workflow.sequence'), + 'res_model': 'stock.picking', + 'res_id': rec.id, + 'users_ids': [(6, 0, [rec.approver_id.id, current_user.id])], + 'employee_ids': [(6, 0, approver_employee.ids)], + 'done_by': current_user.id, + 'approve_state': 'pending', + 'soong_state': 'to_do', + 'department_id': department.id if department else False, + 'workflow_id': 239, + }) + # 申请人提交时立即创建 log:申请人已办(2),审批人待办(0) + self.env['soong.workflow.log'].sudo().create([ + { + 'model': 'stock.picking', + 'res_id': rec.id, + 'done_by': current_user.id, + 'approval_state': '2', + 'lase_click_date': fields.Datetime.now(), + 'comment': '已提交审批', + }, + { + 'model': 'stock.picking', + 'res_id': rec.id, + 'done_by': rec.approver_id.id, + 'approval_state': '0', + 'lase_click_date': fields.Datetime.now(), + 'comment': '待审批', + }, + ]) + + def action_approve(self): + """审批通过:仅更新审批状态,库存已在提交时扣减""" + for rec in self: + if rec.approver_id and rec.approver_id.id != self.env.user.id: + raise UserError('只有指定的审批人才能批准') + rec.approval_state = 'approved' + # 更新 workflow record 为已办(结束状态) + wf_record = rec._get_or_create_workflow_record() + if wf_record: + wf_record.write({ + 'approve_state': 'end', + 'soong_state': 'done', + }) + # 将审批人待办 log 更新为已办结(2) + approver_logs = self.env['soong.workflow.log'].search([ + ('model', '=', 'stock.picking'), + ('res_id', '=', rec.id), + ('done_by', '=', self.env.user.id), + ('approval_state', '=', '0'), + ]) + if approver_logs: + approver_logs.write({'approval_state': '2', 'comment': '批准', 'lase_click_date': fields.Datetime.now()}) + else: + self.env['soong.workflow.log'].sudo().create({ + 'model': 'stock.picking', + 'res_id': rec.id, + 'done_by': self.env.user.id, + 'approval_state': '2', + 'lase_click_date': fields.Datetime.now(), + 'comment': '批准', + }) + + def action_refuse(self): + """审批拒绝:创建反向调拨还原库存""" + for rec in self: + if rec.approver_id and rec.approver_id.id != self.env.user.id: + raise UserError('只有指定的审批人才能拒绝') + if rec.state != 'done': + raise UserError('当前单据状态异常,无法拒绝') + # 创建反向调拨单还原库存 + reverse_vals = { + 'picking_type_id': rec.picking_type_id.id, + 'location_id': rec.location_dest_id.id, + 'location_dest_id': rec.location_id.id, + 'origin': rec.name + '(拒绝还原)', + 'approval_state': 'approved', + 'move_ids': [(0, 0, { + 'name': move.name, + 'product_id': move.product_id.id, + 'product_uom_qty': move.quantity or move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'location_id': rec.location_dest_id.id, + 'location_dest_id': rec.location_id.id, + }) for move in rec.move_ids if move.state == 'done'], + } + reverse_picking = self.env['stock.picking'].create(reverse_vals) + reverse_picking.action_confirm() + reverse_picking.action_assign() + if reverse_picking.state == 'assigned': + reverse_picking.button_validate() + rec.approval_state = 'refused' + # 固定资产:恢复设备为可用 + if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''): + for move in rec.move_ids: + product = move.product_id + if '固定资产' not in product.product_tag_ids.mapped('name'): + continue + qty = int(move.quantity or move.product_uom_qty) + borrowed_equip = self.env['maintenance.equipment'].search([ + ('name', 'like', product.name + '-'), + ('borrow_state', '=', 'borrowed'), + ], order='name asc', limit=qty) + borrowed_equip.write({'borrow_state': 'available'}) + # 更新 workflow record 为已办(结束状态) + wf_record = rec._get_or_create_workflow_record() + if wf_record: + wf_record.write({ + 'approve_state': 'end', + 'soong_state': 'done', + }) + # 将审批人待办 log 更新为已办结(2) + approver_logs = self.env['soong.workflow.log'].search([ + ('model', '=', 'stock.picking'), + ('res_id', '=', rec.id), + ('done_by', '=', self.env.user.id), + ('approval_state', '=', '0'), + ]) + if approver_logs: + approver_logs.write({'approval_state': '2', 'comment': '拒绝', 'lase_click_date': fields.Datetime.now()}) + else: + self.env['soong.workflow.log'].sudo().create({ + 'model': 'stock.picking', + 'res_id': rec.id, + 'done_by': self.env.user.id, + 'approval_state': '2', + 'lase_click_date': fields.Datetime.now(), + 'comment': '拒绝', + }) + + def action_approve_return(self): + """归还审批通过:创建反向调拨单并完成""" + for rec in self: + if rec.return_approver_id and rec.return_approver_id.id != self.env.user.id: + raise UserError('只有指定的归还审批人才能批准归还') + if rec.return_state != 'pending': + raise UserError('当前状态不允许批准归还') + # 创建反向调拨单(目标位置 → 源位置) + return_picking_vals = { + 'picking_type_id': rec.picking_type_id.id, + 'location_id': rec.location_dest_id.id, + 'location_dest_id': rec.location_id.id, + 'origin': rec.name + '(归还)', + 'approval_state': 'approved', + 'move_ids': [(0, 0, { + 'name': move.name, + 'product_id': move.product_id.id, + 'product_uom_qty': move.quantity or move.product_uom_qty, + 'product_uom': move.product_uom.id, + 'location_id': rec.location_dest_id.id, + 'location_dest_id': rec.location_id.id, + }) for move in rec.move_ids if move.state == 'done'], + } + return_picking = self.env['stock.picking'].create(return_picking_vals) + return_picking.action_confirm() + return_picking.action_assign() + if return_picking.state == 'assigned': + return_picking.button_validate() + # 更新归还状态及固定资产设备状态 + rec.return_state = 'approved' + if rec.picking_type_id and '借用' in (rec.picking_type_id.name or ''): + for move in rec.move_ids: + product = move.product_id + if '固定资产' not in product.product_tag_ids.mapped('name'): + continue + qty = int(move.quantity or move.product_uom_qty) + borrowed_equipments = self.env['maintenance.equipment'].search([ + ('name', 'like', product.name + '-'), + ('borrow_state', '=', 'borrowed'), + ], order='name asc', limit=qty) + borrowed_equipments.write({'borrow_state': 'available'}) + + def action_refuse_return(self): + """归还审批拒绝""" + for rec in self: + if rec.return_approver_id and rec.return_approver_id.id != self.env.user.id: + raise UserError('只有指定的归还审批人才能拒绝归还') + if rec.return_state != 'pending': + raise UserError('当前状态不允许拒绝归还') + rec.return_state = 'refused' + def action_refresh_json_display(self): """手动刷新json_display字段""" for record in self: @@ -337,6 +683,20 @@ class StockPicking(models.Model): self.receipt_status = 'certification' # 状态变为证明 return True + def action_reset_to_acceptance(self): + """管理员退回:清空验收信息,重新回到待转交验收节点""" + self.ensure_one() + if not self.env.user.has_group('base.group_system'): + raise UserError('只有系统管理员才能执行此操作!') + if self.state == 'done': + raise UserError('已完成入库的单据不能退回,如需撤销请联系技术人员!') + self.write({ + 'receipt_status': 'acceptance', + 'acceptance_user_id': False, + 'employee_id': False, + 'acceptance_date': False, + }) + def action_transfer_acceptance(self): """转交验收:弹出选人 wizard""" self.ensure_one() @@ -405,10 +765,38 @@ class StockPicking(models.Model): if picking.picking_type_code == 'incoming': new_name = picking._generate_sh_sequence(picking.scheduled_date) picking.write({'name': new_name}) + # 借用/领用单创建时自动建 workflow record,和申请人关联(草稿待办) + elif picking.picking_type_code == 'internal': + current_user = self.env.user + employee = current_user.employee_id + department = employee.department_id if employee else self.env['hr.department'] + if not department: + department = picking.department_id + self.env['soong.workflow.record'].sudo().create({ + 'name': picking.borrow_title or picking.name, + 'code': self.env['ir.sequence'].sudo().next_by_code('borrow.workflow.sequence'), + 'res_model': 'stock.picking', + 'res_id': picking.id, + 'users_ids': [(6, 0, [current_user.id])], + 'employee_ids': [(6, 0, employee.ids)], + 'done_by': current_user.id, + 'approve_state': 'start', + 'soong_state': 'to_do', + 'department_id': department.id if department else False, + 'workflow_id': 239, + }) return pickings def write(self, vals): """重写write方法""" result = super(StockPicking, self).write(vals) + # 标题变化时同步更新 workflow record 的 name + if 'borrow_title' in vals: + for rec in self: + if rec.picking_type_code != 'internal': + continue + wf = rec._get_or_create_workflow_record() + if wf and vals.get('borrow_title'): + wf.sudo().write({'name': vals['borrow_title']}) return result \ No newline at end of file diff --git a/yuthon_stock/models/ir_model_patch.py b/yuthon_stock/models/ir_model_patch.py new file mode 100644 index 000000000..af3577a7a --- /dev/null +++ b/yuthon_stock/models/ir_model_patch.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from odoo import models, api + + +class IrModelPatch(models.Model): + _inherit = 'ir.model' + + def _reflect_model_params(self, model): + vals = super(IrModelPatch, self)._reflect_model_params(model) + # 让 stock.picking 在工作流配置界面可被搜索到 + if model._name == 'stock.picking': + vals['is_workflow'] = True + return vals + + +class WorkflowRecordPatch(models.Model): + _inherit = 'soong.workflow.record' + + @api.model + def fix_borrow_workflow_id(self): + """修复借用领用单 workflow record 缺少 workflow_id 的旧数据""" + records = self.search([ + ('res_model', '=', 'stock.picking'), + ('workflow_id', '=', False), + ]) + if records: + records.write({'workflow_id': 239}) diff --git a/yuthon_stock/security/ir.model.access.csv b/yuthon_stock/security/ir.model.access.csv index 2b54aa105..228669567 100644 --- a/yuthon_stock/security/ir.model.access.csv +++ b/yuthon_stock/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_stock_picking_transfer_wizard,stock.picking.transfer.wizard,model_stock_picking_transfer_wizard,,1,1,1,1 +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 diff --git a/yuthon_stock/views/inherit_product_template_views.xml b/yuthon_stock/views/inherit_product_template_views.xml new file mode 100644 index 000000000..45c1bbe55 --- /dev/null +++ b/yuthon_stock/views/inherit_product_template_views.xml @@ -0,0 +1,22 @@ + + + + + inherit.product.template.stock.putaway + product.template + + + + + +