diff --git a/custom_report/__init__.py b/custom_report/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/custom_report/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/custom_report/__manifest__.py b/custom_report/__manifest__.py new file mode 100644 index 00000000000..977d19e181b --- /dev/null +++ b/custom_report/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'Custom Picking Report', + 'version': '1.0', + 'summary': 'Add a custom report in sales module', + 'description': """ +Checking Product Quantity +========================== +In this module, add a custom report in which no sub kitt product will not print in report. + + """, + 'author': 'Raghav Agiwal', + 'depends': ['sale_management', 'mrp', 'website'], + 'data': [ + 'report/custom_report_mo_views.xml', + "report/custom_report_views.xml" + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3' +} diff --git a/custom_report/report/custom_report_mo_views.xml b/custom_report/report/custom_report_mo_views.xml new file mode 100644 index 00000000000..a92c9020afb --- /dev/null +++ b/custom_report/report/custom_report_mo_views.xml @@ -0,0 +1,15 @@ + + + + + Mo Delivery note + stock.picking + qweb-pdf + custom_report.custom_report_mo_views + custom_report.custom_report_mo_views + 'Mo Delivery Note -- %s - %s' % (object.partner_id.name or '', object.name) + + report + + + diff --git a/custom_report/report/custom_report_views.xml b/custom_report/report/custom_report_views.xml new file mode 100644 index 00000000000..e8496452579 --- /dev/null +++ b/custom_report/report/custom_report_views.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/dev_zero_stock_blockage/__init__.py b/dev_zero_stock_blockage/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/dev_zero_stock_blockage/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/dev_zero_stock_blockage/__manifest__.py b/dev_zero_stock_blockage/__manifest__.py new file mode 100644 index 00000000000..d664993c0e1 --- /dev/null +++ b/dev_zero_stock_blockage/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Dev Zero Stock Blockage', + 'version': '1.0', + 'summary': 'Add zero stock blockage feature to sale order', + 'description': """ +Checking Product Quantity +========================== +This module stops Sales Orders from being confirmed if any product is out of stock, unless a Sales Manager gives approval. + +Sales users can see the Zero Stock Approval field but cannot edit it. + +It only checks stock for physical products, not services or combos. + """, + 'author': 'Raghav Agiwal', + 'depends': ['sale_management', 'stock'], + 'data': [ + 'views/sale_order_views.xml', + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3' +} diff --git a/dev_zero_stock_blockage/models/__init__.py b/dev_zero_stock_blockage/models/__init__.py new file mode 100644 index 00000000000..6aacb753131 --- /dev/null +++ b/dev_zero_stock_blockage/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/dev_zero_stock_blockage/models/sale_order.py b/dev_zero_stock_blockage/models/sale_order.py new file mode 100644 index 00000000000..c83fab92d6d --- /dev/null +++ b/dev_zero_stock_blockage/models/sale_order.py @@ -0,0 +1,74 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + zero_stock_approval = fields.Boolean( + string="Approval", + copy=False, + ) + + @api.onchange('zero_stock_approval') + def _onchange_zero_stock_approval(self): + if self.zero_stock_approval and self.env.user.has_group('sales_team.group_sale_manager'): + warnings = [] + for line in self.order_line: + if line.product_id.type == 'consu' and line.product_uom_qty > line.product_id.qty_available: + warnings.append( + f"Product '{line.product_id.display_name}' has demand {line.product_uom_qty} > available {line.product_id.qty_available}." + ) + if warnings: + return { + 'warning': { + 'title': "Heads Up – Stock Alert ⚠️", + 'message': ( + "You're approving an order where some products have lower available stock than requested quantity:\n\n" + + "\n".join(warnings) + + "\n\nIf you're sure about this decision, you can continue. Otherwise, consider adjusting the quantities or stock." + ) + } + } + + @api.model + def fields_get(self, allfields=None, attributes=None): + fields = super().fields_get(allfields=allfields, attributes=attributes) + if not self.env.user.has_group('sales_team.group_sale_manager'): + if "zero_stock_approval" in fields: + fields['zero_stock_approval']['readonly'] = True + return fields + + def action_confirm(self): + for order in self: + stock_issues = [] + + for line in order.order_line: + demand_qty = line.product_uom_qty + available_qty = line.product_id.qty_available + + # if demand_qty <= 0: + # raise UserError( + # f"You cannot confirm this Sale Order.\n" + # f"Product '{line.product_id.display_name}' has a quantity of {demand_qty}.\n" + # f"Quantity must be greater than zero." + # ) + + if ( + line.product_id.type == 'consu' + and demand_qty > available_qty + and not self.env.user.has_group('sales_team.group_sale_manager') + and not order.zero_stock_approval + ): + stock_issues.append( + f"- {line.product_id.display_name}: Requested {demand_qty}, Available {available_qty}" + ) + + if stock_issues: + raise UserError( + "Cannot confirm this Sale Order due to insufficient stock:\n\n" + + "\n".join(stock_issues) + + "\n\nPlease get approval or adjust the quantities." + ) + + return super().action_confirm() diff --git a/dev_zero_stock_blockage/views/sale_order_views.xml b/dev_zero_stock_blockage/views/sale_order_views.xml new file mode 100644 index 00000000000..158cbf85cbd --- /dev/null +++ b/dev_zero_stock_blockage/views/sale_order_views.xml @@ -0,0 +1,13 @@ + + + + sale.order.form + sale.order + + + + + + + + diff --git a/event_limit_reg/__init__.py b/event_limit_reg/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/event_limit_reg/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/event_limit_reg/__manifest__.py b/event_limit_reg/__manifest__.py new file mode 100644 index 00000000000..96a44aca78e --- /dev/null +++ b/event_limit_reg/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Event limit registration', + 'version': '1.0', + 'summary': 'Event limit registration', + 'author': 'Raghav Agiwal', + 'depends': ['website_event', 'website'], + 'data': [ + 'views/event_ticket_view.xml', + 'views/event_registration_website_view.xml', + ], + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3' +} diff --git a/event_limit_reg/models/__init__.py b/event_limit_reg/models/__init__.py new file mode 100644 index 00000000000..8ae2880cab4 --- /dev/null +++ b/event_limit_reg/models/__init__.py @@ -0,0 +1 @@ +from .import event_ticket diff --git a/event_limit_reg/models/event_ticket.py b/event_limit_reg/models/event_ticket.py new file mode 100644 index 00000000000..927193e50e9 --- /dev/null +++ b/event_limit_reg/models/event_ticket.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class EventTicket(models.Model): + _inherit = "event.event.ticket" + + max_registration_per_user_limit = fields.Integer( + string="Tickets limit per user", + help="maximum number of tickets that a single user can book for this event", + ) diff --git a/event_limit_reg/views/event_registration_website_view.xml b/event_limit_reg/views/event_registration_website_view.xml new file mode 100644 index 00000000000..21741ad2f94 --- /dev/null +++ b/event_limit_reg/views/event_registration_website_view.xml @@ -0,0 +1,20 @@ + + + + diff --git a/event_limit_reg/views/event_ticket_view.xml b/event_limit_reg/views/event_ticket_view.xml new file mode 100644 index 00000000000..9abd23b06f4 --- /dev/null +++ b/event_limit_reg/views/event_ticket_view.xml @@ -0,0 +1,15 @@ + + + + + event.event.ticket.form.inherit.custom + event.event.ticket + + + + + + + + + diff --git a/new_product_type_kit/__init__.py b/new_product_type_kit/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/new_product_type_kit/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/new_product_type_kit/__manifest__.py b/new_product_type_kit/__manifest__.py new file mode 100644 index 00000000000..92854a83e43 --- /dev/null +++ b/new_product_type_kit/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Dev Zero Stock Blockage', + 'version': '1.0', + 'summary': 'Add a new prodcut type kit', + 'description': """ + Add kit-type products with configurable sub-products and conditional report visibility + """, + 'author': 'Raghav Agiwal', + 'depends': ['sale_management', 'stock', 'product'], + 'data': [ + 'security/ir.model.access.csv', + 'views/product_template_view.xml', + 'views/sale_order_line_view.xml', + 'views/kit_wizard_views.xml', + 'views/portal_saleorder_templates.xml', + 'report/report_saleorder_templates.xml', + 'report/report_invoice_templates.xml', + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3' +} diff --git a/new_product_type_kit/models/__init__.py b/new_product_type_kit/models/__init__.py new file mode 100644 index 00000000000..53fa79af356 --- /dev/null +++ b/new_product_type_kit/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order +from . import sale_order_line diff --git a/new_product_type_kit/models/product_template.py b/new_product_type_kit/models/product_template.py new file mode 100644 index 00000000000..1c745b3161c --- /dev/null +++ b/new_product_type_kit/models/product_template.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_kit = fields.Boolean(string="Is Kit") + sub_product_ids = fields.Many2many( + 'product.product', + string="Sub Products", + required=True, + ) diff --git a/new_product_type_kit/models/sale_order.py b/new_product_type_kit/models/sale_order.py new file mode 100644 index 00000000000..b81ca96b0d8 --- /dev/null +++ b/new_product_type_kit/models/sale_order.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + print_in_report = fields.Boolean( + string="Print in report?", + default=False + ) diff --git a/new_product_type_kit/models/sale_order_line.py b/new_product_type_kit/models/sale_order_line.py new file mode 100644 index 00000000000..104ac517e67 --- /dev/null +++ b/new_product_type_kit/models/sale_order_line.py @@ -0,0 +1,42 @@ +from odoo import _, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_kit = fields.Boolean(related="product_template_id.is_kit") + is_kit_component = fields.Boolean(string="Is Subproduct") + kit_parent_line_id = fields.Many2one('sale.order.line', string="Kit Parent Line", ondelete="cascade") + kit_unit_cost = fields.Float(string="Unit Price (Wizard)", default=0.0) + + def action_open_kit_wizard(self): + return { + 'name': 'Kit Components', + 'type': 'ir.actions.act_window', + 'res_model': 'kit.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + "active_id": self.order_id.id, + 'default_product_id': self.product_id.id, + 'default_sale_order_line_id': self.id, + } + } + + def unlink(self): + # Identify sub-product lines + sub_products = self.filtered(lambda line: line.is_kit_component) + + if sub_products and not self.env.context.get('allow_sub_product_deletion'): + if self == sub_products: + raise models.UserError(_("You cannot delete kit sub-products directly. Delete the main kit line instead.")) + return (self - sub_products).unlink() + + child_lines = self.env['sale.order.line'].search([ + ('kit_parent_line_id', 'in', self.ids) + ]) + + if child_lines: + child_lines.with_context(allow_sub_product_deletion=True).unlink() + + return super().unlink() diff --git a/new_product_type_kit/report/report_invoice_templates.xml b/new_product_type_kit/report/report_invoice_templates.xml new file mode 100644 index 00000000000..d02d05e3b3f --- /dev/null +++ b/new_product_type_kit/report/report_invoice_templates.xml @@ -0,0 +1,12 @@ + + + + diff --git a/new_product_type_kit/report/report_saleorder_templates.xml b/new_product_type_kit/report/report_saleorder_templates.xml new file mode 100644 index 00000000000..6f6b72cfaf5 --- /dev/null +++ b/new_product_type_kit/report/report_saleorder_templates.xml @@ -0,0 +1,10 @@ + + + + diff --git a/new_product_type_kit/security/ir.model.access.csv b/new_product_type_kit/security/ir.model.access.csv new file mode 100644 index 00000000000..d0b6ac0f177 --- /dev/null +++ b/new_product_type_kit/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_kit_wizard,access.kit.wizard,model_kit_wizard,,1,1,1,1 +access_kit_wizard_line,access.kit.wizard.line,model_kit_wizard_line,,1,1,1,1 diff --git a/new_product_type_kit/views/kit_wizard_views.xml b/new_product_type_kit/views/kit_wizard_views.xml new file mode 100644 index 00000000000..f8d188f40fc --- /dev/null +++ b/new_product_type_kit/views/kit_wizard_views.xml @@ -0,0 +1,29 @@ + + + + kit.wizard.form + kit.wizard + +
+ + + + + + + + + + + + + + + +
+
+
+
diff --git a/new_product_type_kit/views/portal_saleorder_templates.xml b/new_product_type_kit/views/portal_saleorder_templates.xml new file mode 100644 index 00000000000..2a17eba0a4c --- /dev/null +++ b/new_product_type_kit/views/portal_saleorder_templates.xml @@ -0,0 +1,10 @@ + + + + diff --git a/new_product_type_kit/views/product_template_view.xml b/new_product_type_kit/views/product_template_view.xml new file mode 100644 index 00000000000..036ae8110c0 --- /dev/null +++ b/new_product_type_kit/views/product_template_view.xml @@ -0,0 +1,14 @@ + + + + product.template.kit.form + product.template + + + + + + + + + diff --git a/new_product_type_kit/views/sale_order_line_view.xml b/new_product_type_kit/views/sale_order_line_view.xml new file mode 100644 index 00000000000..f1319711b44 --- /dev/null +++ b/new_product_type_kit/views/sale_order_line_view.xml @@ -0,0 +1,28 @@ + + + sale.order.line.kit.button + sale.order + + + + + + + + + + + + is_kit_component + + + + is_kit_component + + + + is_kit_component + + + + diff --git a/new_product_type_kit/wizard/__init__.py b/new_product_type_kit/wizard/__init__.py new file mode 100644 index 00000000000..67e0de0e3e5 --- /dev/null +++ b/new_product_type_kit/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import kit_wizard +from . import kit_wizard_line diff --git a/new_product_type_kit/wizard/kit_wizard.py b/new_product_type_kit/wizard/kit_wizard.py new file mode 100644 index 00000000000..12fe8f7592b --- /dev/null +++ b/new_product_type_kit/wizard/kit_wizard.py @@ -0,0 +1,79 @@ +from odoo import api, Command, fields, models + + +class KitWizard(models.TransientModel): + _name = 'kit.wizard' + _description = 'Kit Wizard' + + product_id = fields.Many2one('product.product', string='Product', required=True) + kit_line_ids = fields.One2many('kit.wizard.line', 'wizard_id', string="Sub Products") + sale_order_line_id = fields.Many2one('sale.order.line', string="Parent Line") + + @api.model + def default_get(self, fields): + res = super().default_get(fields) + product_id = self.env.context.get("default_product_id") + order_id = self.env.context.get("active_id") + sale_order_line_id = self.env.context.get("default_sale_order_line_id") + + if not product_id or not order_id: + return res + + product = self.env["product.product"].browse(product_id) + order = self.env["sale.order"].browse(order_id) + sub_products = product.sub_product_ids + lines = [] + + for sub in sub_products: + existing_line = order.order_line.filtered( + lambda l: l.product_id.id == sub.id + and l.kit_parent_line_id + and l.kit_parent_line_id.id == sale_order_line_id + ) + lines.append(Command.create({ + 'product_id': sub.id, + 'quantity': existing_line.product_uom_qty if existing_line else 1.0, + 'price': existing_line.kit_unit_cost if existing_line else sub.lst_price, + })) + + res.update({ + 'product_id': product_id, + 'kit_line_ids': lines, + 'sale_order_line_id': sale_order_line_id, + }) + return res + + def action_confirm(self): + order_id = self.env.context.get("active_id") + sale_order_line_id = self.env.context.get("default_sale_order_line_id") + order = self.env["sale.order"].browse(order_id) + parent_line = order.order_line.filtered(lambda l: l.id == sale_order_line_id) + total_price = self.product_id.lst_price + + for line in self.kit_line_ids: + existing_line = order.order_line.filtered( + lambda l: l.product_id.id == line.product_id.id + and l.kit_parent_line_id + and l.kit_parent_line_id.id == sale_order_line_id + ) + if existing_line: + existing_line.write({ + 'product_uom_qty': line.quantity, + 'kit_unit_cost': line.price, + 'price_unit': 0, + }) + else: + self.env['sale.order.line'].create({ + 'order_id': order.id, + 'product_id': line.product_id.id, + 'product_uom_qty': line.quantity, + 'kit_unit_cost': line.price, + 'price_unit': 0.0, + 'is_kit_component': True, + "sequence": parent_line.sequence, + 'kit_parent_line_id': sale_order_line_id, + # 'name': product.name, + }) + + total_price += line.quantity * line.price + parent_line.write({"price_unit": total_price}) diff --git a/new_product_type_kit/wizard/kit_wizard_line.py b/new_product_type_kit/wizard/kit_wizard_line.py new file mode 100644 index 00000000000..036500f0ce7 --- /dev/null +++ b/new_product_type_kit/wizard/kit_wizard_line.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class KitWizardLine(models.TransientModel): + _name = 'kit.wizard.line' + _description = 'Kit Wizard Line' + + wizard_id = fields.Many2one('kit.wizard', string="Wizard", required=True, ondelete="cascade") + product_id = fields.Many2one('product.product', string="Sub Product") + quantity = fields.Float(string="Quantity", default=1.0, required=True) + price = fields.Float(string="Price") diff --git a/pos_customer_display/__init__.py b/pos_customer_display/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pos_customer_display/__manifest__.py b/pos_customer_display/__manifest__.py new file mode 100644 index 00000000000..c84eaf75af0 --- /dev/null +++ b/pos_customer_display/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'POS Customer Display Name', + 'version': '1.0', + 'summary': 'Adding POS customer name on payment screeen', + 'description': """ +Showing Customer Diplay Name +============================ +This module will show customer name on the customer display. + +also will show refund lines septarately under refund subsection. + """, + 'author': 'Raghav Agiwal', + 'depends': ['point_of_sale'], + "assets": { + 'point_of_sale.assets_prod': [ + 'pos_customer_display/static/src/pos_order.js', + ], + 'point_of_sale.customer_display_assets': [ + 'pos_customer_display/static/src/customer_display/*', + ], + }, + 'installable': True, + 'license': 'LGPL-3' +} diff --git a/pos_customer_display/static/src/customer_display/customer_display.xml b/pos_customer_display/static/src/customer_display/customer_display.xml new file mode 100644 index 00000000000..b8476585053 --- /dev/null +++ b/pos_customer_display/static/src/customer_display/customer_display.xml @@ -0,0 +1,28 @@ + + + + +
+
+
Customer
+
+
+ +
+
Amount
+
/ Guest +
+ +
+ + + + + + +
Refunded Items
+ +
+
+
+ diff --git a/pos_customer_display/static/src/pos_order.js b/pos_customer_display/static/src/pos_order.js new file mode 100644 index 00000000000..7b215cdb2ed --- /dev/null +++ b/pos_customer_display/static/src/pos_order.js @@ -0,0 +1,23 @@ +import { patch } from "@web/core/utils/patch"; +import { PosOrder } from "@point_of_sale/app/models/pos_order"; + +patch(PosOrder.prototype, { + getCustomerDisplayData() { + const allLines = this.getSortedOrderlines(); + const returnedItems = allLines.filter(line => line.refunded_orderline_id); + const saleItems = allLines.filter(line => !line.refunded_orderline_id); + + const guests = this.guest_count || 0; + const total = this.get_total_with_tax(); + const perGuestAmount = guests > 0 ? total / guests : null; + + return { + ...super.getCustomerDisplayData(), + + displayCustomerName: this.get_partner_name() || "Guest", + amountDividedPerGuest: perGuestAmount, + refundedItems: returnedItems.map(line => line.getDisplayData()), + activeItems: saleItems.map(line => line.getDisplayData()), + }; + }, +}); diff --git a/pos_order_workflow/__init__.py b/pos_order_workflow/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pos_order_workflow/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_order_workflow/__manifest__.py b/pos_order_workflow/__manifest__.py new file mode 100644 index 00000000000..192aa2b87d7 --- /dev/null +++ b/pos_order_workflow/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'POS Order Workflow', + 'version': '1.0', + 'summary': 'POS Order Workflow', + 'author': 'Raghav Agiwal', + 'depends': ['point_of_sale'], + "assets": { + "point_of_sale._assets_pos": [ + "pos_order_workflow/static/src/*", + ], + }, + 'installable': True, + 'application': True, + 'auto_install': True, + 'license': 'LGPL-3' +} diff --git a/pos_order_workflow/models/__init__.py b/pos_order_workflow/models/__init__.py new file mode 100644 index 00000000000..e9ab911ddcc --- /dev/null +++ b/pos_order_workflow/models/__init__.py @@ -0,0 +1 @@ +from . import pos_order diff --git a/pos_order_workflow/models/pos_order.py b/pos_order_workflow/models/pos_order.py new file mode 100644 index 00000000000..e372ae37f1d --- /dev/null +++ b/pos_order_workflow/models/pos_order.py @@ -0,0 +1,44 @@ +import logging +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = "pos.order" + + def create_picking_set_shipping(self, order_id): + order = self.browse(order_id) + order.ensure_one() + + if order.name == "/": + order.name = order._compute_order_name() + + if not order.shipping_date: + order.shipping_date = fields.Date.today() + + if not order.picking_ids: + order._create_order_picking() + + def check_existing_picking(self, order_id): + order = self.search([('access_token', '=', order_id)], limit=1) + return { + 'hasPicking': bool(order.picking_ids), + 'trackingNumber': order.tracking_number or 'N/A' + } + + def _process_saved_order(self, draft): + self.ensure_one() + if not draft: + self.action_pos_order_paid() + + if not self.picking_ids: + self._create_order_picking() + + self.picking_ids.scheduled_date = self.shipping_date + self._compute_total_cost_in_real_time() + + if self.to_invoice and self.state == 'paid': + self._generate_pos_order_invoice() + + return self.id diff --git a/pos_order_workflow/static/src/send_to_pick.js b/pos_order_workflow/static/src/send_to_pick.js new file mode 100644 index 00000000000..d7dc2fa577b --- /dev/null +++ b/pos_order_workflow/static/src/send_to_pick.js @@ -0,0 +1,53 @@ +import { patch } from "@web/core/utils/patch"; +import { _t } from "@web/core/l10n/translation"; +import { useService } from "@web/core/utils/hooks"; +import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; + +patch(ProductScreen.prototype, { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + }, + + async checkOrderPickingStatus(order_id) { + const result = await this.orm.call("pos.order","check_existing_picking",["pos.order", order_id]); + return result; + }, + + async sendToPick() { + const currentOrder = this.pos.get_order(); + if (!currentOrder) { + this.notification.add(_t("No active order found!"), { type: "warning" }); + return; + } + + const { hasPicking, trackingNumber } = await this.checkOrderPickingStatus(currentOrder.access_token); + if (hasPicking) { + this.notification.add(_t(`Order Already Processed. This order already has a picking associated with it. + Tracking Number : (${trackingNumber})`), { + type: "danger", + }) + return; + } + + if (!currentOrder.partner_id) { + this.notification.add(_t(`Select Customer`), { + type: "warning", + }) + return; + } + + if (!currentOrder.shipping_date) { + const today = new Date().toISOString().split("T")[0]; + currentOrder.shipping_date = today; + } + + const syncOrderResult = await this.pos.push_single_order(currentOrder); + + await this.orm.call("pos.order", "create_picking_set_shipping", ["pos.order" , syncOrderResult[0].id]); + + this.notification.add(_t("Order Sent to Pick"), { + type: "success", + }); + }, +}); diff --git a/pos_order_workflow/static/src/send_to_pick.xml b/pos_order_workflow/static/src/send_to_pick.xml new file mode 100644 index 00000000000..02a4b3bc02a --- /dev/null +++ b/pos_order_workflow/static/src/send_to_pick.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/pos_second_uom/__init__.py b/pos_second_uom/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pos_second_uom/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_second_uom/__manifest__.py b/pos_second_uom/__manifest__.py new file mode 100644 index 00000000000..f07dc562acd --- /dev/null +++ b/pos_second_uom/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'POS Second UOM', + 'version': '1.0', + 'summary': 'Allows products to have a second unit of measure in POS', + 'description': """ + Adds the ability to set and use a second unit of measure for products in the Point of Sale. + This is useful for businesses that sell products in multiple units. + The second UOM is displayed and managed directly within the POS interface for easier sales handling. + """, + 'author': 'Raghav Agiwal', + 'depends': ['point_of_sale'], + 'data': [ + 'views/product_template_view.xml', + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'pos_second_uom/static/src/**/*', + ], + }, + 'installable': True, + 'application': True, + 'auto_install': True, + 'license': 'LGPL-3' +} diff --git a/pos_second_uom/models/__init__.py b/pos_second_uom/models/__init__.py new file mode 100644 index 00000000000..6a7d91cee2d --- /dev/null +++ b/pos_second_uom/models/__init__.py @@ -0,0 +1,2 @@ +from . import product +from . import product_template diff --git a/pos_second_uom/models/product.py b/pos_second_uom/models/product.py new file mode 100644 index 00000000000..f5778caf629 --- /dev/null +++ b/pos_second_uom/models/product.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class Product(models.Model): + _inherit = 'product.product' + + @api.model + def _load_pos_data_fields(self, config_id): + pos_fields = super()._load_pos_data_fields(config_id) + pos_fields.extend(['pos_second_uom_id']) + return pos_fields diff --git a/pos_second_uom/models/product_template.py b/pos_second_uom/models/product_template.py new file mode 100644 index 00000000000..8541e688209 --- /dev/null +++ b/pos_second_uom/models/product_template.py @@ -0,0 +1,27 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + pos_second_uom_id = fields.Many2one( + 'uom.uom', + string="POS Second UoM", + domain="[('category_id', '=', uom_category_id)]" + ) + + uom_category_id = fields.Many2one( + 'uom.category', + string="UoM Category", + related='uom_id.category_id', + ) + + @api.constrains('uom_id', 'pos_second_uom_id') + def _check_secondary_uom_not_same_as_primary(self): + if self.pos_second_uom_id and self.uom_id == self.pos_second_uom_id: + raise ValidationError( + f"Configuration Error:\n\n" + f'Product "{self.name}" has "{self.uom_id.name}" set as both primary and secondary Unit of Measure.\n' + f'These must be different. Please select a different second UoM for POS operations.' + ) diff --git a/pos_second_uom/static/src/app/control_buttons/control_button.js b/pos_second_uom/static/src/app/control_buttons/control_button.js new file mode 100644 index 00000000000..e3b1ae5d106 --- /dev/null +++ b/pos_second_uom/static/src/app/control_buttons/control_button.js @@ -0,0 +1,40 @@ +/** @odoo-module **/ + +import { AddQuantityDialog } from "./dialog_box/add_quantity_dialog"; +import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; +import { useService } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; + +patch(ControlButtons.prototype, { + setup() { + super.setup(); + this.dialog = useService("dialog"); + this.pos = this.env.services.pos; + }, + + get secondUomName() { + const line = this.pos.get_order().get_selected_orderline(); + return line?.get_product()?.pos_second_uom_id?.name; + }, + + get shouldShowAddQtyButton() { + const order = this.pos.get_order(); + const line = order?.get_selected_orderline(); + return !!line?.get_product()?.pos_second_uom_id; + }, + + onAddQuantity() { + const order = this.pos.get_order(); + const line = order.get_selected_orderline(); + + if (!line || !line.get_product()?.pos_second_uom_id) { + return; + } + + this.dialog.add(AddQuantityDialog, { + product: line.get_product(), + orderline: line, + secondUomName: this.secondUomName, + }); + }, +}); diff --git a/pos_second_uom/static/src/app/control_buttons/control_button.xml b/pos_second_uom/static/src/app/control_buttons/control_button.xml new file mode 100644 index 00000000000..03e046c32d2 --- /dev/null +++ b/pos_second_uom/static/src/app/control_buttons/control_button.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.js b/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.js new file mode 100644 index 00000000000..85ecb36e7bd --- /dev/null +++ b/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import { Component, useRef } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class AddQuantityDialog extends Component { + static template = "pos_second_uom.AddQuantityDialog"; + static components = { Dialog }; + static props = { + close: { type: Function }, + product: { type: Object }, + orderline: { type: Object }, + secondUomName: { type: String, optional: true }, + }; + + setup() { + this.pos = usePos(); + this.input_qty = useRef("quantityInput"); + this.uom_ratio = this.computeUomRatio(this.props.product); + this.dialog = useService("dialog"); + } + + computeUomRatio(product) { + if (product.uom_id && product.pos_second_uom_id) { + return product.uom_id.factor / product.pos_second_uom_id.factor; + } + return 1; + } + + confirm() { + const quantity = parseFloat(this.input_qty.el.value); + if (!quantity || quantity <= 0) { + this.pos.dialog.add(AlertDialog, { + title: _t("Error"), + body: _t("Oops!!!!! Quantity cannot be negative, zero, or empty. Please enter a valid positive number !!"), + }); + return; + } + this.props.orderline.set_quantity(quantity * this.uom_ratio); + this.props.close(); + } +} diff --git a/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.xml b/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.xml new file mode 100644 index 00000000000..a90ed99b200 --- /dev/null +++ b/pos_second_uom/static/src/app/control_buttons/dialog_box/add_quantity_dialog.xml @@ -0,0 +1,20 @@ + + + + + +
+ + +
+
+ + + + +
+
+
diff --git a/pos_second_uom/views/product_template_view.xml b/pos_second_uom/views/product_template_view.xml new file mode 100644 index 00000000000..c304a670504 --- /dev/null +++ b/pos_second_uom/views/product_template_view.xml @@ -0,0 +1,13 @@ + + + + product.template.form.inherit.uom + product.template + + + + + + + + diff --git a/redesign_catalog/__manifest__.py b/redesign_catalog/__manifest__.py new file mode 100644 index 00000000000..8bd8e255723 --- /dev/null +++ b/redesign_catalog/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Redesign Catalog View', + 'version': '1.0', + 'summary': 'Redesign Catalog View', + 'author': 'Raghav Agiwal', + 'depends': ['sale_management', 'stock'], + 'data': [ + 'views/product_template_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'redesign_catalog/static/src/scss/image_style.scss', + 'redesign_catalog/static/src/component/product_image_popup.js', + ], + }, + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3' +} diff --git a/redesign_catalog/static/src/component/product_image_popup.js b/redesign_catalog/static/src/component/product_image_popup.js new file mode 100644 index 00000000000..9b0f2e87f20 --- /dev/null +++ b/redesign_catalog/static/src/component/product_image_popup.js @@ -0,0 +1,51 @@ +/** @odoo-module **/ + +import { ProductCatalogKanbanRecord } from "@product/product_catalog/kanban_record"; +import { productCatalogKanbanView } from "@product/product_catalog/kanban_view"; +import { ProductCatalogKanbanRenderer } from "@product/product_catalog/kanban_renderer"; +import { registry } from "@web/core/registry"; +import { useFileViewer } from "@web/core/file_viewer/file_viewer_hook"; + +export class InheritProductMobileCatalog extends ProductCatalogKanbanRecord { + setup() { + super.setup(); + this.fileViewer = useFileViewer(); + } + + onGlobalClick(e) { + const imageContainer = e.target.closest(".o_product_image"); + if (imageContainer) { + const imageUrl = e.target.getAttribute("src"); + if (imageUrl) { + this.openImagePreview(imageUrl); + return; + } + } + super.onGlobalClick(e); + } + + openImagePreview(imageUrl) { + const fileModel = { + isImage: true, + isViewable: true, + displayName: imageUrl, + defaultSource: imageUrl, + downloadUrl: imageUrl, + }; + this.fileViewer.open(fileModel); + } +} + +export class InheritProductMobileCatalogRenderer extends ProductCatalogKanbanRenderer { + static components = { + ...ProductCatalogKanbanRenderer.components, + KanbanRecord: InheritProductMobileCatalog, + }; +} + +export const ProductMobileKanbanCatalogView = { + ...productCatalogKanbanView, + Renderer: InheritProductMobileCatalogRenderer, +}; + +registry.category("views").add("product_mobile_kanban_catalog", ProductMobileKanbanCatalogView); diff --git a/redesign_catalog/static/src/scss/image_style.scss b/redesign_catalog/static/src/scss/image_style.scss new file mode 100644 index 00000000000..215e9174dd9 --- /dev/null +++ b/redesign_catalog/static/src/scss/image_style.scss @@ -0,0 +1,9 @@ +@media (max-width: 768px) { + .o_product_image img { + max-width: 150px !important; + max-height: 150px !important; + width: 150px !important; + height: 150px !important; + object-fit: cover; + } +} diff --git a/redesign_catalog/views/product_template_views.xml b/redesign_catalog/views/product_template_views.xml new file mode 100644 index 00000000000..27f9de80886 --- /dev/null +++ b/redesign_catalog/views/product_template_views.xml @@ -0,0 +1,18 @@ + + + + + product.product.view.kanban.inherit + product.product + + + + + + + product_mobile_kanban_catalog + + + + + diff --git a/salesperson_button_in_pos/__init__.py b/salesperson_button_in_pos/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/salesperson_button_in_pos/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/salesperson_button_in_pos/__manifest__.py b/salesperson_button_in_pos/__manifest__.py new file mode 100644 index 00000000000..34eaea3cf52 --- /dev/null +++ b/salesperson_button_in_pos/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Salesperson button in POS', + 'version': '1.0', + 'summary': 'Adding a salesperson button in pos', + 'description': """ + Adding salesperson button in pos + """, + 'author': 'Raghav Agiwal', + 'depends': ['point_of_sale', 'hr'], + 'data': [ + 'views/pos_order_view.xml', + 'views/employee_form_view.xml', + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'salesperson_button_in_pos/static/src/app/**/*', + ], + }, + 'installable': True, + 'application': True, + 'license': 'LGPL-3' +} diff --git a/salesperson_button_in_pos/models/__init__.py b/salesperson_button_in_pos/models/__init__.py new file mode 100644 index 00000000000..48e4effd57e --- /dev/null +++ b/salesperson_button_in_pos/models/__init__.py @@ -0,0 +1,3 @@ +from . import pos_order +from . import pos_session +from . import hr_employee diff --git a/salesperson_button_in_pos/models/hr_employee.py b/salesperson_button_in_pos/models/hr_employee.py new file mode 100644 index 00000000000..1619f547b97 --- /dev/null +++ b/salesperson_button_in_pos/models/hr_employee.py @@ -0,0 +1,5 @@ +from odoo import models + + +class HrEmployee(models.Model): + _inherit = "hr.employee" diff --git a/salesperson_button_in_pos/models/pos_order.py b/salesperson_button_in_pos/models/pos_order.py new file mode 100644 index 00000000000..9d73dd296c4 --- /dev/null +++ b/salesperson_button_in_pos/models/pos_order.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + salesperson_id = fields.Many2one('hr.employee', string='Salesperson') diff --git a/salesperson_button_in_pos/models/pos_session.py b/salesperson_button_in_pos/models/pos_session.py new file mode 100644 index 00000000000..20fd6f6fc27 --- /dev/null +++ b/salesperson_button_in_pos/models/pos_session.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class PosSession(models.Model): + _inherit = "pos.session" + + @api.model + def _load_pos_data_models(self, config_id): + data = super()._load_pos_data_models(config_id) + data.append("hr.employee") + return data diff --git a/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.js b/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.js new file mode 100644 index 00000000000..2f57f834ef9 --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.js @@ -0,0 +1,93 @@ +import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { Dialog } from "@web/core/dialog/dialog"; +import { Input } from "@point_of_sale/app/generic_components/inputs/input/input"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { _t } from "@web/core/l10n/translation"; +import { unaccent } from "@web/core/utils/strings"; +import { fuzzyLookup } from "@web/core/utils/search"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; +import { makeActionAwaitable } from "@point_of_sale/app/store/make_awaitable_dialog"; + + +export class SalespersonList extends Component { + static template = "salesperson_button_in_pos.SalespersonList"; + static components = {Dialog, Input }; + static props = { + getPayload: { type: Function }, + close: { type: Function }, + currentSelectedSalesperson: { type: Object }, + }; + + setup(){ + this.pos = usePos(); + this.dialog = useService("dialog"); + this.action=useService("action"); + this.ui = useState(useService("ui")); + this.notification = useService("notification"); + this.allSalesperson = this.pos.models["hr.employee"]?.getAll(); + this.state = useState({ + query: "", + selectedSalesPerson: null + }); + useHotkey("enter", () => this.onEnter()); + this.loadSalespeople(); + } + + async loadSalespeople() { + this.state.salespeople = await this.getSalespersons(); + } + + async editSalesperson(salesperson = false) { + try { + const actionProps = salesperson && salesperson.id ? { resId: salesperson.id } : {}; + + await this.env.services.action.doAction("salesperson_button_in_pos.action_salesperson_create_form_view", { + props: actionProps, + onClose: async () => { + await this.loadSalespeople(); + this.props.close(); + }, + }); + } catch (error) { + console.error("Error opening salesperson form:", error); + } + } + + get filteredSalespersons() { + if (!this.state.query) { + return this.allSalesperson; + } + return fuzzyLookup( + this.state.query, + this.allSalesperson, + (salesperson) => unaccent(salesperson.name) + ); + } + + getSalespersons() { + const users = this.pos.models["res.users"].getAll(); + const query = this.state.query?.toLowerCase() || ""; + return users.filter((u) => u.name?.toLowerCase().includes(query)); + } + + selectSalesperson(salesperson) { + const currentOrder = this.pos.get_order(); + if (!currentOrder) return; + + if (this.props.currentSelectedSalesperson?.id === salesperson.id) { + this.props.getPayload(null); + } else { + this.props.getPayload(salesperson); + } + this.props.close(); + } + + onEnter() { + this.notification.add(_t('No more customer found for "%s".', this.state.query),300); + } + + onSearchInput(event) { + this.state.query = event?.target?.value; + } +} diff --git a/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.xml b/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.xml new file mode 100644 index 00000000000..52087164926 --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/salesperson_list/salesperson_list.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameContact
+ +
+
+ + + + + +
No Salesperson Found
+
+ +
+ +
+ +
+
+
+
+
diff --git a/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.js b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.js new file mode 100644 index 00000000000..deec8369bc4 --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.js @@ -0,0 +1,9 @@ +/** @odoo-module **/ + +import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; +import { SelectSalespersonButton } from "./select_salesperson_button/select_salesperson_button"; +import { patch } from "@web/core/utils/patch"; + +patch(ControlButtons, { + components: { ...ControlButtons.components, SelectSalespersonButton, } +}); diff --git a/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.xml b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.xml new file mode 100644 index 00000000000..56d1d560f4e --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/control_button.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.js b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.js new file mode 100644 index 00000000000..ae15d5718ec --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useState } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { useService } from "@web/core/utils/hooks"; +// import { _t } from "@web/core/l10n/translation"; +import { SalespersonList } from "../../../../salesperson_list/salesperson_list"; + +export class SelectSalespersonButton extends Component { + static template = "salesperson_button_in_pos.SelectSalespersonButton"; + static props = []; + + setup() { + this.pos = usePos(); + this.dialog = useService("dialog"); + this.state = useState({ selectedSalesPerson: null }); + } + + async selectSalesperson() { + const currentOrder = this.pos.get_order(); + if (!currentOrder) return; + + const allSalesperson = this.pos.models["hr.employee"]?.getAll(); + + if (!allSalesperson || allSalesperson.length === 0) { + alert("No Salesperson Available"); + return; + } + + this.dialog.add(SalespersonList, { + getPayload: (selectedSalesperson) => { + if ( + selectedSalesperson === null || + this.state.selectedSalesPerson?.id === selectedSalesperson?.id + ) { + this.state.selectedSalesPerson = null; + currentOrder.salesperson_id = false; + } else { + this.state.selectedSalesPerson = selectedSalesperson; + currentOrder.salesperson_id = selectedSalesperson; + } + }, + currentSelectedSalesperson: this.state.selectedSalesPerson || {}, + }); + } +} diff --git a/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.xml b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.xml new file mode 100644 index 00000000000..e5b37ab85b9 --- /dev/null +++ b/salesperson_button_in_pos/static/src/app/screens/product_screen/control_buttons/select_salesperson_button/select_salesperson_button.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/salesperson_button_in_pos/views/employee_form_view.xml b/salesperson_button_in_pos/views/employee_form_view.xml new file mode 100644 index 00000000000..61f3f3c662a --- /dev/null +++ b/salesperson_button_in_pos/views/employee_form_view.xml @@ -0,0 +1,45 @@ + + + + hr.employee.form + hr.employee + +
+ +
+
+
+

+ +

+

+ +

+
+
+
+ + + + + + + + + + +
+
+
+
+ + + Edit Salesperon + hr.employee + form + + new + +
diff --git a/salesperson_button_in_pos/views/pos_order_view.xml b/salesperson_button_in_pos/views/pos_order_view.xml new file mode 100644 index 00000000000..37f720f868b --- /dev/null +++ b/salesperson_button_in_pos/views/pos_order_view.xml @@ -0,0 +1,24 @@ + + + + pos.order.form + pos.order + + + + + + + + + + pos.order.list + pos.order + + + + + + + +