diff --git a/revised_promise_date/__init__.py b/revised_promise_date/__init__.py new file mode 100644 index 00000000000..0a45e674f6a --- /dev/null +++ b/revised_promise_date/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/revised_promise_date/__manifest__.py b/revised_promise_date/__manifest__.py new file mode 100644 index 00000000000..4d70e0ae36c --- /dev/null +++ b/revised_promise_date/__manifest__.py @@ -0,0 +1,11 @@ +{ + 'name':'Revised Promise Date' , + 'version' : '1.0', + 'author' : "Lucky Prajapati" , + 'category' : 'Sale/Inventory', + 'depends' : ["sale_management",'stock','sale_stock'] , + 'data' : ["security/ir.model.access.csv","views/sale_order_views.xml","views/stock_picking_views.xml"], + 'installable' : True , + 'application' : True , + 'license' : 'LGPL-3' +} diff --git a/revised_promise_date/models/__init__.py b/revised_promise_date/models/__init__.py new file mode 100644 index 00000000000..65b33be322d --- /dev/null +++ b/revised_promise_date/models/__init__.py @@ -0,0 +1,3 @@ +from . import sale_order +from . import promise_date_record +from . import stock_picking diff --git a/revised_promise_date/models/promise_date_record.py b/revised_promise_date/models/promise_date_record.py new file mode 100644 index 00000000000..f7c93b0f2d4 --- /dev/null +++ b/revised_promise_date/models/promise_date_record.py @@ -0,0 +1,12 @@ +from odoo import models , fields + +class PromiseDateRecord(models.Model): + _name='promise.date.record' + _description="Store records of the promise date" + + sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete='cascade') + changed_by = fields.Many2one('res.users', string="Changed By", default=lambda self: self.env.user) + changed_on = fields.Date(string="Changed On", default=fields.Datetime.now) + from_date = fields.Date(string="Previous Revised Promise Date") + to_date = fields.Date(string="New Revised Promise Date") + \ No newline at end of file diff --git a/revised_promise_date/models/sale_order.py b/revised_promise_date/models/sale_order.py new file mode 100644 index 00000000000..6d219d3d9a3 --- /dev/null +++ b/revised_promise_date/models/sale_order.py @@ -0,0 +1,82 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError + +class SaleOrder(models.Model): + _inherit = "sale.order" + + original_promise_date = fields.Date() + revised_promise_date = fields.Date(tracking=True) + promise_date_history_ids = fields.One2many('promise.date.record', 'sale_order_id', string="Promise Date History") + + @api.onchange('original_promise_date') + def _onchange_original_promise_date(self): + """Changes the commitment(delivery) date on changes of original promise date when order is in quatation state""" + self.commitment_date = self.original_promise_date + + def action_confirm(self): + for order in self: + """Raise error if original promise date is not set""" + if not order.original_promise_date: + raise ValidationError("You cannot confirm this quotation without setting the Original Promise Date.") + + """Stores the first change in revised promise date None to set date""" + self.env['promise.date.record'].create({ + 'sale_order_id': order.id, + 'changed_by': self.env.user.id, + 'from_date': None, + 'to_date': order.original_promise_date, + }) + + """Changes the commitment(delivery) date when first time original promise date is set""" + order.commitment_date = order.original_promise_date + message = f"Revised Promise Date changed from {None} to {order.original_promise_date} by {self.env.user.name}" + order.message_post(body=message) + + return super(SaleOrder, self).action_confirm() + + def write(self,vals): + for record in self : + """Raise error on changing the original promise date after the confirmation of sale order""" + if 'original_promise_date' in vals and record.state == 'sale': + raise ValidationError("You cannot modify the Original Promise Date once the order is confirmed.") + + """Store the value of revised promise date before saving the record to the database""" + old_date = record.revised_promise_date + + result = super(SaleOrder,self).write(vals) + + """Store the new revised promise date and save that record to the promise.date.record model""" + for record in self: + new_date = record.revised_promise_date + if old_date != new_date : + record.commitment_date = new_date + if record.id : + self.env['promise.date.record'].create({ + 'sale_order_id':record.id, + 'changed_by':self.env.user.id, + 'from_date':old_date, + 'to_date':new_date, + }) + message = f"Revised Promise Date changed from {old_date or 'Empty'} to {new_date} by {self.env.user.name}" + record.message_post(body=message) + + return result + + def create(self, vals): + """Raise error on not setting original promise date""" + if not vals.get('original_promise_date') : + raise ValidationError("Set the Original Promise Date") + + """ Raise error on the set of original promise date lower than the order date""" + date_order_value = fields.Date.to_date(vals.get('date_order')) if vals.get('date_order') else None + original_promise_date_value = fields.Date.to_date(vals.get('original_promise_date')) if vals.get('original_promise_date') else None + + if date_order_value and original_promise_date_value and date_order_value > original_promise_date_value: + raise ValidationError("The Original Promise date must be greater than the order date") + + """Set the revised promise date on the first creation of the sale order""" + if not vals.get('revised_promise_date') and vals.get('original_promise_date'): + vals['revised_promise_date'] = vals['original_promise_date'] + + return super(SaleOrder, self).create(vals) + \ No newline at end of file diff --git a/revised_promise_date/models/stock_picking.py b/revised_promise_date/models/stock_picking.py new file mode 100644 index 00000000000..9a0f4df0c24 --- /dev/null +++ b/revised_promise_date/models/stock_picking.py @@ -0,0 +1,20 @@ +from odoo import models, fields ,api + +class StockPicking(models.Model): + _inherit = "stock.picking" + + original_promise_date = fields.Date("Original Promise Date" , compute='_compute_original_promise_date') + sale_order_date = fields.Datetime(related="sale_id.commitment_date", string="Sale Order Date", store=True) + sale_order_date_only = fields.Date(string="Sale Order Date Only",compute='_compute_order_date_only',store=True) + + @api.depends('sale_id.original_promise_date','date_deadline') + def _compute_original_promise_date(self): + """Set the original promise date of stock.picking model with the value of sale.order model's original promise date""" + for record in self : + record.original_promise_date = record.sale_id.original_promise_date + + @api.depends('sale_order_date') + def _compute_order_date_only(self): + """Converts the date_deadline field from datetime to date""" + for record in self: + record.sale_order_date_only = record.sale_order_date.date() if record.sale_order_date else False diff --git a/revised_promise_date/security/ir.model.access.csv b/revised_promise_date/security/ir.model.access.csv new file mode 100644 index 00000000000..335d5f66408 --- /dev/null +++ b/revised_promise_date/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_promise_date_record,access_promise_date_record,model_promise_date_record,base.group_user,1,1,1,1 diff --git a/revised_promise_date/tests/__init__.py b/revised_promise_date/tests/__init__.py new file mode 100644 index 00000000000..6f699d0d8ba --- /dev/null +++ b/revised_promise_date/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_order diff --git a/revised_promise_date/tests/test_sale_order.py b/revised_promise_date/tests/test_sale_order.py new file mode 100644 index 00000000000..2074adfdbb6 --- /dev/null +++ b/revised_promise_date/tests/test_sale_order.py @@ -0,0 +1,73 @@ +import logging +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +class TestSaleOrder(TransactionCase): + + def setUp(self): + """Set up test records before running test cases.""" + super(TestSaleOrder, self).setUp() + self.partner = self.env['res.partner'].create({ + 'name': 'Test Customer' + }) + self.sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'original_promise_date': '2024-03-15', + 'revised_promise_date': '2024-03-15', + }) + _logger.info("\n✅ Setup: Sale Order Created Successfully") + + def test_sale_order_creation(self): + """Test if a Sale Order is created successfully.""" + self.assertTrue(self.sale_order, "Sale Order should be created.") + self.assertEqual(str(self.sale_order.original_promise_date), '2024-03-15') + _logger.info("\n✅ Test Passed: Sale Order Creation") + + def test_original_promise_date_required(self): + """Test that a Sale Order cannot be confirmed without an Original Promise Date.""" + self.sale_order.original_promise_date = False + with self.assertRaises(ValidationError): + self.sale_order.action_confirm() + _logger.info("\n✅ Test Passed: Original Promise Date Required for Confirmation") + + def test_original_promise_date_readonly_after_confirmation(self): + """Test that Original Promise Date cannot be changed after order is confirmed.""" + self.sale_order.action_confirm() + with self.assertRaises(ValidationError): + self.sale_order.write({'original_promise_date': '2024-03-20'}) + _logger.info("\n✅ Test Passed: Original Promise Date Cannot be Modified After Confirmation") + + def test_revised_promise_date_defaults(self): + """Test that `revised_promise_date` defaults to `original_promise_date` if empty.""" + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'original_promise_date': '2024-04-01', + 'revised_promise_date': None, + }) + self.assertEqual(str(sale_order.revised_promise_date), '2024-04-01', + "Revised Promise Date should default to Original Promise Date") + _logger.info("\n✅ Test Passed: Revised Promise Date Defaults to Original Promise Date") + + def test_revised_promise_date_change_logs_history(self): + """Test if changing `revised_promise_date` logs it in the history table.""" + old_date = self.sale_order.revised_promise_date + new_date = '2024-03-20' + self.sale_order.write({'revised_promise_date': new_date}) + history_record = self.env['promise.date.record'].search([ + ('sale_order_id', '=', self.sale_order.id) + ], order="id desc", limit=1) + self.assertEqual(str(history_record.from_date), str(old_date)) + self.assertEqual(str(history_record.to_date), new_date) + _logger.info("\n✅ Test Passed: Revised Promise Date Change Logged in History") + + def test_promise_date_record_creation(self): + """Test that a promise date record is created when revised_promise_date changes.""" + self.sale_order.write({'revised_promise_date': '2024-03-25'}) + history_record = self.env['promise.date.record'].search([ + ('sale_order_id', '=', self.sale_order.id) + ], order="id desc", limit=1) + self.assertTrue(history_record, "A promise date record should be created.") + self.assertEqual(str(history_record.to_date), '2024-03-25') + _logger.info("\n✅ Test Passed: Promise Date Record Created") diff --git a/revised_promise_date/views/sale_order_views.xml b/revised_promise_date/views/sale_order_views.xml new file mode 100644 index 00000000000..38755786d37 --- /dev/null +++ b/revised_promise_date/views/sale_order_views.xml @@ -0,0 +1,28 @@ +<odoo> + <record id="sale_order_form_view_inherit" model="ir.ui.view"> + <field name="name">sale.order.form.inherit.revised.date</field> + <field name="model">sale.order</field> + <field name="inherit_id" ref="sale.view_order_form" /> + <field name="arch" type="xml"> + <xpath expr="//field[@name='payment_term_id']" position="after"> + <field name="original_promise_date" string="Original Promise Date" /> + <field name="revised_promise_date" string="Revised Promise Date" invisible="state != 'sale'" /> + </xpath> + <xpath expr="//field[@name='commitment_date']" position="attributes"> + <attribute name="readonly">True</attribute> + </xpath> + <xpath expr="//page" position="after"> + <page string="Promise Date History" name="promise_date_history"> + <field name="promise_date_history_ids" nolabel="1"> + <list> + <field name="changed_on"/> + <field name="changed_by"/> + <field name="from_date"/> + <field name="to_date"/> + </list> + </field> + </page> + </xpath> + </field> + </record> +</odoo> diff --git a/revised_promise_date/views/stock_picking_views.xml b/revised_promise_date/views/stock_picking_views.xml new file mode 100644 index 00000000000..0c63dbbea4e --- /dev/null +++ b/revised_promise_date/views/stock_picking_views.xml @@ -0,0 +1,16 @@ +<odoo> +<record id="stock_picking_form_view" model='ir.ui.view'> + <field name='name'>stock.picking.form.view.inherit.revised.date</field> + <field name='model'>stock.picking</field> + <field name='inherit_id' ref='stock.view_picking_form'/> + <field name='arch' type='xml'> + <xpath expr="//field[@name='date_deadline']" position='before'> + <field name="original_promise_date" readonly='1'/> + </xpath> + <xpath expr="//field[@name='date_deadline']" position="attributes"> + <attribute name="string">Revised Promise Date</attribute> + <attribute name="decoration-danger">sale_order_date_only != original_promise_date</attribute> + </xpath> + </field> +</record> +</odoo>