Files
odoo-addons/addons/tk_construction_management/models/material_requisition.py

736 lines
30 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2020-Today TechKhedut.
# Part of TechKhedut. See LICENSE file for full copyright and licensing details.
from odoo import fields, api, models, _
from odoo.exceptions import ValidationError
class MaterialRequisition(models.Model):
_name = 'material.requisition'
_description = "Material Requisition"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Sequence', required=True,
readonly=True, default=lambda self: _('New'))
title = fields.Char(string="Title", tracking=True)
reject_reason = fields.Text(string="Reject Reason")
allow_resubmit = fields.Boolean(string="Allow Resubmit", tracking=True)
po_created = fields.Boolean()
delivery_ready = fields.Boolean(compute="_compute_delivery_ready")
line_added = fields.Boolean()
stage = fields.Selection(
[('draft', 'Draft'),
('department_approval', 'Waiting Department Approval'),
('approve', 'In Progress'),
('ready_delivery', 'Ready for Delivery'),
('reject', 'Reject'),
('internal_transfer', 'Internal Transfer'),
('material_arrived', 'Material Arrived'),
('cancel', 'Cancel')], default='draft', tracking=True)
desc = fields.Text(string="Description")
# Project Details
site_id = fields.Many2one('tk.construction.site', string="Project")
project_id = fields.Many2one(
'tk.construction.project', string="Sub Project", tracking=True)
warehouse_id = fields.Many2one(
related="project_id.warehouse_id", string="Warehouse")
company_id = fields.Many2one(
'res.company', string="Company", default=lambda self: self.env.company)
# Other Details
date = fields.Datetime(string="Date", default=fields.Datetime.now())
responsible_id = fields.Many2one('res.users', default=lambda self: self.env.user and self.env.user.id or False,
string="Created By")
# Department Details
department_id = fields.Many2one(
'construction.department', string="Department", tracking=True)
manager_ids = fields.Many2many('res.users', string="Manager")
user_id = fields.Many2one('res.users', string="Responsible User ")
# Work Type & Job Sheet and Phase
work_type_id = fields.Many2one('job.type', string="Work Type")
work_order_id = fields.Many2one('job.order', string="Work Order")
job_sheet_id = fields.Many2one(
related="work_order_id.job_sheet_id", store=True)
internal_transfer_id = fields.Many2one(
'internal.transfer', string="Transfer Ref.")
# Back Order
is_back_order = fields.Boolean(tracking=True)
is_any_back_order = fields.Boolean(compute="_compute_any_back_order")
back_order_id = fields.Many2one(
'material.requisition', string="Back Order", tracking=True)
material_req_ref = fields.Char(
string="Material Requisition Ref.", tracking=True)
# One 2 Many
material_line_ids = fields.One2many(
'material.requisition.line', 'material_req_id')
material_purchase_ids = fields.One2many(
'material.purchase.line', 'material_req_id')
material_transfer_ids = fields.One2many(
'material.transfer.line', 'material_req_id')
# compute
po_count = fields.Integer(
string="Purchase Order Count", compute="_compute_count")
bill_count = fields.Integer(string="Bill Count", compute="_compute_count")
delivery_count = fields.Integer(
string="Delivery Count", compute="_compute_count")
# Create, Write, Unlink, Constrain
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code(
'material.req') or _('New')
res = super(MaterialRequisition, self).create(vals_list)
return res
def name_get(self):
data = []
for rec in self:
data.append((rec.id, '%s - %s' % (rec.name, rec.title)))
return data
# Compute
def _compute_count(self):
for rec in self:
rec.po_count = self.env['purchase.order'].search_count(
[('material_req_id', '=', rec.id)])
rec.bill_count = self.env['account.move'].search_count(
[('material_req_id', '=', rec.id)])
po = self.material_purchase_ids.mapped(
'purchase_order_id').mapped('name')
ids = self.env['stock.picking'].search(
[('origin', 'in', po), ('code', '=', 'incoming')]).mapped('id')
rec.delivery_count = self.env['stock.picking'].search_count(
[('id', 'in', ids)])
def _compute_delivery_ready(self):
if self.material_purchase_ids:
incomplete = False
for rec in self.material_purchase_ids:
if not rec.status == 'complete':
incomplete = True
break
if not incomplete:
self.delivery_ready = True
else:
self.delivery_ready = False
else:
self.delivery_ready = True
@api.depends('material_line_ids')
def _compute_any_back_order(self):
back_order = False
for rec in self.material_line_ids:
if rec.operation_type == 'back_order':
back_order = True
break
if back_order:
self.is_any_back_order = True
else:
self.is_any_back_order = False
# Onchange
@api.onchange('department_id', 'manager_ids')
def _onchange_department_manager(self):
ids = []
for rec in self:
if rec.department_id:
ids = rec.department_id.manager_ids.ids
return {'domain': {'manager_ids': [('id', 'in', ids)]}}
@api.onchange('department_id', 'manager_ids', 'user_id')
def _onchange_department_responsible(self):
ids = []
for rec in self:
if rec.department_id:
ids = rec.department_id.user_ids.ids
return {'domain': {'user_id': [('id', 'in', ids)]}}
# Smart Button
def action_view_purchase_order(self):
return {
'type': 'ir.actions.act_window',
'name': _('Purchase Order'),
'res_model': 'purchase.order',
'domain': [('material_req_id', '=', self.id)],
'context': {
'default_material_req_id': self.id
},
'view_mode': 'list,form,kanban',
'target': 'current'
}
def action_view_bills(self):
return {
'type': 'ir.actions.act_window',
'name': _('Bills'),
'res_model': 'account.move',
'domain': [('material_req_id', '=', self.id)],
'context': {
'default_material_req_id': self.id
},
'view_mode': 'list,form,kanban',
'target': 'current'
}
def action_view_delivery_order(self):
po = self.material_purchase_ids.mapped(
'purchase_order_id').mapped('name')
ids = self.env['stock.picking'].search(
[('origin', 'in', po)]).mapped('id')
return {
'type': 'ir.actions.act_window',
'name': _('Delivery Orders'),
'res_model': 'stock.picking',
'domain': [('id', 'in', ids), ('code', '=', 'incoming')],
'view_mode': 'list,form,kanban',
'target': 'current'
}
# Button
def action_department_approval(self):
user_id = self.env.user.id
msg = "<ul><li>Request for Department Approval of <strong>" + str(
self.name) + "</strong>" + " On <strong>" + str(
fields.Datetime.now()) + "</strong></li></ul>"
self.message_post(body=msg, partner_ids=[user_id])
self.stage = 'department_approval'
def action_approve_requisition(self):
material_arrives = True
for rec in self.material_line_ids:
if not (rec.forcast_qty >= rec.qty):
material_arrives = False
if material_arrives:
self.stage = 'material_arrived'
self.work_order_id.state = 'material_arrive'
else:
for rec in self.material_line_ids:
if rec.forcast_qty >= rec.qty:
rec.is_created = True
ready = True
for rec in self.material_line_ids:
if not rec.is_created:
ready = False
if not ready:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'message': "Please Validate Lines to Approve Order",
'sticky': False,
}
}
return message
self.stage = 'approve'
user_id = self.env.user.id
msg = "<ul><li>Approval request of <strong>" + str(self.name) + "</strong> has been approved On <strong>" + str(
fields.Datetime.now()) + "</strong></li></ul>"
self.message_post(body=msg, partner_ids=[user_id])
def action_draft_requisition(self):
self.stage = 'draft'
self.allow_resubmit = False
def action_ready_delivery(self):
delivery_warehouse = False
for rec in self.material_transfer_ids:
if not rec.delivery_warehouse_id:
delivery_warehouse = True
break
if delivery_warehouse:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': _('Warehouse Missing'),
'message': "Please Select Warehouse in Internal Transfer",
'sticky': False,
}
}
return message
if not self.material_purchase_ids:
self.line_added = True
if not self.line_added:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': _('Add lines'),
'message': "Please add purchase order line to internal transfer",
'sticky': False,
}
}
return message
if not self.material_transfer_ids:
self.stage = 'material_arrived'
self.work_order_id.state = 'material_arrive'
return {
'type': 'ir.actions.act_window',
'name': _('Work Order'),
'res_model': 'job.order',
'res_id': self.work_order_id.id,
'view_mode': 'form',
'target': 'current'
}
else:
self.stage = 'ready_delivery'
def action_create_purchase_order(self):
ready = True
for rec in self.material_purchase_ids:
if not rec.vendor_id:
ready = False
if not ready:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': ('Vendor or Warehouse Missing !'),
'message': "Please Select Warehouse or Vendor in Material Purchase",
'sticky': False,
}
}
return message
price_check = True
for rec in self.material_purchase_ids:
if not rec.price > 0:
price_check = False
if not price_check:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': ('Invalid Product Price !'),
'message': "Price must greater than zero to create Purchase Order.",
'sticky': False,
}
}
return message
vendor_count = self.material_purchase_ids.mapped(
'vendor_id').mapped('id')
warehouse_count = self.material_purchase_ids.mapped(
'purchase_warehouse_id').mapped('id')
for warehouse in warehouse_count:
for data in vendor_count:
lines = []
create_po_ids = []
for rec in self.material_purchase_ids:
if rec.purchase_warehouse_id.id == warehouse and not rec.purchase_order_id and rec.vendor_id.id == data:
lines.append((0, 0, {
'product_id': rec.product_id.id,
'name': rec.name,
'product_qty': rec.qty,
'product_uom': rec.uom_id.id,
'price_unit': rec.price,
}))
create_po_ids.append(rec.id)
rec.product_id.last_po_price = rec.price
stock_picking_type_id = self.env['stock.picking.type'].search(
[('code', '=', 'incoming'), ('warehouse_id', '=', warehouse)], limit=1)
if lines and stock_picking_type_id:
record = {
'partner_id': data,
'order_line': lines,
'material_req_id': self.id,
'picking_type_id': stock_picking_type_id.id
}
purchase_order_id = self.env['purchase.order'].create(
record)
for rec in create_po_ids:
materia_po_line = self.env['material.purchase.line'].browse(
rec)
materia_po_line.purchase_order_id = purchase_order_id.id
self.po_created = True
def action_create_back_order(self):
ready = True
for rec in self.material_line_ids:
if not rec.is_created:
ready = False
if not ready:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'message': "Please Validate Lines to Create Back Order",
'sticky': False,
}
}
return message
mrq = {
'title': self.title,
'project_id': self.project_id.id,
'warehouse_id': self.warehouse_id.id,
'department_id': self.department_id.id,
'manager_ids': self.manager_ids.ids,
'user_id': self.user_id.id,
'company_id': self.company_id.id,
'job_sheet_id': self.job_sheet_id.id,
'stage': 'approve',
'is_back_order': True,
'work_type_id': self.work_type_id.id,
'work_order_id': self.work_order_id.id,
'job_sheet_id': self.job_sheet_id.id,
'site_id': self.site_id.id,
'material_req_ref': self.name
}
mrq_id = self.env['material.requisition'].create(mrq)
self.back_order_id = mrq_id.id
for data in self.material_line_ids:
if data.operation_type == 'back_order':
record = {
'product_id': data.material_id.id,
'name': data.name,
'qty': data.back_order_qty,
'price': data.material_id.last_po_price if data.material_id.last_po_price != 0 else data.material_id.standard_price,
'purchase_warehouse_id': data.warehouse_id.id,
'material_req_id': mrq_id.id,
'sub_category_id': data.sub_category_id.id,
'job_sheet_id': data.job_sheet_id.id
}
self.env['material.purchase.line'].create(record)
return {
'type': 'ir.actions.act_window',
'name': _('Back Order'),
'res_model': 'material.requisition',
'res_id': mrq_id.id,
'view_mode': 'form',
'target': 'current'
}
def validate_material_line_all(self):
validate = True
for rec in self.material_line_ids:
if not rec.is_created and (not rec.warehouse_id or not rec.operation_type):
validate = False
if not validate:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': ('Warehouse or operation type Missing !'),
'message': "Please Select warehouse or operation Type to validate.",
'sticky': False,
}
}
return message
for rec in self.material_line_ids:
if not rec.is_created:
rec.validate_material_line()
def action_insert_internal_transfer(self):
for rec in self.material_purchase_ids:
if not rec.purchase_warehouse_id.id == self.project_id.warehouse_id.id:
internal_transfer_id = self.env['material.transfer.line'].create({
'product_id': rec.product_id.id,
'name': rec.name,
'pickup_warehouse_id': rec.purchase_warehouse_id.id,
'delivery_warehouse_id': self.project_id.warehouse_id.id,
'qty': rec.qty,
'material_req_id': rec.material_req_id.id,
'sub_category_id': rec.sub_category_id.id,
'job_sheet_id': rec.job_sheet_id.id,
'from_purchase': True
})
internal_transfer_id._compute_forcast_qty()
self.line_added = True
def action_create_internal_transfer(self):
internal_transfer_id = self.env['internal.transfer'].create({
'title': 'Internal Transfer of ' + str(self.name),
'site_id': self.site_id.id,
'project_id': self.project_id.id,
'work_type_id': self.work_type_id.id,
'job_sheet_id': self.job_sheet_id.id,
'work_order_id': self.work_order_id.id,
'material_req_id': self.id,
'department_id': self.department_id.id,
'manager_ids': self.manager_ids.ids,
'user_id': self.user_id.id,
})
for data in self.material_transfer_ids:
self.env['internal.transfer.line'].create({
'product_id': data.product_id.id,
'name': data.name,
'qty': data.qty,
'pickup_warehouse_id': data.pickup_warehouse_id.id,
'delivery_warehouse_id': data.delivery_warehouse_id.id,
'internal_transfer_id': internal_transfer_id.id,
'sub_category_id': data.sub_category_id.id,
})
return {
'type': 'ir.actions.act_window',
'name': _('Internal Transfer'),
'res_model': 'internal.transfer',
'res_id': internal_transfer_id.id,
'view_mode': 'form',
'target': 'current'
}
class MaterialRequisitionLine(models.Model):
_name = 'material.requisition.line'
_description = "Material Requisition Line"
material_id = fields.Many2one(
'product.product', string="Material", domain="[('is_material','=',True)]")
name = fields.Char(string="Description")
qty = fields.Integer(string="Qty.", default=1)
uom_id = fields.Many2one(
related="material_id.uom_po_id", string="Unit of Measure")
operation_type = fields.Selection([('purchase_order', 'Purchase Order'),
('internal_transfer', 'Internal Transfer'), ('back_order', 'Back Order')],
string="Operation Type")
forcast_qty = fields.Float(
string="Forcast Qty.", compute="_compute_forcast_qty")
warehouse_id = fields.Many2one(
'stock.warehouse', string="Pickup / Delivery Warehouse")
material_req_id = fields.Many2one('material.requisition')
is_created = fields.Boolean()
forcast_check = fields.Boolean(compute="_compute_forcast_check")
sub_category_id = fields.Many2one(
'job.sub.category', string="Work Sub Type")
stage = fields.Selection(related="material_req_id.stage")
job_sheet_id = fields.Many2one('job.costing', string="Job Cost Sheet")
remain_qty = fields.Integer(string="Remaining Qty.")
back_order_qty = fields.Integer(string="Back Order Qty.")
@api.depends('receive_qty', 'qty')
def _compute_receive_qty(self):
for rec in self:
if rec.receive_qty and rec.qty:
rec.remain_qty = rec.qty - rec.receive_qty
else:
rec.remain_qty = 0
@api.onchange('material_id')
def _onchange_product_desc(self):
for rec in self:
rec.name = rec.material_id.name
@api.depends('forcast_qty', 'qty')
def _compute_forcast_check(self):
for rec in self:
if rec.forcast_qty >= rec.qty:
rec.forcast_check = True
else:
rec.forcast_check = False
@api.depends('warehouse_id', 'material_id')
def _compute_forcast_qty(self):
for rec in self:
qty = 0.0
if rec.material_id:
qty = rec.material_id.with_context(
warehouse=rec.warehouse_id.id).virtual_available
rec.forcast_qty = qty
def validate_material_line(self):
if not self.is_created:
if not self.warehouse_id or not self.operation_type:
message = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': ('Warehouse or operation type Missing !'),
'message': "Please Select warehouse or operation Type to validate.",
'sticky': False,
}
}
return message
if self.operation_type == 'purchase_order':
purchase_data = {
'product_id': self.material_id.id,
'name': self.name,
'qty': self.qty,
'purchase_warehouse_id': self.warehouse_id.id,
'material_req_id': self.material_req_id.id,
'sub_category_id': self.sub_category_id.id,
'job_sheet_id': self.job_sheet_id.id,
'price': self.material_id.last_po_price if self.material_id.last_po_price != 0 else self.material_id.standard_price
}
self.env['material.purchase.line'].create(purchase_data)
if self.operation_type == 'internal_transfer':
internal_data = {
'product_id': self.material_id.id,
'name': self.name,
'qty': self.qty,
'pickup_warehouse_id': self.warehouse_id.id,
'delivery_warehouse_id': self.material_req_id.project_id.warehouse_id.id,
'material_req_id': self.material_req_id.id,
'sub_category_id': self.sub_category_id.id,
'job_sheet_id': self.job_sheet_id.id
}
if internal_data['pickup_warehouse_id'] == internal_data['delivery_warehouse_id']:
pass
else:
self.env['material.transfer.line'].create(internal_data)
if self.operation_type == 'back_order':
if self.qty > self.forcast_qty:
self.back_order_qty = self.qty - self.forcast_qty
internal_data = {
'product_id': self.material_id.id,
'name': self.name,
'qty': self.forcast_qty,
'pickup_warehouse_id': self.warehouse_id.id,
'delivery_warehouse_id': self.material_req_id.project_id.warehouse_id.id,
'material_req_id': self.material_req_id.id,
'sub_category_id': self.sub_category_id.id,
'job_sheet_id': self.job_sheet_id.id
}
if internal_data['pickup_warehouse_id'] == internal_data['delivery_warehouse_id']:
pass
else:
self.env['material.transfer.line'].create(
internal_data)
self.is_created = True
class MaterialPurchaseLine(models.Model):
_name = 'material.purchase.line'
_description = "Material Purchase Line"
product_id = fields.Many2one('product.product', string="Product")
name = fields.Char(string="Description")
qty = fields.Integer(string="Qty.", default=1)
company_id = fields.Many2one(
'res.company', string='Company', default=lambda self: self.env.company)
currency_id = fields.Many2one(
'res.currency', related='company_id.currency_id', string='Currency')
forcast_qty = fields.Float(
string="Forcast Qty.", compute="_compute_forcast_qty")
price = fields.Monetary(string="Price")
total_price = fields.Monetary(
string="Total Price", compute="_compute_total_price")
uom_id = fields.Many2one(
related="product_id.uom_po_id", string="Unit of Measure")
vendor_id = fields.Many2one('res.partner', string="Vendor")
purchase_warehouse_id = fields.Many2one(
'stock.warehouse', string="Delivery Warehouse")
material_req_id = fields.Many2one('material.requisition')
purchase_order_id = fields.Many2one(
'purchase.order', string="Purchase Order")
status = fields.Selection([('incomplete', 'Incomplete'), ('partial_complete', 'Partial Complete'),
('complete', 'Complete')], compute="_compute_po_delivery_status")
sub_category_id = fields.Many2one(
'job.sub.category', string="Work Sub Type")
job_sheet_id = fields.Many2one('job.costing', string="Job Cost Sheet")
def unlink(self):
if not self.purchase_order_id:
return super(MaterialPurchaseLine, self).unlink()
else:
raise ValidationError(
_('You can not delete material purchase line after creating purchase order'))
@api.depends('purchase_warehouse_id', 'product_id')
def _compute_forcast_qty(self):
for rec in self:
qty = 0.0
if rec.product_id:
qty = rec.product_id.with_context(
warehouse=rec.purchase_warehouse_id.id).virtual_available
rec.forcast_qty = qty
@api.depends('price', 'qty')
def _compute_total_price(self):
for rec in self:
total = 0.0
if rec.qty and rec.price:
total = rec.qty * rec.price
rec.total_price = total
@api.onchange('product_id')
def _onchange_product_desc(self):
for rec in self:
rec.name = rec.product_id.name
@api.depends('purchase_order_id')
def _compute_po_delivery_status(self):
for rec in self:
delivery_orders = self.env['stock.picking'].search(
[('code', '=', 'incoming'), ('origin', '=', rec.purchase_order_id.name)])
if rec.purchase_order_id and delivery_orders:
incomplete, complete = False, False
for data in delivery_orders:
if data.state == 'done':
complete = True
else:
incomplete = True
if complete and not incomplete:
rec.status = 'complete'
elif incomplete and not complete:
rec.status = 'incomplete'
else:
rec.status = "partial_complete"
else:
rec.status = ""
@api.onchange('product_id', 'vendor_id')
def onchange_product_vendor_price_list(self):
for rec in self:
if rec.vendor_id and rec.product_id:
domain = [('partner_id', '=', rec.vendor_id.id), '|',
('product_tmpl_id', '=', rec.product_id.product_tmpl_id.id),
('product_id', '=', rec.product_id.id)]
vendor_price = self.env['product.supplierinfo'].search(
domain, limit=1, order='create_date desc')
if vendor_price.price > 0:
rec.price = vendor_price.price
elif rec.product_id.last_po_price > 0:
rec.price = rec.product_id.last_po_price
else:
rec.price = rec.product_id.standard_price
class MaterialTransferLine(models.Model):
_name = 'material.transfer.line'
_description = "Material Transfer Line"
product_id = fields.Many2one('product.product', string="Product")
name = fields.Char(string="Description")
qty = fields.Integer(string="Qty.", default=1)
forcast_qty = fields.Float(
string="Forcast Qty.", compute="_compute_forcast_qty")
pickup_warehouse_id = fields.Many2one(
'stock.warehouse', string="Picking Warehouse")
delivery_warehouse_id = fields.Many2one(
'stock.warehouse', string="Delivery Warehouse")
material_req_id = fields.Many2one('material.requisition')
sub_category_id = fields.Many2one(
'job.sub.category', string="Work Sub Type")
job_sheet_id = fields.Many2one('job.costing', string="Job Cost Sheet")
from_purchase = fields.Boolean(string="From Purchase")
@api.depends('pickup_warehouse_id', 'product_id')
def _compute_forcast_qty(self):
for rec in self:
qty = 0.0
if rec.product_id:
qty = rec.product_id.with_context(
warehouse=rec.pickup_warehouse_id.id).virtual_available
rec.forcast_qty = qty
@api.onchange('product_id')
def _onchange_product_desc(self):
for rec in self:
rec.name = rec.product_id.name