You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							404 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							404 lines
						
					
					
						
							18 KiB
						
					
					
				| # -*- coding: utf-8 -*- | |
| ################################################################################ | |
| # | |
| #    Cybrosys Technologies Pvt. Ltd. | |
| # | |
| #    Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |
| #    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 <http://www.gnu.org/licenses/>. | |
| # | |
| ################################################################################ | |
| 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 | |
|         }
 | |
| 
 |