# -*- coding: utf-8 -*- ################################################################################ # # Cybrosys Technologies Pvt. Ltd. # # Copyright (C) 2023-TODAY Cybrosys Technologies(). # Author: Ruksana P (odoo@cybrosys.com) # # You can modify it under the terms of the GNU AFFERO # GENERAL PUBLIC LICENSE (AGPL v3), Version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. # # You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE # (AGPL v3) along with this program. # If not, see . # ################################################################################ from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import api, exceptions, fields, models, _ class PurchaseRecurringAgreement(models.Model): """Model for generating purchase recurring agreement""" _name = 'purchase.recurring.agreement' _inherit = 'mail.thread' _description = "Purchase Recurring Agreement" def _default_company_id(self): """Returns the Current Company id""" company = self.env['res.company']._company_default_get('purchase') return company name = fields.Char(string='Sequence Number', index=True, size=32, copy=False, help="Sequence number of agreement", readonly=True, default=_('New')) active = fields.Boolean(string='Active', default=True, help='Unchecking this field, quotation for that ' 'product is not generated') partner_id = fields.Many2one('res.partner', string='Supplier', index=True, change_default=True, required=True, help="Supplier you are making the agreement") company_id = fields.Many2one('res.company', string='Company', required=True, help="Company that signs the agreement", default=_default_company_id) start_date = fields.Date(string='Start Date', index=True, copy=False, help=" Starting of the agreement. Keep empty to" " use the current date") prolong = fields.Selection( selection=[('recurrent', 'Renewable Fixed Term'), ('unlimited', 'Unlimited Term'), ('fixed', 'Fixed Term')], string='Prolongation', default='unlimited', help="Sets the term of the agreement. 'Renewable fixed term': It sets " "a fixed term, but with possibility of manual renew; 'Unlimited " "term': Renew is made automatically; 'Fixed term': The term is " "fixed and there is no possibility to renew.") end_date = fields.Date(string='End date', help="End date of the agreement") prolong_interval = fields.Integer( string='Interval', default=1, help="Interval in time units to prolong the agreement until new " "renewable (that is automatic for unlimited term, manual for " "renewable fixed term).") prolong_unit = fields.Selection([('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ('years', 'Years')], string='Interval Unit', default='years', help='Time unit for the prolongation ' 'interval') agreement_line_ids = fields.One2many('recurring.agreement.line', inverse_name='recurring_agreement_id', string='Agreement Lines', help='Agreement product records') order_ids = fields.One2many('purchase.order', copy=False, inverse_name='recurring_agreement_id', string='Orders', readonly=True, help='Purchase orders for this agreement') renewal_ids = fields.One2many('agreement.renewal.line', copy=False, string='Renewal Lines', readonly=True, inverse_name='recurring_agreement_id', help='Renewal records') last_renovation_date = fields.Datetime( string='Last Renovation Date', onchange='_onchange_last_renovation_date', help="Last date when agreement was renewed (same as start date if not " "renewed)") next_expiration_date = fields.Datetime( compute="_compute_next_expiration_date", string='Next Expiration Date', help="Date when agreement will expired") state = fields.Selection([('empty', 'Without Orders'), ('first', 'First Order Created'), ('orders', 'With Orders')], string='State', help="Indicates the state of recurring agreement", readonly=True, default='empty') renewal_state = fields.Selection([('not_renewed', 'Agreement not Renewed'), ('renewed', 'Agreement Renewed')], string='Renewal State', readonly=True, help="Renewal Status of the Recurring " "agreement", default='not_renewed') notes = fields.Text('Notes', help="Notes regarding Renewal agreement") order_count = fields.Integer(compute='_compute_order_count', help="Indicates the No. of Orders Generated" " with this Agreement", string='Order Count') _sql_constraints = [ ('name_uniq', 'unique(name)', 'Agreement Number Must be Unique !'), ] @api.model def _get_next_term_date(self, date, unit, interval): """Returns the Next Term Date""" if unit == 'days': date = date + timedelta(days=interval) elif unit == 'weeks': date = date + timedelta(weeks=interval) elif unit == 'months': date = date + relativedelta(months=interval) elif unit == 'years': date = date + relativedelta(years=interval) return date def _compute_next_expiration_date(self): """Calculates the Next Expiration Date According to the Prolongation Unit Chosen""" for agreement in self: if agreement.prolong == 'fixed': agreement.next_expiration_date = agreement.end_date elif agreement.prolong == 'unlimited': now = fields.Date.from_string(fields.Datetime.today()) date = self._get_next_term_date( fields.Date.from_string(agreement.start_date), agreement.prolong_unit, agreement.prolong_interval) while date < now: date = self._get_next_term_date( date, agreement.prolong_unit, agreement.prolong_interval) agreement.next_expiration_date = date else: agreement.next_expiration_date = self._get_next_term_date( fields.Datetime.from_string( agreement.last_renovation_date or agreement.start_date), agreement.prolong_unit, agreement.prolong_interval) def _compute_order_count(self): """Finds the count of orders generated from the Agreement""" for record in self: record.order_count = self.env['purchase.order'].search_count( [('recurring_agreement_id', '=', record.id)]) @api.constrains('start_date', 'end_date') def _check_dates(self): """Method for ensuring start date will be always less than or equal to end date""" for record in self: if record.end_date and record.end_date < record.start_date: raise exceptions.Warning( _('Agreement End Date must be Greater than Start Date')) @api.model def create(self, vals): """Function that supering create function""" if not vals.get('start_date'): vals['start_date'] = fields.Datetime.today() if not vals.get('name'): vals['name'] = self.env['ir.sequence'].get( 'purchase.r_o.agreement.sequence') return super().create(vals) def write(self, vals): """Function that supering write function""" value = super().write(vals) if (any(vals.get(x) is not None for x in ['active', 'name', 'agreement_line_ids', 'prolong', 'end_date', 'prolong_interval', 'prolong_unit', 'partner_id'])): self.unlink_orders(fields.Datetime.today()) return value @api.returns('self', lambda value: value.id) def copy(self, default=None): default = dict(default or {}) if 'name' not in default: default['name'] = _("%s (Copy)") % self.name return super().copy(default=default) def unlink(self): """Function that supering unlink function which will unlink Self and the Current record""" for agreement in self: if any(agreement.mapped('order_ids')): raise exceptions.Warning( _('You Cannot Remove Agreements with Confirmed Orders!')) self.unlink_orders(fields.Datetime.from_string(fields.Datetime.today())) return models.Model.unlink(self) @api.onchange('start_date') def _onchange_last_renovation_date(self): """Method for updating last renovation date""" self.last_renovation_date = self.start_date @api.model def revise_agreements_expirations_planned(self): """Method for changing the prolong as unlimited""" for agreement in self.search([('prolong', '=', 'unlimited')]): if agreement.next_expiration_date <= fields.Datetime.today(): agreement.write({'prolong': 'unlimited'}) return True @api.model def _prepare_purchase_order_vals(self, agreement, date): """Creates purchase order values""" # Order Values order_vals = {'date_order': date, 'origin': agreement.name, 'partner_id': agreement.partner_id.id, 'state': 'draft', 'company_id': agreement.company_id.id, 'is_agreement': True, 'recurring_agreement_id': agreement.id, 'date_planned': date, 'payment_term_id': agreement.partner_id. property_supplier_payment_term_id.id, 'currency_id': agreement.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id, 'user_id': agreement.partner_id.user_id.id} return order_vals @api.model def _prepare_purchase_order_line_vals(self, agreement_line_ids, order): """Returns the Purchase Order Line Values as a Dictionary Which can be Used While creating the Purchase Order""" product = agreement_line_ids.product_id product_lang = product.with_context({ 'lang': order.partner_id.lang, 'partner_id': order.partner_id.id, }) fpos = order.fiscal_position_id # Order Line Values as a Dictionary order_line_vals = { 'order_id': order.id, 'product_id': product.id, 'product_qty': agreement_line_ids.quantity, 'date_planned': order.date_planned, 'price_unit': product._get_tax_included_unit_price( order.company_id, order.currency_id, order.date_order, 'purchase', fiscal_position=order.fiscal_position_id, product_uom=product.uom_po_id), 'product_uom': product.uom_po_id.id or product.uom_id.id, 'name': product_lang.display_name, 'taxes_id': fpos.map_tax( product.supplier_taxes_id.filtered( lambda r: r.company_id.id == self.company_id.id).ids) } # product price changed if specific price is added if agreement_line_ids.specific_price: order_line_vals['price_unit'] = agreement_line_ids.specific_price order_line_vals['taxes_id'] = [ (6, 0, tuple(order_line_vals['taxes_id']))] # product price changed if specific price is added if agreement_line_ids.additional_description: order_line_vals['name'] += " %s" % ( agreement_line_ids.additional_description) return order_line_vals def create_order(self, date, agreement_lines): """Create Purchase Order from Recurring Agreement """ self.ensure_one() order_line_obj = self.env['purchase.order.line'].with_context( company_id=self.company_id.id) order_vals = self._prepare_purchase_order_vals(self, date) order = self.env['purchase.order'].create(order_vals) for agreement_line in agreement_lines: # Create Purchase Order Line Values order_line_vals = self._prepare_purchase_order_line_vals( agreement_line, order) order_line_obj.create(order_line_vals) agreement_lines.write({'last_order_date': fields.Datetime.today()}) if self.state != 'orders': self.state = 'orders' return order def _get_next_order_date(self, line, start_date): """Return The date of Next Purchase order generated from the Agreement""" self.ensure_one() next_date = fields.Datetime.from_string(self.start_date) while next_date <= start_date: next_date = self._get_next_term_date( next_date, line.ordering_unit, line.ordering_interval) return next_date def generate_agreement_orders(self, start_date, end_date): """Method for generating agreement orders""" self.ensure_one() if not self.active: return lines_to_order = {} # Get next expiration date exp_date = fields.Datetime.from_string(self.next_expiration_date) if exp_date < end_date and self.prolong != 'unlimited': end_date = exp_date for line in self.agreement_line_ids: if not line.is_active: continue # Get Date of Next Order next_order_date = self._get_next_order_date(line, start_date) while next_order_date <= end_date: if not lines_to_order.get(next_order_date): lines_to_order[next_order_date] = self.env[ 'recurring.agreement.line'] lines_to_order[next_order_date] = line next_order_date = self._get_next_order_date( line, next_order_date) dates = lines_to_order.keys() sorted(dates) for date in dates: order = self.order_ids.filtered( lambda x: ( fields.Date.to_string( fields.Datetime.from_string(x.date_order)) == fields.Date.to_string(date))) if not order: self.create_order( fields.Datetime.to_string(date), lines_to_order[date]) @api.model def generate_next_orders_planned(self, years=1, start_date=None): """Method for generating the planned orders""" if start_date: start_date = fields.Datetime.from_string(start_date) self.search([]).generate_next_orders( years=years, start_date=start_date) def generate_next_orders(self, years=1, start_date=None): if not start_date: start_date = fields.Datetime.from_string(fields.Date.today()) end_date = start_date + relativedelta(years=years) for agreement in self: agreement.generate_agreement_orders(start_date, end_date) return True @api.model def confirm_current_orders_planned(self): """This will Confirm All Orders satisfying the Domain""" tomorrow = fields.Date.to_string( fields.Datetime.from_string(fields.Datetime.today()) + timedelta( days=1)) orders = self.env['purchase.order'].search([ ('recurring_agreement_id', '!=', False), ('state', 'in', ('draft', 'sent')), ('date_order', '<', tomorrow) ]) for order in orders: order.signal_workflow('order_confirm') def unlink_orders(self, start_date): """ Remove the relation between ``self`` and the related record.""" orders = self.mapped('order_ids').filtered( lambda x: (x.state in ('draft', 'sent') and x.date_order >= start_date)) orders.unlink() def action_view_purchase_order(self): """Returns All Orders Generated from the Agreement""" self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': 'Orders', 'views': [[False, 'tree'], [False, 'form']], 'res_model': 'purchase.order', 'domain': [('recurring_agreement_id', '=', self.id)], 'context': "{'create': False}" } def action_generate_next_year_orders(self): """This will Generate Orders for Next year""" return self.generate_next_orders(years=1) def action_generate_initial_order(self): """This will generate the Initial purchase Order from the Purchase Agreement""" self.ensure_one() agreement_lines = self.mapped('agreement_line_ids').filtered( 'is_active') order = self.create_order(self.start_date, agreement_lines) self.write({'state': 'first'}) order.button_confirm() return { 'domain': "[('id', '=', %s)]" % order.id, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'purchase.order', 'context': self.env.context, 'res_id': order.id, 'view_id': [self.env.ref('purchase.purchase_order_form').id], 'type': 'ir.actions.act_window', 'nodestroy': True }