diff --git a/pos_quotation_order/README.rst b/pos_quotation_order/README.rst new file mode 100644 index 000000000..aee196dbd --- /dev/null +++ b/pos_quotation_order/README.rst @@ -0,0 +1,44 @@ +======================== +Pos Quotation Orders V10 +======================== + +This module allows to create and process quotation orders from POS screen. + +Installation +============ + +Just select it from available modules to install it, there is no need to extra installations. + +Configuration +============= + +Nothing to configure. + +Features +======== + +* 'Create Quotation' button in POS. +* Create pos quotations. +* 'Quotation List' button in POS. +* Quotation list window. +* Manage quotations from POS. +* Quotation reference in order receipt and order. + +Usage +===== + +* We cannot create quotation order if there is no order lines added. +* Otherwise it will raise an alert popup with invalid order line. +* On confirming Quotation creation with invalid order date, display an error message. +* On confirming Order Creation, create quotation with order lines and it shows popup with quotation reference. +* You can view all draft quotations on 'Quotation List' button click. +* There is a confirm option for each quotations. +* On confirming quotation, order lines and customer assigned with quotation details. + + +Credits +======= + +Developer: Aswani pc @ cybrosys + + diff --git a/pos_quotation_order/__init__.py b/pos_quotation_order/__init__.py new file mode 100644 index 000000000..ab8ae5888 --- /dev/null +++ b/pos_quotation_order/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2009-TODAY Cybrosys Technologies(). +# Author: Aswani PC() +# you can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# It is forbidden to publish, distribute, sublicense, or sell copies +# of the Software or modified copies of the Software. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +from . import models diff --git a/pos_quotation_order/__manifest__.py b/pos_quotation_order/__manifest__.py new file mode 100644 index 000000000..be70cf097 --- /dev/null +++ b/pos_quotation_order/__manifest__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2009-TODAY Cybrosys Technologies(). +# Author: Aswani PC() +# you can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# It is forbidden to publish, distribute, sublicense, or sell copies +# of the Software or modified copies of the Software. +# +# 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 LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. +# If not, see . +# +############################################################################## +{ + 'name': "Pos Quotation Orders", + 'version': '10.0.1.0.0', + 'summary': """Create & Process Quotation from POS""", + 'description': """This module allows to create and process quotation orders from POS.""", + 'author': "Cybrosys Techno Solutions", + 'company': "Cybrosys Techno Solutions", + 'website': "http://www.cybrosys.com", + 'category': 'Point of Sale', + 'depends': ['base', 'point_of_sale'], + 'data': [ + 'security/ir.model.access.csv', + 'views/quotation_templates.xml', + 'views/pos_quotation.xml', + ], + 'qweb': ['static/src/xml/pos_quotation.xml'], + 'images': ['static/description/banner.jpg'], + 'license': 'AGPL-3', + 'installable': True, + 'auto_install': False, +} diff --git a/pos_quotation_order/models/__init__.py b/pos_quotation_order/models/__init__.py new file mode 100644 index 000000000..1bdc29d8b --- /dev/null +++ b/pos_quotation_order/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import pos_quotation + diff --git a/pos_quotation_order/models/pos_quotation.py b/pos_quotation_order/models/pos_quotation.py new file mode 100644 index 000000000..e3b3c605b --- /dev/null +++ b/pos_quotation_order/models/pos_quotation.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +import logging +import psycopg2 +from functools import partial +from odoo import models, fields, api, tools, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + quot_ref = fields.Many2one('pos.quotation', string='Quotation Ref') + + @api.model + def create_from_ui(self, orders): + # Keep only new orders + submitted_references = [o['data']['name'] for o in orders] + pos_order = self.search([('pos_reference', 'in', submitted_references)]) + existing_orders = pos_order.read(['pos_reference']) + existing_references = set([o['pos_reference'] for o in existing_orders]) + orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] + order_ids = [] + quot_ids = [] + + for tmp_order in orders_to_save: + to_invoice = tmp_order['to_invoice'] + order = tmp_order['data'] + if to_invoice: + self._match_payment_to_invoice(order) + pos_order = self._process_order(order) + if pos_order.quot_ref: + pos_order.quot_ref.write({'state': 'confirmed'}) + quot_ids.append(pos_order.quot_ref.id) + order_ids.append(pos_order.id) + + try: + pos_order.action_pos_order_paid() + except psycopg2.OperationalError: + # do not hide transactional errors, the order(s) won't be saved! + raise + except Exception as e: + _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) + + if to_invoice: + pos_order.action_pos_order_invoice() + pos_order.invoice_id.sudo().action_invoice_open() + pos_order.account_move = pos_order.invoice_id.move_id + return order_ids, quot_ids + + @api.model + def _order_fields(self, ui_order): + process_line = partial(self.env['pos.order.line']._order_line_fields) + quot_id = False + if 'quotation_ref' in ui_order: + if ui_order['quotation_ref']: + quot_id = ui_order['quotation_ref']['id'] + return { + 'name': ui_order['name'], + 'quot_ref': quot_id, + 'user_id': ui_order['user_id'] or False, + 'session_id': ui_order['pos_session_id'], + 'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False, + 'pos_reference': ui_order['name'], + 'partner_id': ui_order['partner_id'] or False, + 'date_order': ui_order['creation_date'], + 'fiscal_position_id': ui_order['fiscal_position_id'] + } + + +class PosQuotation(models.Model): + _name = 'pos.quotation' + + @api.model + def _amount_line_tax(self, line, fiscal_position_id): + taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id) + if fiscal_position_id: + taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id) + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = taxes.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, + partner=line.order_id.partner_id or False)['taxes'] + return sum(tax.get('amount', 0.0) for tax in taxes) + + @api.model + def _order_fields(self, ui_order): + process_line = partial(self.env['pos.quotation.line']._order_line_fields) + return { + 'lines': [process_line(l) for l in ui_order['lines']] if ui_order['lines'] else False, + 'partner_id': ui_order['partner_id'] or False, + 'date_order': ui_order['date_order'], + 'note': ui_order['note'] or '', + } + + def _default_session(self): + return self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1) + + def _default_pricelist(self): + return self._default_session().config_id.pricelist_id + + name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, + default=lambda self: self.env.user.company_id) + date_quotation = fields.Datetime(string='Quotation Date', readonly=True, index=True, default=fields.Datetime.now) + date_order = fields.Date(string='Order Date', readonly=True, index=True) + amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0) + amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) + lines = fields.One2many('pos.quotation.line', 'order_id', string='Order Lines', copy=True) + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', default=_default_pricelist) + partner_id = fields.Many2one('res.partner', string='Customer', change_default=True, index=True) + state = fields.Selection([('draft', 'New'), ('confirmed', 'Confirmed')], 'Status', readonly=True, copy=False, default='draft') + note = fields.Text(string='Internal Notes') + fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position') + + @api.depends('lines.price_subtotal_incl', 'lines.discount') + def _compute_amount_all(self): + for order in self: + order.amount_tax = 0.0 + currency = order.pricelist_id.currency_id + order.amount_tax = currency.round( + sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines)) + amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines)) + order.amount_total = order.amount_tax + amount_untaxed + + @api.model + def create_from_ui(self, orders): + order_id = self.create(self._order_fields(orders)) + order = {'id': order_id.id, + 'name': order_id.name} + return order + + @api.model + def create(self, vals): + if vals.get('name', '/') == '/': + vals['name'] = self.env['ir.sequence'].next_by_code('pos.quotation') or '/' + return super(PosQuotation, self).create(vals) + + +class PosQuotationLine(models.Model): + _name = "pos.quotation.line" + _description = "Lines of Point of Sale" + _rec_name = "product_id" + + def _order_line_fields(self, line): + if line and 'tax_ids' not in line[2]: + product = self.env['product.product'].browse(line[2]['product_id']) + line[2]['tax_ids'] = [(6, 0, [x.id for x in product.taxes_id])] + return line + + company_id = fields.Many2one('res.company', string='Company', required=True, + default=lambda self: self.env.user.company_id) + name = fields.Char(string='Line No') + notice = fields.Char(string='Discount Notice') + product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], + required=True, change_default=True) + price_unit = fields.Float(string='Unit Price', digits=0) + qty = fields.Float('Quantity', default=1) + price_subtotal = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal w/o Tax') + price_subtotal_incl = fields.Float(compute='_compute_amount_line_all', digits=0, string='Subtotal') + discount = fields.Float(string='Discount (%)', digits=0, default=0.0) + order_id = fields.Many2one('pos.quotation', string='Order Ref', ondelete='cascade') + create_date = fields.Datetime(string='Creation Date', readonly=True) + tax_ids = fields.Many2many('account.tax', string='Taxes', readonly=True) + tax_ids_after_fiscal_position = fields.Many2many('account.tax',string='Taxes') + pack_lot_ids = fields.One2many('pos.pack.operation.lot', 'pos_order_line_id', string='Lot/serial Number') + + @api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id') + def _compute_amount_line_all(self): + for line in self: + currency = line.order_id.pricelist_id.currency_id + taxes = line.tax_ids.filtered(lambda tax: tax.company_id.id == line.order_id.company_id.id) + fiscal_position_id = line.order_id.fiscal_position_id + if fiscal_position_id: + taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id) + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + line.price_subtotal = line.price_subtotal_incl = price * line.qty + if taxes: + taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, + partner=line.order_id.partner_id or False) + line.price_subtotal = taxes['total_excluded'] + line.price_subtotal_incl = taxes['total_included'] + + line.price_subtotal = currency.round(line.price_subtotal) + line.price_subtotal_incl = currency.round(line.price_subtotal_incl) + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + if not self.order_id.pricelist_id: + raise UserError( + _('You have to select a pricelist in the sale form !\n' + 'Please set one before choosing a product.')) + price = self.order_id.pricelist_id.get_product_price( + self.product_id, self.qty or 1.0, self.order_id.partner_id) + self._onchange_qty() + self.price_unit = price + self.tax_ids = self.product_id.taxes_id + + @api.onchange('qty', 'discount', 'price_unit', 'tax_ids') + def _onchange_qty(self): + if self.product_id: + if not self.order_id.pricelist_id: + raise UserError(_('You have to select a pricelist in the sale form !')) + price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) + self.price_subtotal = self.price_subtotal_incl = price * self.qty + if self.product_id.taxes_id: + taxes = self.product_id.taxes_id.compute_all(price, self.order_id.pricelist_id.currency_id, self.qty, + product=self.product_id, partner=False) + self.price_subtotal = taxes['total_excluded'] + self.price_subtotal_incl = taxes['total_included'] diff --git a/pos_quotation_order/security/ir.model.access.csv b/pos_quotation_order/security/ir.model.access.csv new file mode 100644 index 000000000..52b474786 --- /dev/null +++ b/pos_quotation_order/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_pos_quotation,pos.quotation,model_pos_quotation,point_of_sale.group_pos_user,1,1,1,1 +access_pos_quotation_line,pos.quotation.line,model_pos_quotation_line,point_of_sale.group_pos_user,1,1,1,1 diff --git a/pos_quotation_order/static/description/banner.jpg b/pos_quotation_order/static/description/banner.jpg new file mode 100644 index 000000000..e23a45666 Binary files /dev/null and b/pos_quotation_order/static/description/banner.jpg differ diff --git a/pos_quotation_order/static/description/cybro_logo.png b/pos_quotation_order/static/description/cybro_logo.png new file mode 100644 index 000000000..bb309114c Binary files /dev/null and b/pos_quotation_order/static/description/cybro_logo.png differ diff --git a/pos_quotation_order/static/description/icon.png b/pos_quotation_order/static/description/icon.png new file mode 100644 index 000000000..b3033ac1c Binary files /dev/null and b/pos_quotation_order/static/description/icon.png differ diff --git a/pos_quotation_order/static/description/index.html b/pos_quotation_order/static/description/index.html new file mode 100644 index 000000000..28c7ad2f3 --- /dev/null +++ b/pos_quotation_order/static/description/index.html @@ -0,0 +1,201 @@ +
+
+

Pos Quotation Orders

+

Create And Process Quotation from POS

+

Cybrosys Technologies

+
+
+

Features:

+
+ 'Create Quotation' button in POS.
+ Create pos quotations.
+ 'Quotation List' button in POS.
+ Quotation list window.
+ Manage quotations from POS.
+ Quotation reference in order receipt and order.
+
+
+
+ +
+
+
+

Overview

+

+ POS Quotation Orders is a plugin that facilitates the creation and management of Quotation orders from POS window. By default, you can’t create a quotation order from POS window in Odoo. But in some cases, such a simple option can make considerable changes in your sales. This plugin will add a ‘Create Quotation’ in POS window and will facilitate the Quotation creation process without affecting the normal workflow of POS. +

+
+
+
+ +
+
+
+

After installation, open POS window and start and new Session.

+
+
+ +
+
+
+
+
+ +
+
+
+

In order to create a quotation you have to add products in Order line. Otherwise it will raise an alert popup with invalid order line.

+
+
+ +
+
+
+
+
+ +
+
+

Quotation Popup

+
+
+

Add the products in order line and click ‘Create Quotation’ it will give you a popup to add order date and a note for quotation.

+
+ +
+
+
+
+
+ +
+
+

Quotation Reference

+
+
+

Once you have successfully created a quotation, you will get a Quotation reference number as in the figure.

+
+ +
+
+
+
+
+ +
+
+

Created POS Quotation View

+
+
+

You can view and manage the created PoS quotations from

+
+

Point of Sale -> Orders -> Quotation

+
+ +
+

Click on any of the quotation to view the details of the particular quotation.

+
+ +
+
+
+
+
+ +
+
+
+
+

Manage Quotation

+

You can view all ‘Draft’ quotation by clicking ‘Quotation List' button in POS. Click the corresponding ‘Confirm’ button.

+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+





+

On confirming quotation, you can view the order lines and customer assigned with quotation details.

+
+
+
+ +
+
+
+
+

POS Receipt with Quotation Reference

+
+
+ +
+
+
+
+
+
+ +
+
+
+
+

Quotation status

+

Now, if you go to the quotation details you can see that the Quotation status is changed to ‘confirmed’.

+
+ +
+
+
+
+
+ +
+
+
+
+

POS Order with Quotation Reference

+
+
+ +
+
+
+
+
+
+ +
+

Need Any Help?

+ +
diff --git a/pos_quotation_order/static/description/quot_01.png b/pos_quotation_order/static/description/quot_01.png new file mode 100644 index 000000000..3c2286687 Binary files /dev/null and b/pos_quotation_order/static/description/quot_01.png differ diff --git a/pos_quotation_order/static/description/quot_02.png b/pos_quotation_order/static/description/quot_02.png new file mode 100644 index 000000000..6abf60f16 Binary files /dev/null and b/pos_quotation_order/static/description/quot_02.png differ diff --git a/pos_quotation_order/static/description/quot_03.png b/pos_quotation_order/static/description/quot_03.png new file mode 100644 index 000000000..4a37a1c6d Binary files /dev/null and b/pos_quotation_order/static/description/quot_03.png differ diff --git a/pos_quotation_order/static/description/quot_04.png b/pos_quotation_order/static/description/quot_04.png new file mode 100644 index 000000000..d76159d21 Binary files /dev/null and b/pos_quotation_order/static/description/quot_04.png differ diff --git a/pos_quotation_order/static/description/quot_05.png b/pos_quotation_order/static/description/quot_05.png new file mode 100644 index 000000000..19886c970 Binary files /dev/null and b/pos_quotation_order/static/description/quot_05.png differ diff --git a/pos_quotation_order/static/description/quot_06.png b/pos_quotation_order/static/description/quot_06.png new file mode 100644 index 000000000..ec724764c Binary files /dev/null and b/pos_quotation_order/static/description/quot_06.png differ diff --git a/pos_quotation_order/static/description/quot_07.png b/pos_quotation_order/static/description/quot_07.png new file mode 100644 index 000000000..8e612f6f3 Binary files /dev/null and b/pos_quotation_order/static/description/quot_07.png differ diff --git a/pos_quotation_order/static/description/quot_08.png b/pos_quotation_order/static/description/quot_08.png new file mode 100644 index 000000000..8d30f1682 Binary files /dev/null and b/pos_quotation_order/static/description/quot_08.png differ diff --git a/pos_quotation_order/static/description/quot_09.png b/pos_quotation_order/static/description/quot_09.png new file mode 100644 index 000000000..aa430cc19 Binary files /dev/null and b/pos_quotation_order/static/description/quot_09.png differ diff --git a/pos_quotation_order/static/description/quot_10.png b/pos_quotation_order/static/description/quot_10.png new file mode 100644 index 000000000..3a6e82e7d Binary files /dev/null and b/pos_quotation_order/static/description/quot_10.png differ diff --git a/pos_quotation_order/static/description/quot_11.png b/pos_quotation_order/static/description/quot_11.png new file mode 100644 index 000000000..a4fa2a197 Binary files /dev/null and b/pos_quotation_order/static/description/quot_11.png differ diff --git a/pos_quotation_order/static/src/css/pos_quotation.css b/pos_quotation_order/static/src/css/pos_quotation.css new file mode 100644 index 000000000..db7ce1887 --- /dev/null +++ b/pos_quotation_order/static/src/css/pos_quotation.css @@ -0,0 +1,51 @@ +.alert_msg{ + color:red; +} + +.popup_quot_ref{ + height: 150px !important; +} + + +/* The Quotation List Screen */ + +.quotation_list-screen .quotation-list{ + font-size: 16px; + width: 100%; + line-height: 40px; +} +.quotation_list-screen .quotation-list th, +.quotation_list-screen .quotation-list td { + padding: 0px 8px; +} +.quotation_list-screen .quotation-list tr{ + transition: all 150ms linear; + background: rgb(230,230,230); +} +.quotation_list-screen .quotation-list thead > tr, +.quotation_list-screen .quotation-list tr:nth-child(even) { + background: rgb(247,247,247); +} +.quotation_list-screen .quotation-list tr.highlight{ + transition: all 150ms linear; + background: rgb(110,200,155) !important; + color: white; +} +.quotation_list-screen .quotation-list td tr.lowlight{ + transition: all 150ms linear; + background: rgb(216, 238, 227); +} +.quotation_list-screen .quotation-list tr.lowlight:nth-child(even){ + transition: all 150ms linear; + background: rgb(227, 246, 237); +} + +.quotation_list-screen .searchbox{ + right: auto; + margin-left: -90px; + margin-top:8px; + left: 50%; +} +.quotation_list-screen .searchbox input{ + width: 120px; +} diff --git a/pos_quotation_order/static/src/js/models.js b/pos_quotation_order/static/src/js/models.js new file mode 100644 index 000000000..d6953f7f8 --- /dev/null +++ b/pos_quotation_order/static/src/js/models.js @@ -0,0 +1,102 @@ +odoo.define('pos_quotation_order.models', function (require) { +"use strict"; + +var screens = require('point_of_sale.screens'); +var gui = require('point_of_sale.gui'); +var core = require('web.core'); +var models = require('point_of_sale.models'); +var Model = require('web.DataModel'); +var QWeb = core.qweb; +var _t = core._t; + +models.load_models({ + model: 'pos.quotation', + fields: ['name', 'partner_id','date_order','amount_total','lines','state'], + domain: [['state','=','draft']], + loaded: function(self, quotations){ + self.quotations = quotations; + } + }); + + models.load_models({ + model: 'pos.quotation.line', + fields: ['product_id', 'qty'], + loaded: function(self, quotation_lines){ + self.quotation_lines = quotation_lines; + } + }); + + var _super_order = models.Order.prototype; + models.Order = models.Order.extend({ + export_as_JSON: function() { + var data = _super_order.export_as_JSON.apply(this, arguments); + data.quotation_ref = this.quotation_ref; + return data; + }, + init_from_JSON: function(json) { + this.quotation_ref = json.quotation_ref; + _super_order.init_from_JSON.call(this, json); + }, + }); + + var posmodel_super = models.PosModel.prototype; + models.PosModel = models.PosModel.extend({ + _save_to_server: function (orders, options) { + if (!orders || !orders.length) { + var result = $.Deferred(); + result.resolve([]); + return result; + } + + options = options || {}; + + var self = this; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; + var order_ids_to_sync = _.pluck(orders, 'id'); + var quot_model = new Model('pos.quotation'); + var fields = _.find(this.models,function(model){ return model.model === 'pos.quotation'; }).fields; + var posOrderModel = new Model('pos.order'); + return posOrderModel.call('create_from_ui', + [_.map(orders, function (order) { + order.to_invoice = options.to_invoice || false; + return order; + })], + undefined, + { + shadow: !options.to_invoice, + timeout: timeout + } + ).then(function (server_ids) { + if (server_ids[1].length != 0){ + for (var item in server_ids[1]){ + quot_model.query(fields).filter([['id','=',server_ids[1][item]]]).first().then(function(quotation){ + var index = self.quotations.indexOf(quotation); + self.quotations.splice(index, 1); + }); + } + } + _.each(order_ids_to_sync, function (order_id) { + self.db.remove_order(order_id); + }); + self.set('failed',false); + return server_ids[0]; + }).fail(function (error, event){ + if(error.code === 200 ){ + if (error.data.exception_type == 'warning') { + delete error.data.debug; + } + if ((!self.get('failed') || options.show_error) && !options.to_invoice) { + self.gui.show_popup('error-traceback',{ + 'title': error.data.message, + 'body': error.data.debug + }); + } + self.set('failed',error) + } + event.preventDefault(); + console.error('Failed to send orders:', orders); + }); + }, + }); + +}); \ No newline at end of file diff --git a/pos_quotation_order/static/src/js/pos_quotation.js b/pos_quotation_order/static/src/js/pos_quotation.js new file mode 100644 index 000000000..c2ec9245a --- /dev/null +++ b/pos_quotation_order/static/src/js/pos_quotation.js @@ -0,0 +1,243 @@ +odoo.define('point_of_sale.pos_quotation_order', function (require) { +"use strict"; + +var screens = require('point_of_sale.screens'); +var gui = require('point_of_sale.gui'); +var Model = require('web.DataModel'); +var core = require('web.core'); +var PopupWidget = require('point_of_sale.popups'); +var ProductListWidget = screens.ProductListWidget; +var ScreenWidget = screens.ScreenWidget; +var QWeb = core.qweb; +var _t = core._t; + +var QuotationPopupWidget = PopupWidget.extend({ + template: 'QuotationPopupWidget', + events: _.extend({}, PopupWidget.prototype.events,{ + "keyup .order_date" : "date_validate", + }), + show: function(options){ + options = options || {}; + var self = this; + this._super(options); + this.renderElement(); + }, + date_validate: function(){ + var v = $(".order_date").val(); + if (v.match(/^\d{4}$/) !== null) { + $(".order_date").val(v + '/'); + } + else if (v.match(/^\d{4}\/\d{2}$/) !== null) { + $(".order_date").val(v + '/'); + } + }, + click_confirm: function(){ + var self = this; + var new_quotation = []; + var model = new Model('pos.quotation'); + var line_model = new Model('pos.quotation.line'); + var fields = _.find(this.pos.models,function(model){ return model.model === 'pos.quotation'; }).fields; + var line_fields = _.find(this.pos.models,function(model){ return model.model === 'pos.quotation.line'; }).fields; + var today = new Date().toJSON().slice(0,10); + var order = this.pos.get_order(); + var order_to_save = order.export_as_JSON(); + var order_lines = this.pos.get_order().get_orderlines(); + var order_date = this.$('.order_date').val(); + var order_note = this.$('.order_note').val(); + var valid_date = true; + var validatePattern = /^(\d{4})([/|-])(\d{1,2})([/|-])(\d{1,2})$/; + if (order_date){ + var dateValues = order_date.match(validatePattern); + if (dateValues == null){ + valid_date = false; + } + else{ + var orderYear = dateValues[1]; + var orderMonth = dateValues[3]; + var orderDate = dateValues[5]; + if ((orderMonth < 1) || (orderMonth > 12)) { + valid_date = false; + } + else if ((orderDate < 1) || (orderDate> 31)) { + valid_date = false; + } + else if ((orderMonth==4 || orderMonth==6 || orderMonth==9 || orderMonth==11) && orderDate ==31) { + valid_date = false; + } + else if (orderMonth == 2){ + var isleap = (orderYear % 4 == 0 && (orderYear % 100 != 0 || orderYear % 400 == 0)); + if (orderDate> 29 || (orderDate ==29 && !isleap)){ + valid_date = false; + } + } + var dates = [orderYear,orderMonth,orderDate]; + order_date = dates.join('-'); + } + } + $('.alert_msg').text(""); + if (order_date && order_date < today || valid_date==false || !order_date){ + $('.alert_msg').text("Please Select Valid Order Date!"); + } + else{ + $('.alert_msg').text(""); + if (order_date){ + order_to_save.date_order = order_date; + } + order_to_save.note = order_note; + model.call('create_from_ui',[order_to_save]).then(function(order){ + model.query(fields).filter([['id','=',order['id']]]).first().then(function(quotation){ + self.pos.quotations.push(quotation); + for (var line in quotation['lines']){ + line_model.query(line_fields).filter([['id','=',quotation['lines'][line]]]).first().then(function(quotation_line){ + self.pos.quotation_lines.push(quotation_line); + }); + } + }); + self.gui.close_popup(); + self.pos.delete_current_order(); + self.gui.show_popup('pos_quot_result',{ + 'body': _t('Quotation Ref : ')+ order['name'] , + }); + }); + } + }, + +}); + +var QuotationListScreenWidget = ScreenWidget.extend({ + template: 'QuotationListScreenWidget', + back_screen: 'product', + init: function(parent, options){ + var self = this; + this._super(parent, options); + }, + + show: function(){ + var self = this; + this._super(); + this.renderElement(); + this.$('.back').click(function(){ + self.gui.back(); + }); + + var quotations = this.pos.quotations; + this.render_list(quotations); + + this.$('.quotation-list-contents').delegate('.quotation-line .confirm_quotation','click',function(event){ + self.line_select(event,$(this.parentElement.parentElement),parseInt($(this.parentElement.parentElement).data('id'))); + }); + + var search_timeout = null; + + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.connect(this.$('.searchbox input')); + } + + this.$('.searchbox input').on('keypress',function(event){ + clearTimeout(search_timeout); + var query = this.value; + search_timeout = setTimeout(function(){ + self.perform_search(query,event.which === 13); + },70); + }); + + this.$('.searchbox .search-clear').click(function(){ + self.clear_search(); + }); + }, + + render_list: function(quotations){ + var contents = this.$el[0].querySelector('.quotation-list-contents'); + contents.innerHTML = ""; + for(var i = 0, len = Math.min(quotations.length,1000); i < len; i++){ + var quotation = quotations[i]; + var quotation_line_html = QWeb.render('QuotationLine',{widget: this, quotation:quotations[i]}); + var quotation_line = document.createElement('tbody'); + quotation_line.innerHTML = quotation_line_html; + quotation_line = quotation_line.childNodes[1]; + contents.appendChild(quotation_line); + } + }, + + line_select: function(event,$line,id){ + var self = this; + var order = this.pos.get_order(); + for (var quot_id in this.pos.quotations){ + if (this.pos.quotations[quot_id]['id'] == id){ + var selected_quotation = this.pos.quotations[quot_id] + } + } + if (selected_quotation){ + for (var line in this.pos.quotation_lines){ + if (selected_quotation['lines'].indexOf(this.pos.quotation_lines[line]['id']) > -1 ){ + var product_id = this.pos.db.get_product_by_id(this.pos.quotation_lines[line]['product_id'][0]); + this.pos.get_order().add_product(product_id,{ quantity: this.pos.quotation_lines[line]['qty']}); + } + } + order.quotation_ref = selected_quotation; + if (selected_quotation.partner_id){ + var partner = this.pos.db.get_partner_by_id(selected_quotation.partner_id[0]); + order.set_client(partner); + } + this.gui.show_screen('products'); + } + + }, +}); + + +gui.define_popup({name:'pos_quot', widget: QuotationPopupWidget}); + +var QuotationResultPopupWidget = PopupWidget.extend({ + template: 'QuotationResultPopupWidget', +}); + +gui.define_popup({name:'pos_quot_result', widget: QuotationResultPopupWidget}); +gui.define_screen({name:'quotation_list', widget: QuotationListScreenWidget}); + +var QuotationListButton = screens.ActionButtonWidget.extend({ + template: 'QuotationListButton', + button_click: function(){ + this.gui.show_screen('quotation_list'); + } +}); + +screens.define_action_button({ + 'name': 'pos_quotation_list', + 'widget': QuotationListButton, +}); + + +var QuotationButton = screens.ActionButtonWidget.extend({ + template: 'QuotationButton', + button_click: function(){ + var order_lines = this.pos.get_order().get_orderlines(); + var flag_negative = false; + for (var line in order_lines){ + if (order_lines[line].quantity < 0){ + flag_negative = true; + } + } + if(this.pos.get_order().get_orderlines().length > 0 && flag_negative == false && this.pos.get_order().get_total_with_tax()>0){ + this.gui.show_popup('pos_quot'); + } + else if(flag_negative == true){ + this.gui.show_popup('pos_quot_result',{ + 'body': _t('Invalid Order: Negative Quantity is Not Allowed'), + }); + } + else if(this.pos.get_order().get_orderlines().length == 0 || this.pos.get_order().get_total_with_tax() <=0){ + this.gui.show_popup('pos_quot_result',{ + 'body': _t('Invalid Order : Please Add Some Order Lines'), + }); + } + }, +}); + +screens.define_action_button({ + 'name': 'pos_quotation_order', + 'widget': QuotationButton, +}); + +}); + diff --git a/pos_quotation_order/static/src/xml/pos_quotation.xml b/pos_quotation_order/static/src/xml/pos_quotation.xml new file mode 100644 index 000000000..749f3f2b6 --- /dev/null +++ b/pos_quotation_order/static/src/xml/pos_quotation.xml @@ -0,0 +1,144 @@ + +