summaryrefslogtreecommitdiff
path: root/addons/purchase_requisition/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/purchase_requisition/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/purchase_requisition/models')
-rw-r--r--addons/purchase_requisition/models/__init__.py5
-rw-r--r--addons/purchase_requisition/models/product.py34
-rw-r--r--addons/purchase_requisition/models/purchase.py123
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py264
4 files changed, 426 insertions, 0 deletions
diff --git a/addons/purchase_requisition/models/__init__.py b/addons/purchase_requisition/models/__init__.py
new file mode 100644
index 00000000..a12e95fe
--- /dev/null
+++ b/addons/purchase_requisition/models/__init__.py
@@ -0,0 +1,5 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import purchase
+from . import product
+from . import purchase_requisition
diff --git a/addons/purchase_requisition/models/product.py b/addons/purchase_requisition/models/product.py
new file mode 100644
index 00000000..36f6a421
--- /dev/null
+++ b/addons/purchase_requisition/models/product.py
@@ -0,0 +1,34 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class SupplierInfo(models.Model):
+ _inherit = 'product.supplierinfo'
+
+ purchase_requisition_id = fields.Many2one('purchase.requisition', related='purchase_requisition_line_id.requisition_id', string='Agreement', readonly=False)
+ purchase_requisition_line_id = fields.Many2one('purchase.requisition.line')
+
+
+class ProductProduct(models.Model):
+ _inherit = 'product.product'
+
+ def _prepare_sellers(self, params=False):
+ sellers = super(ProductProduct, self)._prepare_sellers(params=params)
+ if params and params.get('order_id'):
+ return sellers.filtered(lambda s: not s.purchase_requisition_id or s.purchase_requisition_id == params['order_id'].requisition_id)
+ else:
+ return sellers
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ purchase_requisition = fields.Selection(
+ [('rfq', 'Create a draft purchase order'),
+ ('tenders', 'Propose a call for tenders')],
+ string='Procurement', default='rfq',
+ help="Create a draft purchase order: Based on your product configuration, the system will create a draft "
+ "purchase order.Propose a call for tender : If the 'purchase_requisition' module is installed and this option "
+ "is selected, the system will create a draft call for tender.")
diff --git a/addons/purchase_requisition/models/purchase.py b/addons/purchase_requisition/models/purchase.py
new file mode 100644
index 00000000..a3665665
--- /dev/null
+++ b/addons/purchase_requisition/models/purchase.py
@@ -0,0 +1,123 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class PurchaseOrder(models.Model):
+ _inherit = 'purchase.order'
+
+ requisition_id = fields.Many2one('purchase.requisition', string='Purchase Agreement', copy=False)
+ is_quantity_copy = fields.Selection(related='requisition_id.is_quantity_copy', readonly=False)
+
+ @api.onchange('requisition_id')
+ def _onchange_requisition_id(self):
+ if not self.requisition_id:
+ return
+
+ self = self.with_company(self.company_id)
+ requisition = self.requisition_id
+ if self.partner_id:
+ partner = self.partner_id
+ else:
+ partner = requisition.vendor_id
+ payment_term = partner.property_supplier_payment_term_id
+
+ FiscalPosition = self.env['account.fiscal.position']
+ fpos = FiscalPosition.with_company(self.company_id).get_fiscal_position(partner.id)
+
+ self.partner_id = partner.id
+ self.fiscal_position_id = fpos.id
+ self.payment_term_id = payment_term.id,
+ self.company_id = requisition.company_id.id
+ self.currency_id = requisition.currency_id.id
+ if not self.origin or requisition.name not in self.origin.split(', '):
+ if self.origin:
+ if requisition.name:
+ self.origin = self.origin + ', ' + requisition.name
+ else:
+ self.origin = requisition.name
+ self.notes = requisition.description
+ self.date_order = fields.Datetime.now()
+
+ if requisition.type_id.line_copy != 'copy':
+ return
+
+ # Create PO lines if necessary
+ order_lines = []
+ for line in requisition.line_ids:
+ # Compute name
+ product_lang = line.product_id.with_context(
+ lang=partner.lang or self.env.user.lang,
+ partner_id=partner.id
+ )
+ name = product_lang.display_name
+ if product_lang.description_purchase:
+ name += '\n' + product_lang.description_purchase
+
+ # Compute taxes
+ taxes_ids = fpos.map_tax(line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id)).ids
+
+ # Compute quantity and price_unit
+ if line.product_uom_id != line.product_id.uom_po_id:
+ product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
+ price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
+ else:
+ product_qty = line.product_qty
+ price_unit = line.price_unit
+
+ if requisition.type_id.quantity_copy != 'copy':
+ product_qty = 0
+
+ # Create PO line
+ order_line_values = line._prepare_purchase_order_line(
+ name=name, product_qty=product_qty, price_unit=price_unit,
+ taxes_ids=taxes_ids)
+ order_lines.append((0, 0, order_line_values))
+ self.order_line = order_lines
+
+ def button_confirm(self):
+ res = super(PurchaseOrder, self).button_confirm()
+ for po in self:
+ if not po.requisition_id:
+ continue
+ if po.requisition_id.type_id.exclusive == 'exclusive':
+ others_po = po.requisition_id.mapped('purchase_ids').filtered(lambda r: r.id != po.id)
+ others_po.button_cancel()
+ if po.state not in ['draft', 'sent', 'to approve']:
+ po.requisition_id.action_done()
+ return res
+
+ @api.model
+ def create(self, vals):
+ purchase = super(PurchaseOrder, self).create(vals)
+ if purchase.requisition_id:
+ purchase.message_post_with_view('mail.message_origin_link',
+ values={'self': purchase, 'origin': purchase.requisition_id},
+ subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'))
+ return purchase
+
+ def write(self, vals):
+ result = super(PurchaseOrder, self).write(vals)
+ if vals.get('requisition_id'):
+ self.message_post_with_view('mail.message_origin_link',
+ values={'self': self, 'origin': self.requisition_id, 'edit': True},
+ subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'))
+ return result
+
+
+class PurchaseOrderLine(models.Model):
+ _inherit = 'purchase.order.line'
+
+ @api.onchange('product_qty', 'product_uom')
+ def _onchange_quantity(self):
+ res = super(PurchaseOrderLine, self)._onchange_quantity()
+ if self.order_id.requisition_id:
+ for line in self.order_id.requisition_id.line_ids.filtered(lambda l: l.product_id == self.product_id):
+ if line.product_uom_id != self.product_uom:
+ self.price_unit = line.product_uom_id._compute_price(
+ line.price_unit, self.product_uom)
+ else:
+ self.price_unit = line.price_unit
+ break
+ return res
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
new file mode 100644
index 00000000..7eb3d0db
--- /dev/null
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -0,0 +1,264 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, time
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+PURCHASE_REQUISITION_STATES = [
+ ('draft', 'Draft'),
+ ('ongoing', 'Ongoing'),
+ ('in_progress', 'Confirmed'),
+ ('open', 'Bid Selection'),
+ ('done', 'Closed'),
+ ('cancel', 'Cancelled')
+]
+
+
+class PurchaseRequisitionType(models.Model):
+ _name = "purchase.requisition.type"
+ _description = "Purchase Requisition Type"
+ _order = "sequence"
+
+ name = fields.Char(string='Agreement Type', required=True, translate=True)
+ sequence = fields.Integer(default=1)
+ exclusive = fields.Selection([
+ ('exclusive', 'Select only one RFQ (exclusive)'), ('multiple', 'Select multiple RFQ (non-exclusive)')],
+ string='Agreement Selection Type', required=True, default='multiple',
+ help="""Select only one RFQ (exclusive): when a purchase order is confirmed, cancel the remaining purchase order.\n
+ Select multiple RFQ (non-exclusive): allows multiple purchase orders. On confirmation of a purchase order it does not cancel the remaining orders""")
+ quantity_copy = fields.Selection([
+ ('copy', 'Use quantities of agreement'), ('none', 'Set quantities manually')],
+ string='Quantities', required=True, default='none')
+ line_copy = fields.Selection([
+ ('copy', 'Use lines of agreement'), ('none', 'Do not create RfQ lines automatically')],
+ string='Lines', required=True, default='copy')
+
+
+class PurchaseRequisition(models.Model):
+ _name = "purchase.requisition"
+ _description = "Purchase Requisition"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = "id desc"
+
+ def _get_type_id(self):
+ return self.env['purchase.requisition.type'].search([], limit=1)
+
+ name = fields.Char(string='Reference', required=True, copy=False, default='New', readonly=True)
+ origin = fields.Char(string='Source Document')
+ order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders')
+ vendor_id = fields.Many2one('res.partner', string="Vendor", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+ type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id)
+ ordering_date = fields.Date(string="Ordering Date", tracking=True)
+ date_end = fields.Datetime(string='Agreement Deadline', tracking=True)
+ schedule_date = fields.Date(string='Delivery Date', index=True, help="The expected and scheduled delivery date where all the products are received", tracking=True)
+ user_id = fields.Many2one(
+ 'res.users', string='Purchase Representative',
+ default=lambda self: self.env.user, check_company=True)
+ description = fields.Text()
+ company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
+ purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]})
+ line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True)
+ product_id = fields.Many2one('product.product', related='line_ids.product_id', string='Product', readonly=False)
+ state = fields.Selection(PURCHASE_REQUISITION_STATES,
+ 'Status', tracking=True, required=True,
+ copy=False, default='draft')
+ state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state')
+ is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True)
+ currency_id = fields.Many2one('res.currency', 'Currency', required=True,
+ default=lambda self: self.env.company.currency_id.id)
+
+ @api.depends('state')
+ def _set_state(self):
+ for requisition in self:
+ requisition.state_blanket_order = requisition.state
+
+ @api.onchange('vendor_id')
+ def _onchange_vendor(self):
+ self = self.with_company(self.company_id)
+ if not self.vendor_id:
+ self.currency_id = self.env.company.currency_id.id
+ else:
+ self.currency_id = self.vendor_id.property_purchase_currency_id.id or self.env.company.currency_id.id
+
+ requisitions = self.env['purchase.requisition'].search([
+ ('vendor_id', '=', self.vendor_id.id),
+ ('state', '=', 'ongoing'),
+ ('type_id.quantity_copy', '=', 'none'),
+ ('company_id', '=', self.company_id.id),
+ ])
+ if any(requisitions):
+ title = _("Warning for %s", self.vendor_id.name)
+ message = _("There is already an open blanket order for this supplier. We suggest you complete this open blanket order, instead of creating a new one.")
+ warning = {
+ 'title': title,
+ 'message': message
+ }
+ return {'warning': warning}
+
+ @api.depends('purchase_ids')
+ def _compute_orders_number(self):
+ for requisition in self:
+ requisition.order_count = len(requisition.purchase_ids)
+
+ def action_cancel(self):
+ # try to set all associated quotations to cancel state
+ for requisition in self:
+ for requisition_line in requisition.line_ids:
+ requisition_line.supplier_info_ids.unlink()
+ requisition.purchase_ids.button_cancel()
+ for po in requisition.purchase_ids:
+ po.message_post(body=_('Cancelled by the agreement associated to this quotation.'))
+ self.write({'state': 'cancel'})
+
+ def action_in_progress(self):
+ self.ensure_one()
+ if not self.line_ids:
+ raise UserError(_("You cannot confirm agreement '%s' because there is no product line.", self.name))
+ if self.type_id.quantity_copy == 'none' and self.vendor_id:
+ for requisition_line in self.line_ids:
+ if requisition_line.price_unit <= 0.0:
+ raise UserError(_('You cannot confirm the blanket order without price.'))
+ if requisition_line.product_qty <= 0.0:
+ raise UserError(_('You cannot confirm the blanket order without quantity.'))
+ requisition_line.create_supplier_info()
+ self.write({'state': 'ongoing'})
+ else:
+ self.write({'state': 'in_progress'})
+ # Set the sequence number regarding the requisition type
+ if self.name == 'New':
+ if self.is_quantity_copy != 'none':
+ self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.purchase.tender')
+ else:
+ self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.blanket.order')
+
+ def action_open(self):
+ self.write({'state': 'open'})
+
+ def action_draft(self):
+ self.ensure_one()
+ self.name = 'New'
+ self.write({'state': 'draft'})
+
+ def action_done(self):
+ """
+ Generate all purchase order based on selected lines, should only be called on one agreement at a time
+ """
+ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')):
+ raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.'))
+ for requisition in self:
+ for requisition_line in requisition.line_ids:
+ requisition_line.supplier_info_ids.unlink()
+ self.write({'state': 'done'})
+
+ def unlink(self):
+ if any(requisition.state not in ('draft', 'cancel') for requisition in self):
+ raise UserError(_('You can only delete draft requisitions.'))
+ # Draft requisitions could have some requisition lines.
+ self.mapped('line_ids').unlink()
+ return super(PurchaseRequisition, self).unlink()
+
+
+class PurchaseRequisitionLine(models.Model):
+ _name = "purchase.requisition.line"
+ _description = "Purchase Requisition Line"
+ _rec_name = 'product_id'
+
+ product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True)
+ product_uom_id = fields.Many2one('uom.uom', string='Product Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
+ product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure')
+ product_description_variants = fields.Char('Custom Description')
+ price_unit = fields.Float(string='Unit Price', digits='Product Price')
+ qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities')
+ requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
+ company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env.company)
+ account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
+ analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
+ schedule_date = fields.Date(string='Scheduled Date')
+ supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
+
+ @api.model
+ def create(self,vals):
+ res = super(PurchaseRequisitionLine, self).create(vals)
+ if res.requisition_id.state not in ['draft', 'cancel', 'done'] and res.requisition_id.is_quantity_copy == 'none':
+ supplier_infos = self.env['product.supplierinfo'].search([
+ ('product_id', '=', vals.get('product_id')),
+ ('name', '=', res.requisition_id.vendor_id.id),
+ ])
+ if not any(s.purchase_requisition_id for s in supplier_infos):
+ res.create_supplier_info()
+ if vals['price_unit'] <= 0.0:
+ raise UserError(_('You cannot confirm the blanket order without price.'))
+ return res
+
+ def write(self, vals):
+ res = super(PurchaseRequisitionLine, self).write(vals)
+ if 'price_unit' in vals:
+ if vals['price_unit'] <= 0.0 and any(
+ requisition.state not in ['draft', 'cancel', 'done'] and
+ requisition.is_quantity_copy == 'none' for requisition in self.mapped('requisition_id')):
+ raise UserError(_('You cannot confirm the blanket order without price.'))
+ # If the price is updated, we have to update the related SupplierInfo
+ self.supplier_info_ids.write({'price': vals['price_unit']})
+ return res
+
+ def unlink(self):
+ to_unlink = self.filtered(lambda r: r.requisition_id.state not in ['draft', 'cancel', 'done'])
+ to_unlink.mapped('supplier_info_ids').unlink()
+ return super(PurchaseRequisitionLine, self).unlink()
+
+ def create_supplier_info(self):
+ purchase_requisition = self.requisition_id
+ if purchase_requisition.type_id.quantity_copy == 'none' and purchase_requisition.vendor_id:
+ # create a supplier_info only in case of blanket order
+ self.env['product.supplierinfo'].create({
+ 'name': purchase_requisition.vendor_id.id,
+ 'product_id': self.product_id.id,
+ 'product_tmpl_id': self.product_id.product_tmpl_id.id,
+ 'price': self.price_unit,
+ 'currency_id': self.requisition_id.currency_id.id,
+ 'purchase_requisition_line_id': self.id,
+ })
+
+ @api.depends('requisition_id.purchase_ids.state')
+ def _compute_ordered_qty(self):
+ for line in self:
+ total = 0.0
+ for po in line.requisition_id.purchase_ids.filtered(lambda purchase_order: purchase_order.state in ['purchase', 'done']):
+ for po_line in po.order_line.filtered(lambda order_line: order_line.product_id == line.product_id):
+ if po_line.product_uom != line.product_uom_id:
+ total += po_line.product_uom._compute_quantity(po_line.product_qty, line.product_uom_id)
+ else:
+ total += po_line.product_qty
+ line.qty_ordered = total
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ if self.product_id:
+ self.product_uom_id = self.product_id.uom_po_id
+ self.product_qty = 1.0
+ if not self.schedule_date:
+ self.schedule_date = self.requisition_id.schedule_date
+
+ def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False):
+ self.ensure_one()
+ requisition = self.requisition_id
+ if self.product_description_variants:
+ name += '\n' + self.product_description_variants
+ if requisition.schedule_date:
+ date_planned = datetime.combine(requisition.schedule_date, time.min)
+ else:
+ date_planned = datetime.now()
+ return {
+ 'name': name,
+ 'product_id': self.product_id.id,
+ 'product_uom': self.product_id.uom_po_id.id,
+ 'product_qty': product_qty,
+ 'price_unit': price_unit,
+ 'taxes_id': [(6, 0, taxes_ids)],
+ 'date_planned': date_planned,
+ 'account_analytic_id': self.account_analytic_id.id,
+ 'analytic_tag_ids': self.analytic_tag_ids.ids,
+ }