diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/purchase_requisition/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/purchase_requisition/models')
| -rw-r--r-- | addons/purchase_requisition/models/__init__.py | 5 | ||||
| -rw-r--r-- | addons/purchase_requisition/models/product.py | 34 | ||||
| -rw-r--r-- | addons/purchase_requisition/models/purchase.py | 123 | ||||
| -rw-r--r-- | addons/purchase_requisition/models/purchase_requisition.py | 264 |
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, + } |
