diff --git a/discounts_in_pos/README.rst b/discounts_in_pos/README.rst new file mode 100644 index 000000000..55f1014f6 --- /dev/null +++ b/discounts_in_pos/README.rst @@ -0,0 +1,21 @@ +================= +Pos Discounts v19 +================= + +This module adds a feature to provide several types of discounts in point of sale. + +Installation +============ + +Just select it from available modules to install it, there is no need to extra installations. + +Configuration +============= + +We need to specify a discount account when we create the point of sale. This account will be used to record the discount amount. + +Credits +======= +Developer: Linto CT @ cybrosys, linto@cybrosys.in + + diff --git a/discounts_in_pos/__init__.py b/discounts_in_pos/__init__.py new file mode 100644 index 000000000..fca0067a7 --- /dev/null +++ b/discounts_in_pos/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +import models + diff --git a/discounts_in_pos/__openerp__.py b/discounts_in_pos/__openerp__.py new file mode 100644 index 000000000..7950291c0 --- /dev/null +++ b/discounts_in_pos/__openerp__.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Cybrosys Technologies Pvt. Ltd. +# Copyright (C) 2017-TODAY Cybrosys Technologies(). +# Author: LINTO C T() +# 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': 'Point of Sale Discounts', + 'version': '9.0.1.0.0', + 'category': 'Point of Sale', + 'summary': 'Discounts in the Point of Sale(Fixed and Percentage) ', + 'author': 'Cybrosys Techno Solutions', + 'company': 'Cybrosys Techno Solutions', + 'images': ['static/description/banner.jpg'], + 'website': 'https://www.cybrosys.com', + 'depends': ['base', 'point_of_sale', 'account', 'report', 'account_accountant'], + 'data': [ + 'views/report_paperformat_new.xml', + 'views/templates.xml', + 'views/report_payment_new.xml', + 'views/pos_reports_new_invoice.xml', + 'views/report_saleslines_new.xml', + 'views/report_receipt_new.xml', + 'views/account_invoice_view_pos.xml', + 'views/pos_reports_account_invoice.xml', + 'views/pos_view.xml', + ], + 'qweb': [ + 'static/src/xml/discount.xml' + ], + 'license': 'AGPL-3', + 'installable': True, + 'auto_install': False, +} + diff --git a/discounts_in_pos/models/__init__.py b/discounts_in_pos/models/__init__.py new file mode 100644 index 000000000..d414d0347 --- /dev/null +++ b/discounts_in_pos/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +import pos_order_lines_new +import pos_payment_report_new +import pos_lines_new +import pos_invoice_new +import pos_receipt_new diff --git a/discounts_in_pos/models/p.py~ b/discounts_in_pos/models/p.py~ new file mode 100644 index 000000000..d59e50bf6 --- /dev/null +++ b/discounts_in_pos/models/p.py~ @@ -0,0 +1,49 @@ +import time +from openerp.osv import osv +from openerp.report import report_sxw + + +class pos_payment_report(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(pos_payment_report, self).__init__(cr, uid, name, context=context) + self.total = 0.0 + self.localcontext.update({ + 'time': time, + 'pos_payment': self._pos_payment, + 'pos_payment_total':self._pos_payment_total, + }) + + def _pos_payment(self, obj): + self.total = 0 + data={} + sql = """ select id from pos_order where id = %d"""%(obj.id) + self.cr.execute(sql) + if self.cr.fetchone(): + self.cr.execute ("select pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.price_unit, " \ + "(pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)) as total " \ + "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ + "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ + "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date and po.id=%d"%(obj.id)) + data=self.cr.dictfetchall() + else: + self.cr.execute ("select pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.price_unit, " \ + "(pol.price_unit * pol.qty * (1 - (pol.discount) / 100.0)) as total " \ + "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ + "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ + "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date") + data=self.cr.dictfetchall() + + for d in data: + self.total += d['price_unit'] * d['qty'] + return data + + def _pos_payment_total(self, o): + return self.total + + +class report_pos_payment(osv.AbstractModel): + _name = 'report.point_of_sale.report_payment' + _inherit = 'report.abstract_report' + _template = 'point_of_sale.report_payment' + _wrapped_report_class = pos_payment_report diff --git a/discounts_in_pos/models/pos_invoice_new.py b/discounts_in_pos/models/pos_invoice_new.py new file mode 100644 index 000000000..47945bc56 --- /dev/null +++ b/discounts_in_pos/models/pos_invoice_new.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- + +from openerp import SUPERUSER_ID +from openerp.osv import osv +from openerp.tools import float_is_zero + +from openerp.exceptions import UserError +from openerp import api, fields, models, _ +import openerp.addons.decimal_precision as dp + + +class PosInvoiceReportNew(osv.AbstractModel): + _name = 'report.discounts_in_pos.report_invoice' + + def render_html(self, cr, uid, ids, data=None, context=None): + report_obj = self.pool['report'] + posorder_obj = self.pool['pos.order'] + report = report_obj._get_report_from_name(cr, uid, 'discounts_in_pos.report_invoice') + selected_orders = posorder_obj.browse(cr, uid, ids, context=context) + ids_to_print = [] + invoiced_posorders_ids = [] + for order in selected_orders: + if order.invoice_id: + ids_to_print.append(order.invoice_id.id) + invoiced_posorders_ids.append(order.id) + + not_invoiced_orders_ids = list(set(ids) - set(invoiced_posorders_ids)) + if not_invoiced_orders_ids: + not_invoiced_posorders = posorder_obj.browse(cr, uid, not_invoiced_orders_ids, context=context) + not_invoiced_orders_names = list(map(lambda a: a.name, not_invoiced_posorders)) + raise UserError(_('No link to an invoice for %s.') % ', '.join(not_invoiced_orders_names)) + + docargs = { + 'docs': self.pool['account.invoice'].browse(cr, uid, ids_to_print, context=context) + } + + return report_obj.render(cr, SUPERUSER_ID, ids, 'discounts_in_pos.report_invoice', docargs, context=context) + + +class POSInvoiceNew(models.Model): + _inherit = 'account.invoice.line' + + discount_fixed = fields.Float(string='Discount.Fixed', digits=dp.get_precision('DiscountFixed'), + default=0.0) + + @api.one + @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', + 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id') + def _compute_price(self): + super(POSInvoiceNew, self)._compute_price() + + currency = self.invoice_id and self.invoice_id.currency_id or None + + price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) + + if self.discount != 0: + price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) + if self.discount_fixed != 0: + price = self.price_unit + taxes = False + if self.invoice_line_tax_ids: + taxes = self.invoice_line_tax_ids.compute_all(price, currency, self.quantity, product=self.product_id, + partner=self.invoice_id.partner_id) + + self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price + + if self.discount != 0: + self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price + + if self.discount_fixed != 0: + self.price_subtotal = price_subtotal_signed = taxes[ + 'total_excluded'] if taxes else self.quantity * price - self.discount_fixed + if self.invoice_id.currency_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id: + price_subtotal_signed = self.invoice_id.currency_id.compute(price_subtotal_signed, + self.invoice_id.company_id.currency_id) + sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1 + self.price_subtotal_signed = price_subtotal_signed * sign + + +class POSInvoiceTotalDisc(models.Model): + _inherit = 'account.invoice' + + discount_total = fields.Float(string='Total Discount', default=0.0) + discount_percent = fields.Float(string='Total Discount(%)', default=0.0) + + @api.one + @api.depends('invoice_line_ids.price_subtotal', 'tax_line_ids.amount', 'currency_id', 'company_id') + def _compute_amount(self): + super(POSInvoiceTotalDisc, self)._compute_amount() + self.amount_total = self.amount_untaxed + self.amount_tax + if self.discount_total > 0: + self.amount_total -= self.discount_total + if self.discount_percent > 0: + self.amount_total -= ((self.amount_untaxed + self.amount_tax) * self.discount_percent / 100) + + def _compute_residual(self): + super(POSInvoiceTotalDisc, self)._compute_residual() + residual = 0.0 + residual_company_signed = 0.0 + sign = self.type in ['in_refund', 'out_refund'] and -1 or 1 + for line in self.sudo().move_id.line_ids: + if line.account_id.internal_type in ('receivable', 'payable'): + residual_company_signed += line.amount_residual + if line.currency_id == self.currency_id: + residual += line.amount_residual_currency if line.currency_id else line.amount_residual + else: + from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date) + residual += from_currency.compute(line.amount_residual, self.currency_id) + + if self.discount_total > 0: + residual -= self.discount_total + if self.discount_percent > 0: + residual -= self.amount_untaxed * self.discount_percent / 100 + + self.residual_company_signed = abs(residual_company_signed) * sign + self.residual_signed = abs(residual) * sign + self.residual = abs(residual) + digits_rounding_precision = self.currency_id.rounding + if float_is_zero(self.residual, precision_rounding=digits_rounding_precision): + self.reconciled = True + else: + self.reconciled = False + + @api.multi + def action_move_create(self): + res = super(POSInvoiceTotalDisc, self).action_move_create() + order = self.env['pos.order'].search([('invoice_id', '=', self.id)]) + session = None + if order: + if order.discount_total > 0 or order.discount_percent > 0: + session = order.session_id + discount_ac = None + if session: + if session.config_id.discount_account: + discount_ac = session.config_id.discount_account + else: + raise UserError(_('Please set a discount account for this session')) + lines = self.env['account.move.line'].search([('move_id', '=', self.move_id.id), ('debit', '>', 0)], limit=1) + if order.discount_total > 0: + discount = order.discount_total + elif order.discount_percent > 0: + move_lines = self.env['account.move.line'].search([('move_id', '=', self.move_id.id), ('credit', '>', 0)]) + sum = 0 + for i in move_lines: + sum += i.credit + discount = sum - order.amount_total + lines.write({ + 'debit': lines.debit - discount + }) + temp2 = { + 'partner_id': self.partner_id.id, + 'name': "Discount", + 'credit': 0, + 'debit': discount, + 'account_id': discount_ac.id, + 'quantity': 1, + 'move_id': self.move_id.id, + } + self.env['account.move.line'].create(temp2) + return res + + +class PosConfigNew(models.Model): + _inherit = 'pos.config' + # discount account is used to enter the total discount values + discount_account = fields.Many2one('account.account', string="Discount Account", required=True) \ No newline at end of file diff --git a/discounts_in_pos/models/pos_lines_new.py b/discounts_in_pos/models/pos_lines_new.py new file mode 100644 index 000000000..4407125ac --- /dev/null +++ b/discounts_in_pos/models/pos_lines_new.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +import time +from openerp.osv import osv +from openerp.report import report_sxw + + +class pos_lines(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(pos_lines, self).__init__(cr, uid, name, context=context) + self.total = 0.0 + self.localcontext.update({ + 'time': time, + 'total_quantity': self.__total_quantity__, + 'taxes':self.__taxes__, + + }) + + def __total_quantity__(self, obj): + tot = 0 + for line in obj.lines: + tot += line.qty + self.total = tot + return self.total + + def __taxes__(self, obj): + self.cr.execute ( " Select acct.name from pos_order as po " \ + " LEFT JOIN pos_order_line as pol ON po.id = pol.order_id " \ + " LEFT JOIN product_product as pp ON pol.product_id = pp.id" + " LEFT JOIN product_taxes_rel as ptr ON pp.product_tmpl_id = ptr.prod_id " \ + " LEFT JOIN account_tax as acct ON acct.id = ptr.tax_id " \ + " WHERE pol.id = %s", (obj.id,)) + res=self.cr.fetchone()[0] + return res + + +class report_pos_lines(osv.AbstractModel): + _name = 'report.discounts_in_pos.report_saleslines' + _inherit = 'report.abstract_report' + _template = 'discounts_in_pos.report_saleslines' + _wrapped_report_class = pos_lines diff --git a/discounts_in_pos/models/pos_order_lines_new.py b/discounts_in_pos/models/pos_order_lines_new.py new file mode 100644 index 000000000..2acf9679d --- /dev/null +++ b/discounts_in_pos/models/pos_order_lines_new.py @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- + +import logging + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools.translate import _ +from openerp.exceptions import UserError +import openerp.addons.decimal_precision as dp + +from openerp import api, models, fields as Fields + +_logger = logging.getLogger(__name__) + + +class NewPosOrder(osv.osv): + _inherit = "pos.order" + + discount_total = Fields.Float(string='Total Discount(Fixed)', default=0.0) + discount_percent = Fields.Float(string='Total Discount(%)', default=0.0) + + _defaults = { + 'discount_total': lambda *a: 0.0, + 'discount_percent': lambda *a: 0.0, + } + + @api.onchange('discount_percent') + def change_discount_fixed(self): + if self.discount_percent: + self.discount_total = 0.0 + + @api.onchange('discount_total') + def change_discount_percent(self): + if self.discount_total: + self.discount_percent = 0.0 + + @api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount', 'lines.discount_fixed','discount_total','discount_percent') + def _compute_amount_all(self): + super(NewPosOrder, self)._compute_amount_all() + + for order in self: + + currency = order.pricelist_id.currency_id + amount_untaxed = currency.round(sum(line.price_subtotal for line in order.lines)) + + order.amount_total = order.amount_tax + amount_untaxed + if order.discount_total > 0: + order.amount_total -= order.discount_total + if order.discount_percent > 0: + order.amount_total -= ((order.amount_tax + amount_untaxed) * order.discount_percent / 100) + + def _order_fields(self, cr, uid, ui_order, context=None): + new_discount = super(NewPosOrder, self)._order_fields(cr, uid, ui_order) + new_discount['discount_total'] = ui_order['discount_total'] + new_discount['discount_percent'] = ui_order['discount_percent'] + return new_discount + + def action_invoice(self, cr, uid, ids, context=None): + + inv_ref = self.pool.get('account.invoice') + inv_line_ref = self.pool.get('account.invoice.line') + product_obj = self.pool.get('product.product') + inv_ids = [] + + for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context): + # Force company for all SUPERUSER_ID action + company_id = order.company_id.id + local_context = dict(context or {}, force_company=company_id, company_id=company_id) + if order.invoice_id: + inv_ids.append(order.invoice_id.id) + continue + + if not order.partner_id: + raise UserError(_('Please provide a partner for the sale.')) + + acc = order.partner_id.property_account_receivable_id.id + +# =============adding 'discount_total' to invoice================================================================ + inv = { + 'name': order.name, + 'origin': order.name, + 'account_id': acc, + 'journal_id': order.sale_journal.id or None, + 'type': 'out_invoice', + 'reference': order.name, + 'partner_id': order.partner_id.id, + 'comment': order.note or '', + 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency + 'company_id': company_id, + 'user_id': uid, + 'discount_total': order.discount_total, + 'discount_percent': order.discount_percent, + + } + invoice = inv_ref.new(cr, uid, inv) + invoice._onchange_partner_id() + invoice.fiscal_position_id = order.fiscal_position_id + + inv = invoice._convert_to_write(invoice._cache) + if not inv.get('account_id', None): + inv['account_id'] = acc + inv_id = inv_ref.create(cr, SUPERUSER_ID, inv, context=local_context) + + self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=local_context) + inv_ids.append(inv_id) + for line in order.lines: + inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=local_context)[0][1] + +# ===============adding 'discount fixed' to invoice lines========================================== + inv_line = { + 'invoice_id': inv_id, + 'product_id': line.product_id.id, + 'quantity': line.qty, + 'account_analytic_id': self._prepare_analytic_account(cr, uid, line, context=local_context), + 'name': inv_name, + 'discount_fixed': line.discount_fixed, + } + + #Oldlin trick + invoice_line = inv_line_ref.new(cr, SUPERUSER_ID, inv_line, context=local_context) + invoice_line._onchange_product_id() + invoice_line.invoice_line_tax_ids = [tax.id for tax in invoice_line.invoice_line_tax_ids if tax.company_id.id == company_id] + fiscal_position_id = line.order_id.fiscal_position_id + if fiscal_position_id: + invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids) + invoice_line.invoice_line_tax_ids = [tax.id for tax in invoice_line.invoice_line_tax_ids] + # We convert a new id object back to a dictionary to write to bridge between old and new api + inv_line = invoice_line._convert_to_write(invoice_line._cache) + inv_line.update(price_unit=line.price_unit, discount=line.discount, discount_fixed=line.discount_fixed) + inv_line_ref.create(cr, SUPERUSER_ID, inv_line, context=local_context) + inv_ref.compute_taxes(cr, SUPERUSER_ID, [inv_id], context=local_context) + self.signal_workflow(cr, uid, [order.id], 'invoice') + inv_ref.signal_workflow(cr, SUPERUSER_ID, [inv_id], 'validate') + + if not inv_ids: return {} + + mod_obj = self.pool.get('ir.model.data') + res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') + res_id = res and res[1] or False + return { + 'name': _('Customer Invoice'), + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': [res_id], + 'res_model': 'account.invoice', + 'context': "{'type':'out_invoice'}", + 'type': 'ir.actions.act_window', + 'target': 'current', + 'res_id': inv_ids and inv_ids[0] or False, + } + + def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): + # Tricky, via the workflow, we only have one id in the ids variable + """Create a account move line of order grouped by products or not.""" + account_move_obj = self.pool.get('account.move') + account_tax_obj = self.pool.get('account.tax') + property_obj = self.pool.get('ir.property') + cur_obj = self.pool.get('res.currency') + + # session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) + + if session and not all( + session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): + raise UserError(_('Selected orders do not have the same session!')) + + grouped_data = {} + have_to_group_by = session and session.config_id.group_by or False + rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method + + for order in self.browse(cr, uid, ids, context=context): + if order.account_move: + continue + if order.state != 'paid': + continue + + current_company = order.sale_journal.company_id + + group_tax = {} + account_def = property_obj.get(cr, uid, 'property_account_receivable_id', 'res.partner', + context=context) + + order_account = order.partner_id and \ + order.partner_id.property_account_receivable_id and \ + order.partner_id.property_account_receivable_id.id or \ + account_def and account_def.id + + if move_id is None: + # Create an entry for the sale + # FORWARD-PORT UP TO SAAS-12 + journal_id = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, + 'pos.closing.journal_id_%s' % ( + current_company.id), + default=order.sale_journal.id, + context=context) + move_id = self._create_account_move(cr, uid, order.session_id.start_at, order.name, int(journal_id), + order.company_id.id, context=context) + + move = account_move_obj.browse(cr, SUPERUSER_ID, move_id, context=context) + + def insert_data(data_type, values): + # if have_to_group_by: + + # 'quantity': line.qty, + # 'product_id': line.product_id.id, + values.update({ + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False, + 'move_id': move_id, + }) + + if data_type == 'product': + key = ('product', values['partner_id'], + (values['product_id'], tuple(values['tax_ids'][0][2]), values['name']), + values['analytic_account_id'], values['debit'] > 0) + elif data_type == 'tax': + key = ('tax', values['partner_id'], values['tax_line_id'], values['debit'] > 0) + elif data_type == 'counter_part': + key = ('counter_part', values['partner_id'], values['account_id'], values['debit'] > 0) + else: + return + + grouped_data.setdefault(key, []) + + # if not have_to_group_by or (not grouped_data[key]): + # grouped_data[key].append(values) + # else: + # pass + + if have_to_group_by: + if not grouped_data[key]: + grouped_data[key].append(values) + else: + for line in grouped_data[key]: + if line.get('tax_code_id') == values.get('tax_code_id'): + current_value = line + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get( + 'quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', + 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + break + else: + grouped_data[key].append(values) + else: + grouped_data[key].append(values) + + # because of the weird way the pos order is written, we need to make sure there is at least one line, + # because just after the 'for' loop there are references to 'line' and 'income_account' variables (that + # are set inside the for loop) + # TOFIX: a deep refactoring of this method (and class!) is needed in order to get rid of this stupid hack + assert order.lines, _('The POS order must have lines when calling this method') + # Create an move for each order line + + cur = order.pricelist_id.currency_id + for line in order.lines: + amount = line.price_subtotal + + # Search for the income account + if line.product_id.property_account_income_id.id: + income_account = line.product_id.property_account_income_id.id + elif line.product_id.categ_id.property_account_income_categ_id.id: + income_account = line.product_id.categ_id.property_account_income_categ_id.id + else: + raise UserError(_('Please define income ' \ + 'account for this product: "%s" (id:%d).') \ + % (line.product_id.name, line.product_id.id)) + + name = line.product_id.name + if line.notice: + # add discount reason in move + name = name + ' (' + line.notice + ')' + + # Create a move for the line for the order line + insert_data('product', { + 'name': name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': income_account, + 'analytic_account_id': self._prepare_analytic_account(cr, uid, line, context=context), + 'credit': ((amount > 0) and amount) or 0.0, + 'debit': ((amount < 0) and -amount) or 0.0, + 'tax_ids': [(6, 0, line.tax_ids_after_fiscal_position.ids)], + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False + }) + + # Create the tax lines + taxes = [] + for t in line.tax_ids_after_fiscal_position: + if t.company_id.id == current_company.id: + taxes.append(t.id) + if not taxes: + continue + for tax in account_tax_obj.browse(cr, uid, taxes, context=context).compute_all( + line.price_unit * (100.0 - line.discount) / 100.0, cur, line.qty)['taxes']: + insert_data('tax', { + 'name': _('Tax') + ' ' + tax['name'], + 'product_id': line.product_id.id, + 'quantity': line.qty, + 'account_id': tax['account_id'] or income_account, + 'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0, + 'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0, + 'tax_line_id': tax['id'], + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False + }) + + # round tax lines per order + if rounding_method == 'round_globally': + for group_key, group_value in grouped_data.iteritems(): + if group_key[0] == 'tax': + for line in group_value: + line['credit'] = cur.round(line['credit']) + line['debit'] = cur.round(line['debit']) + + if order.discount_total: + insert_data('counter_part', { + 'name': 'Discount', + 'account_id': order.session_id.config_id.discount_account.id, + 'credit': ((order.discount_total < 0) and -order.discount_total) or 0.0, + 'debit': ((order.discount_total > 0) and order.discount_total) or 0.0, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False + }) + elif order.discount_percent: + discount = (100 * order.amount_total) / (100 - order.discount_percent) + discount -= order.amount_total + print discount, "--------------" + insert_data('counter_part', { + 'name': 'Discount', + 'account_id': order.session_id.config_id.discount_account.id, + 'credit': ((discount < 0) and -discount) or 0.0, + 'debit': ((discount > 0) and discount) or 0.0, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False + }) + # counterpart + insert_data('counter_part', { + 'name': _("Trade Receivables"), # order.name, + 'account_id': order_account, + 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, + 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, + 'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner( + order.partner_id).id or False + }) + + order.write({'state': 'done', 'account_move': move_id}) + + all_lines = [] + for group_key, group_data in grouped_data.iteritems(): + for value in group_data: + all_lines.append((0, 0, value), ) + if move_id: # In case no order was changed + self.pool.get("account.move").write(cr, SUPERUSER_ID, [move_id], {'line_ids': all_lines}, + context=dict(context or {}, dont_create_taxes=True)) + self.pool.get("account.move").post(cr, SUPERUSER_ID, [move_id], context=context) + + return True + + +class NewPosLines(osv.osv): + _inherit = "pos.order.line" + + _columns = { + 'price_unit': fields.float(string='Unit Price', digits=0), + 'qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')), + 'discount_fixed': fields.float('Discount Fixed'), + 'discountStr': fields.char('discountStr'), + + } + + _defaults = { + 'discount_fixed': lambda *a: 0.0, + } + + @api.onchange('discount_fixed') + def change_discount_fixed_line(self): + if self.discount_fixed: + self.discount = 0.0 + + @api.onchange('discount') + def change_discount_line(self): + if self.discount: + self.discount_fixed = 0.0 + + @api.onchange('qty') + def change_qty_line(self): + self._compute_amount_line_all() + + @api.onchange('price_unit') + def change_price_unit_line(self): + self._compute_amount_line_all() + + @api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'discount_fixed', '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) +# =============== finding subtotal for each orderline========================================================= + if line.discount_fixed != 0: + price = line.price_unit + line.price_subtotal = line.price_subtotal_incl = price * line.qty - line.discount_fixed + else: + price = line.price_unit + line.price_subtotal = line.price_subtotal_incl = (price * line.qty) - (price * line.qty * (line.discount or 0.0) / 100) + 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) + + +class AccountMoveLineExtra(models.Model): + _inherit = 'account.move.line' + + _sql_constraints = [ + ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), + ('credit_debit2', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), + ] + + @api.multi + def _update_check(self): + """ This module is overwritten, to avoid the warning raised when we edit the journal entries""" + move_ids = set() + for line in self: + err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id)) + if line.move_id.id not in move_ids: + move_ids.add(line.move_id.id) + self.env['account.move'].browse(list(move_ids))._check_lock_date() + return True + + @api.multi + def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False): + """ This function is overwritten to remove some warnings""" + # Perform all checks on lines + company_ids = set() + all_accounts = [] + partners = set() + for line in self: + company_ids.add(line.company_id.id) + all_accounts.append(line.account_id) + if (line.account_id.internal_type in ('receivable', 'payable')): + partners.add(line.partner_id.id) + if line.reconciled: + raise UserError(_('You are trying to reconcile some entries that are already reconciled!')) + if len(company_ids) > 1: + raise UserError(_('To reconcile the entries company should be the same for all entries!')) + if not all_accounts[0].reconcile: + raise UserError(_('The account %s (%s) is not marked as reconciliable !') % ( + all_accounts[0].name, all_accounts[0].code)) + # reconcile everything that can be + remaining_moves = self.auto_reconcile_lines() + + # if writeoff_acc_id specified, then create write-off move with value the remaining amount from move in self + if writeoff_acc_id and writeoff_journal_id and remaining_moves: + all_aml_share_same_currency = all([x.currency_id == self[0].currency_id for x in self]) + writeoff_vals = { + 'account_id': writeoff_acc_id.id, + 'journal_id': writeoff_journal_id.id + } + if not all_aml_share_same_currency: + writeoff_vals['amount_currency'] = False + writeoff_to_reconcile = remaining_moves._create_writeoff(writeoff_vals) + # add writeoff line to reconcile algo and finish the reconciliation + remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines() + return writeoff_to_reconcile + return True + + +class AccountMoveNew(models.Model): + _inherit = 'account.move' + + @api.multi + def assert_balanced(self): + """Overwritten to remove the warning raised.(For editing the journal entry)""" + if not self.ids: + return True + prec = self.env['decimal.precision'].precision_get('Account') + + self._cr.execute("""\ + SELECT move_id + FROM account_move_line + WHERE move_id in %s + GROUP BY move_id + HAVING abs(sum(debit) - sum(credit)) > %s + """, (tuple(self.ids), 10 ** (-max(5, prec)))) + return True + + + + + + + + + + + + diff --git a/discounts_in_pos/models/pos_payment_report_new.py b/discounts_in_pos/models/pos_payment_report_new.py new file mode 100644 index 000000000..3405ce06e --- /dev/null +++ b/discounts_in_pos/models/pos_payment_report_new.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +import time +from openerp.osv import osv +from openerp.report import report_sxw + + +class pos_payment_report_new(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(pos_payment_report_new, self).__init__(cr, uid, name, context=context) + self.total = 0.0 + self.localcontext.update({ + 'time': time, + 'pos_payment': self._pos_payment, + 'pos_payment_total':self._pos_payment_total, + 'pos_fixed_discount':self._pos_fixed_discount, + 'pos_percent_discount': self._pos_percent_discount, + }) + + def _pos_payment(self, obj): + self.total = 0 + self.discount_total = 0 + self.discount_percent = 0 + data={} + new_data={} + sql = """ select id from pos_order where id = %d"""%(obj.id) + self.cr.execute(sql) + if self.cr.fetchone(): + self.cr.execute ("select pol.id,pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.discount_fixed,pol.price_unit, " \ + "(pol.price_unit * pol.qty) as total " \ + "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ + "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ + "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date and po.id=%d"%(obj.id)) + data = self.cr.dictfetchall() + + else: + self.cr.execute ("select pt.name,pp.default_code as code,pol.qty,pu.name as uom,pol.discount,pol.discount_fixed,pol.price_unit, " \ + "(pol.price_unit * pol.qty) as total " \ + "from pos_order as po,pos_order_line as pol,product_product as pp,product_template as pt, product_uom as pu " \ + "where pt.id=pp.product_tmpl_id and pp.id=pol.product_id and po.id = pol.order_id and pu.id=pt.uom_id " \ + "and po.state IN ('paid','invoiced') and to_char(date_trunc('day',po.date_order),'YYYY-MM-DD')::date = current_date") + data = self.cr.dictfetchall() + + for d in data: + if d['discount_fixed'] != 0: + d['total'] = d['price_unit'] * d['qty'] - d['discount_fixed'] + else: + d['total'] = d['price_unit'] * d['qty'] * (1 - (d['discount'] / 100)) + + self.total = obj.amount_total + self.discount_total += obj.discount_total + self.discount_percent += obj.discount_percent + return data + + def _pos_payment_total(self, o): + return self.total + + def _pos_fixed_discount(self, o): + return self.discount_total + + def _pos_percent_discount(self, o): + return self.discount_percent + + +class report_pos_payment(osv.AbstractModel): + _name = 'report.discounts_in_pos.report_payment' + _inherit = 'report.abstract_report' + _template = 'discounts_in_pos.report_payment' + _wrapped_report_class = pos_payment_report_new diff --git a/discounts_in_pos/models/pos_receipt_new.py b/discounts_in_pos/models/pos_receipt_new.py new file mode 100644 index 000000000..e6341e92e --- /dev/null +++ b/discounts_in_pos/models/pos_receipt_new.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +import time +from openerp.osv import osv +from openerp.report import report_sxw + + +def titlize(journal_name): + words = journal_name.split() + while words.pop() != 'journal': + continue + return ' '.join(words) + + +class order(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(order, self).__init__(cr, uid, name, context=context) + + user = self.pool['res.users'].browse(cr, uid, uid, context=context) + partner = user.company_id.partner_id + + self.localcontext.update({ + 'time': time, + 'disc': self.discount, + 'net': self.netamount, + 'get_journal_amt': self._get_journal_amt, + 'address': partner or False, + 'titlize': titlize + }) + + def netamount(self, order_line_id): + sql = 'select (qty*price_unit) as net_price from pos_order_line where id = %s' + self.cr.execute(sql, (order_line_id,)) + res = self.cr.fetchone() + return res[0] + + def discount(self, order_id): + sql = 'select discount, discount_fixed, price_unit, qty from pos_order_line where order_id = %s ' + self.cr.execute(sql, (order_id,)) + res = self.cr.fetchall() + dsum = 0 + for line in res: + if line[1] != 0: + dsum = dsum + (line[1]) + else: + dsum = dsum + (line[3] * (line[0] * line[2] / 100)) + return dsum + + def _get_journal_amt(self, order_id): + data={} + sql = """ select aj.name,absl.amount as amt from account_bank_statement as abs + LEFT JOIN account_bank_statement_line as absl ON abs.id = absl.statement_id + LEFT JOIN account_journal as aj ON aj.id = abs.journal_id + WHERE absl.pos_statement_id =%d"""%(order_id) + self.cr.execute(sql) + data = self.cr.dictfetchall() + return data + + +class report_order_receipt(osv.AbstractModel): + _name = 'report.discounts_in_pos.report_receipt' + _inherit = 'report.abstract_report' + _template = 'discounts_in_pos.report_receipt' + _wrapped_report_class = order diff --git a/discounts_in_pos/static/description/account.png b/discounts_in_pos/static/description/account.png new file mode 100644 index 000000000..73d53edcd Binary files /dev/null and b/discounts_in_pos/static/description/account.png differ diff --git a/discounts_in_pos/static/description/banner.jpg b/discounts_in_pos/static/description/banner.jpg new file mode 100644 index 000000000..f1fc61b85 Binary files /dev/null and b/discounts_in_pos/static/description/banner.jpg differ diff --git a/discounts_in_pos/static/description/cybro_logo.png b/discounts_in_pos/static/description/cybro_logo.png new file mode 100644 index 000000000..bb309114c Binary files /dev/null and b/discounts_in_pos/static/description/cybro_logo.png differ diff --git a/discounts_in_pos/static/description/discount-button.png b/discounts_in_pos/static/description/discount-button.png new file mode 100644 index 000000000..34b0a64a5 Binary files /dev/null and b/discounts_in_pos/static/description/discount-button.png differ diff --git a/discounts_in_pos/static/description/icon.png b/discounts_in_pos/static/description/icon.png new file mode 100644 index 000000000..c7dc5c884 Binary files /dev/null and b/discounts_in_pos/static/description/icon.png differ diff --git a/discounts_in_pos/static/description/index.html b/discounts_in_pos/static/description/index.html new file mode 100644 index 000000000..017c5a574 --- /dev/null +++ b/discounts_in_pos/static/description/index.html @@ -0,0 +1,169 @@ +
+
+

POS Discount Total

+

POS Discount Total(Fixed and Percentage)

+

Cybrosys Technologies , www.cybrosys.com

+
+

Features:

+
    +
  •    Percentage discount on order lines.
  • +
  •    Fixed discount on order lines.
  • +
  •    Percentage discount on order.
  • +
  •    Fixed discount on order.
  • +
+
+
+
+ +
+
+
+
+
+

+ This module allows us to provide different types of discounts in point of sale. + Possible discounts are, fixed discounts and percentage discounts(Order lines and Order). +

+
+ +
+ +
+
+
+ +
+
+

Fixed discount on order lines.

+
+
+
+
+ +
+
+ +
+
+

+ A new button 'Disc.Fixed' is added for providing fixed discounts on orderlines. By clicking on this + button, we can enter the fixed discount. After we have entered the discount, + it will be shown under the product name of the orderline to which we have applied the discount. +

+
+
+
+
+ +
+
+

Percentage discount on order.

+
+
+
+

+ If we need to provide discount on the total amount of an order, it can be done using this module. + The button 'Disc(Total%)' can be used to provide percentage discounts to an order. +

+
+
+
+
+
+ +
+
+

Fixed discount on order.

+
+
+

+ This feature can be used to apply a fixed amount discount to an order. The button 'Dis.Fixed(Total)' + can be used for this purpose.

+
+
+
+
+ +
+
+
+
+

+ We can have either one type of discount at a time for an order and order line. i.e, it is + not possible to provide percentage discount and fixed discount at the same time. + Suppose we have applied fixed discount on an order or orderline, then if we try to apply + percentage discount, the existing discount will be removed and new one will be applied. +

+

+ The applied discounts will be shown in the receipts and the reports. +

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

+ Before providing discounts we need to set an account for recording the discount amount. It can be + created when we are creating the point of sale. +

+
+
+ +
+ +
+
+
+ +
+

Need Any Help?

+ +
diff --git a/discounts_in_pos/static/description/receipt-percentage.png b/discounts_in_pos/static/description/receipt-percentage.png new file mode 100644 index 000000000..a19161a33 Binary files /dev/null and b/discounts_in_pos/static/description/receipt-percentage.png differ diff --git a/discounts_in_pos/static/description/receipt.png b/discounts_in_pos/static/description/receipt.png new file mode 100644 index 000000000..1a86def5e Binary files /dev/null and b/discounts_in_pos/static/description/receipt.png differ diff --git a/discounts_in_pos/static/description/total-disc.png b/discounts_in_pos/static/description/total-disc.png new file mode 100644 index 000000000..31e3c83b8 Binary files /dev/null and b/discounts_in_pos/static/description/total-disc.png differ diff --git a/discounts_in_pos/static/src/js/discount.js b/discounts_in_pos/static/src/js/discount.js new file mode 100644 index 000000000..dcc896b04 --- /dev/null +++ b/discounts_in_pos/static/src/js/discount.js @@ -0,0 +1,486 @@ +odoo.define('discounts_in_pos', function (require) { +"use strict"; + +var screens = require('point_of_sale.screens'); +var models = require('point_of_sale.models'); +var core = require('web.core'); +var formats = require('web.formats'); +var utils = require('web.utils'); + +var QWeb = core.qweb; +var round_di = utils.round_decimals; +var round_pr = utils.round_precision; + +screens.OrderWidget.include({ + + set_value: function(val) { + var order = this.pos.get_order(); + if (order.get_selected_orderline()) { + var mode = this.numpad_state.get('mode'); + if( mode === 'quantity'){ + order.get_selected_orderline().set_quantity(val); + }else if( mode === 'discount'){ + order.get_selected_orderline().set_discount(val); + }else if( mode === 'price'){ + order.get_selected_orderline().set_unit_price(val); + } + // for the new button 'DiscFix' + else if( mode === 'discount_fixed'){ + console.log(order) + console.log(order.get_selected_orderline()) + order.get_selected_orderline().set_discount_fixed(val); + } + else if( mode === 'discount_total'){ + order.get_selected_orderline().set_discount_total(val); + order.set_discount_total_order(val); + } + else if( mode === 'discount_percent'){ + order.get_selected_orderline().set_discount_percent(val); + order.set_discount_percent_order(val); + } + } + }, +// ============================updates total discount(fixed and percentage) on order ========================================== + update_summary: function(){ + var order = this.pos.get_order(); + if (!order.get_orderlines().length) { + return; + } + var total = order ? order.get_total_with_tax() : 0; + var taxes = order ? total - order.get_total_without_tax() : 0; + var discount_total = (order && order.get_discount_total_order() > 0) ? order.get_discount_total_order() : 0; + var discount_percent = (order && order.get_discount_percent_order() > 0) ? order.get_discount_percent_order() : 0; + + this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total); + this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes); + + if (discount_total > 0) { + this.el.querySelector('.summary .total .subentry .value_discount_percent').textContent = ""; + this.el.querySelector('.summary .total .subentry .value_discount_total').textContent = "Discount(Total Fixed)" +this.format_currency(discount_total); + } + else { + this.el.querySelector('.summary .total .subentry .value_discount_total').textContent = ""; + } + if (discount_percent > 0) { + this.el.querySelector('.summary .total .subentry .value_discount_total').textContent = ""; + this.el.querySelector('.summary .total .subentry .value_discount_percent').textContent = "Discount(Total %)" + discount_percent; + } + else { + this.el.querySelector('.summary .total .subentry .value_discount_percent').textContent = ""; + } + }, +}); +var OrderlineSuper = models.Orderline; +models.Orderline = models.Orderline.extend({ + initialize: function(attr,options){ + OrderlineSuper.prototype.initialize.call(this, attr,options); + this.discount_fixed = 0; + this.discount_total = 0; + this.discount_percent = 0; + }, + init_from_JSON: function(json) { + OrderlineSuper.prototype.init_from_JSON.call(this, json); + if(json.discount_fixed > 0) { + this.set_discount_fixed(json.discount_fixed); + } + else { + this.set_discount(json.discount); + } + }, + clone: function(){ + var orderline = OrderlineSuper.prototype.clone.call(this); + orderline.discount_fixed = this.discount_fixed; + return orderline; + }, + set_discount: function(discount){ + OrderlineSuper.prototype.set_discount.call(this, discount); + this.discount_fixed = 0.0; + this.discount_total = 0.0; + this.discount_percent = 0.0; + this.discountStr = 'percentage'; + this.trigger('change',this); + }, + set_discount_fixed: function(discount){ + this.discount_fixed = discount; + this.discount = 0.0; + this.discount_total = 0.0; + this.discount_percent = 0.0; + this.discountStr = 'fixed' ; + this.trigger('change',this); + }, + set_discount_total: function(discount){ + this.discount_total = discount; + this.discount_percent = 0.0; + }, + set_discount_percent: function(discount){ + var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); + this.discount_percent = disc; + this.discount_total = 0.0; + }, + get_discount_total: function(){ + return this.discount_total; + }, + get_discount_percent: function(){ + return this.discount_percent; + }, + get_discount_fixed: function(){ + return this.discount_fixed; + }, + set_quantity: function(quantity){ + var order = this.pos.get_order(); + if (order) { + if(order.selected_orderline == undefined) { + order.set_discount_total_order(0); + order.set_discount_percent_order(0); + } + } + + this.order.assert_editable(); + if(quantity === 'remove'){ + this.order.remove_orderline(this); + return; + }else{ + var quant = parseFloat(quantity) || 0; + + var unit = this.get_unit(); + if(unit){ + if (unit.rounding) { + this.quantity = round_pr(quant, unit.rounding); + var decimals = this.pos.dp['Product Unit of Measure']; + this.quantityStr = formats.format_value(round_di(this.quantity, decimals), { type: 'float', digits: [69, decimals]}); + } else { + this.quantity = round_pr(quant, 1); + this.quantityStr = this.quantity.toFixed(0); + } + }else{ + this.quantity = quant; + this.quantityStr = '' + this.quantity; + } + } + this.trigger('change',this); + }, + can_be_merged_with: function(orderline){ + if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged + return false; + }else if(!this.get_unit() || !this.get_unit().groupable){ + return false; + }else if(this.get_product_type() !== orderline.get_product_type()){ + return false; + }else if(this.get_discount() > 0){ // we don't merge discounted orderlines + return false; + }else if(this.get_discount_fixed() > 0){ // we don't merge discounted orderlines + return false; + }else if(this.price !== orderline.price){ + return false; + }else{ + return true; + } + }, + export_as_JSON: function() { + return { + qty: this.get_quantity(), + price_unit: this.get_unit_price(), + discount: this.get_discount(), + discount_fixed: this.get_discount_fixed(), + discount_total: this.get_discount_total(), + discount_percent: this.get_discount_percent(), + discountStr:this.get_discount_str(), + product_id: this.get_product().id, + tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]], + id: this.id, + }; + }, + export_for_printing: function(){ + return { + quantity: this.get_quantity(), + unit_name: this.get_unit().name, + price: this.get_unit_display_price(), + discount: this.get_discount(), + discount_fixed: this.get_discount_fixed(), + discount_total: this.get_discount_total(), + discount_percent: this.get_discount_percent(), + discountStr: this.get_discount_str(), + product_name: this.get_product().display_name, + price_display : this.get_display_price(), + price_with_tax : this.get_price_with_tax(), + price_without_tax: this.get_price_without_tax(), + tax: this.get_tax(), + product_description: this.get_product().description, + product_description_sale: this.get_product().description_sale, + }; + }, + get_base_price: function(){ + var rounding = this.pos.currency.rounding; + if(this.discount_fixed !== 0){ + return round_pr(this.get_unit_price() * this.get_quantity() - this.get_discount_fixed(), rounding); + } + + return round_pr(this.get_unit_price() * this.get_quantity() * (1 - this.get_discount()/100), rounding); + }, + get_all_prices: function(){ + + if(this.discount_fixed > 0) + { + var price_unit = this.get_unit_price() * this.get_quantity() - this.get_discount_fixed(); + } + else { + var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)); + } + var taxtotal = 0; + + var product = this.get_product(); + var taxes_ids = product.taxes_id; + var taxes = this.pos.taxes; + var taxdetail = {}; + var product_taxes = []; + + _(taxes_ids).each(function(el){ + product_taxes.push(_.detect(taxes, function(t){ + return t.id === el; + })); + }); + + var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding); + _(all_taxes.taxes).each(function(tax) { + taxtotal += tax.amount; + taxdetail[tax.id] = tax.amount; + }); + if(this.get_discount_fixed() != 0) { + all_taxes.total_excluded = price_unit; + } + return { + "priceWithTax": all_taxes.total_included, + "priceWithoutTax": all_taxes.total_excluded, + "tax": taxtotal, + "taxDetails": taxdetail, + }; + }, +}); + +var OrderSuper = models.Order; +models.Order = models.Order.extend({ + initialize: function(attributes,options){ + var order = OrderSuper.prototype.initialize.call(this, attributes,options); + order.discount_total = 0; + order.discount_percent = 0; + return order; + }, + init_from_JSON: function(json) { + OrderSuper.prototype.init_from_JSON.call(this, json); + this.discount_total = json.discount_total; + this.discount_percent = json.discount_percent; + }, + export_as_JSON: function() { + var json_new = OrderSuper.prototype.export_as_JSON.call(this); + json_new.discount_total = this.get_discount_total_order(); + json_new.discount_percent = this.get_discount_percent_order(); + return json_new; + }, + export_for_printing: function(){ + var orderlines = []; + var self = this; + + this.orderlines.each(function(orderline){ + orderlines.push(orderline.export_for_printing()); + }); + + var paymentlines = []; + this.paymentlines.each(function(paymentline){ + paymentlines.push(paymentline.export_for_printing()); + }); + var client = this.get('client'); + var cashier = this.pos.cashier || this.pos.user; + var company = this.pos.company; + var shop = this.pos.shop; + var date = new Date(); + + function is_xml(subreceipt){ + return subreceipt ? (subreceipt.split('\n')[0].indexOf('= 0) : false; + } + + function render_xml(subreceipt){ + if (!is_xml(subreceipt)) { + return subreceipt; + } else { + subreceipt = subreceipt.split('\n').slice(1).join('\n'); + var qweb = new QWeb2.Engine(); + qweb.debug = core.debug; + qweb.default_dict = _.clone(QWeb.default_dict); + qweb.add_template(''+subreceipt+''); + + return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ; + } + } + + var receipt = { + orderlines: orderlines, + paymentlines: paymentlines, + subtotal: this.get_subtotal(), + total_with_tax: this.get_total_with_tax(), + total_without_tax: this.get_total_without_tax(), + total_tax: this.get_total_tax(), + total_paid: this.get_total_paid(), + discount_total_fixed: this.get_discount_total_order(), + discount_total_percent: this.get_discount_percent_order(), + total_discount: this.get_total_discount(), + tax_details: this.get_tax_details(), + change: this.get_change(), + name : this.get_name(), + client: client ? client.name : null , + invoice_id: null, //TODO + cashier: cashier ? cashier.name : null, + precision: { + price: 2, + money: 2, + quantity: 3, + }, + date: { + year: date.getFullYear(), + month: date.getMonth(), + date: date.getDate(), // day of the month + day: date.getDay(), // day of the week + hour: date.getHours(), + minute: date.getMinutes() , + isostring: date.toISOString(), + localestring: date.toLocaleString(), + }, + company:{ + email: company.email, + website: company.website, + company_registry: company.company_registry, + contact_address: company.partner_id[1], + vat: company.vat, + name: company.name, + phone: company.phone, + logo: this.pos.company_logo_base64, + }, + shop:{ + name: shop.name, + }, + currency: this.pos.currency, + }; + + if (is_xml(this.pos.config.receipt_header)){ + receipt.header = ''; + receipt.header_xml = render_xml(this.pos.config.receipt_header); + } else { + receipt.header = this.pos.config.receipt_header || ''; + } + + if (is_xml(this.pos.config.receipt_footer)){ + receipt.footer = ''; + receipt.footer_xml = render_xml(this.pos.config.receipt_footer); + } else { + receipt.footer = this.pos.config.receipt_footer || ''; + } + + return receipt; + }, + set_discount_total_order: function(discount){ + this.discount_total = discount; + this.discount_percent = 0; + this.trigger('change',this); + }, + get_discount_total_order: function(){ + return this.discount_total; + }, + set_discount_percent_order: function(discount){ + var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); + this.discount_percent = disc; + this.discount_total = 0.0; + this.trigger('change',this); + }, + get_discount_percent_order: function(){ + return this.discount_percent; + }, + get_total_without_tax: function() { + var total_fixed_disc = this.get_discount_total_order(); + var total_percent_disc = this.get_discount_percent_order(); + if (total_fixed_disc) { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }), 0), this.pos.currency.rounding) - total_fixed_disc ; + } + if (total_percent_disc) { + var temp = round_pr(this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }), 0), this.pos.currency.rounding); + return (temp - (temp * total_percent_disc / 100)) + } + return round_pr(this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }), 0), this.pos.currency.rounding); + }, + get_total_discount: function() { + var sum = OrderSuper.prototype.get_total_discount.call(this); + sum = 0.0; + var disc = 0.0; + for (var i = 0; i < this.orderlines.length; i++) { + var NewOrder = this.orderlines.models[i]; + disc += (NewOrder.quantity * NewOrder.price); + if (NewOrder.discountStr == 'fixed') { + sum += parseFloat(NewOrder.discount_fixed); + } + else { + sum += NewOrder.quantity * NewOrder.price * (parseFloat(NewOrder.discount) / 100); + } + } + if (this.discount_total) { sum += parseFloat(this.discount_total); } + disc -= parseFloat(this.get_total_without_tax() + sum); + if (this.discount_percent) { sum += disc; } + + return sum; + + }, +}); +models.PosModel = models.PosModel.extend({ + push_and_invoice_order: function(order){ + var self = this; + var invoiced = new $.Deferred(); + + if(!order.get_client()){ + invoiced.reject({code:400, message:'Missing Customer', data:{}}); + return invoiced; + } + + var order_id = this.db.add_order(order.export_as_JSON()); + + this.flush_mutex.exec(function(){ + var done = new $.Deferred(); // holds the mutex + + // send the order to the server + // we have a 30 seconds timeout on this push. + // FIXME: if the server takes more than 30 seconds to accept the order, + // the client will believe it wasn't successfully sent, and very bad + // things will happen as a duplicate will be sent next time + // so we must make sure the server detects and ignores duplicated orders + + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); + + transfer.fail(function(error){ + invoiced.reject(error); + done.reject(); + }); + + // on success, get the order id generated by the server + transfer.pipe(function(order_server_id){ + + // generate the pdf and download it + self.chrome.do_action('discounts_in_pos.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}); + + invoiced.resolve(); + done.resolve(); + }); + + return done; + + }); + + return invoiced; + }, +}); +}); + + diff --git a/discounts_in_pos/static/src/xml/discount.xml b/discounts_in_pos/static/src/xml/discount.xml new file mode 100644 index 000000000..b859ec148 --- /dev/null +++ b/discounts_in_pos/static/src/xml/discount.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+ +
+ +
+ + +
  • + + + + + + +
      + +
    • + + + + + at + + / + +
    • +
      + +
    • + With a + + % + + discount +
    • +
      + +
    • + With a + + + + discount +
    • +
      +
    +
  • +
    + + +
    +
    +
    + +
    + +

    Your shopping cart is empty

    +
    +
    + +
      +
      +
      +
      + Total: 0.00 € +
      Taxes: 0.00€
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + +
      + +
      +
      +
      + Phone:
      + User:
      + Shop:
      +
      + +
      + +
      +
      +
      + + + + + + + + + + + +
      + + +
      + With a % discount +
      +
      + +
      + With a discount +
      +
      +
      + + + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Subtotal: + +
      + +
      Disc.Fixed(Total): + +
      Disc.Percent(Total): + % +
      Total Discount: + +
      Total: + +
      +
      + + + + + + + +
      + + + +
      +
      + + +
      Change: + +
      + +
      +
      + +
      +
      +
      +
      + +
      diff --git a/discounts_in_pos/views/account_invoice_view_pos.xml b/discounts_in_pos/views/account_invoice_view_pos.xml new file mode 100644 index 000000000..0941ff0bd --- /dev/null +++ b/discounts_in_pos/views/account_invoice_view_pos.xml @@ -0,0 +1,21 @@ + + + + + + pos.order.invoice.extend + account.invoice + + + + + + + + + + + + + + \ No newline at end of file diff --git a/discounts_in_pos/views/pos_reports_account_invoice.xml b/discounts_in_pos/views/pos_reports_account_invoice.xml new file mode 100644 index 000000000..445e09709 --- /dev/null +++ b/discounts_in_pos/views/pos_reports_account_invoice.xml @@ -0,0 +1,186 @@ + + + + + + + + + \ No newline at end of file diff --git a/discounts_in_pos/views/pos_reports_new_invoice.xml b/discounts_in_pos/views/pos_reports_new_invoice.xml new file mode 100644 index 000000000..d11668fc3 --- /dev/null +++ b/discounts_in_pos/views/pos_reports_new_invoice.xml @@ -0,0 +1,186 @@ + + + + + + + + + \ No newline at end of file diff --git a/discounts_in_pos/views/pos_view.xml b/discounts_in_pos/views/pos_view.xml new file mode 100644 index 000000000..5d033311d --- /dev/null +++ b/discounts_in_pos/views/pos_view.xml @@ -0,0 +1,89 @@ + + + + + pos.config.extend + pos.config + + + + + + + + + pos.order.extend + pos.order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/discounts_in_pos/views/report_paperformat_new.xml b/discounts_in_pos/views/report_paperformat_new.xml new file mode 100644 index 000000000..d5bc0dba8 --- /dev/null +++ b/discounts_in_pos/views/report_paperformat_new.xml @@ -0,0 +1,20 @@ + + + + + Point Of Sale Receipt + + custom + 150 + 90 + Portrait + 3 + 3 + 3 + 3 + + 3 + 130 + + + diff --git a/discounts_in_pos/views/report_payment_new.xml b/discounts_in_pos/views/report_payment_new.xml new file mode 100644 index 000000000..572823705 --- /dev/null +++ b/discounts_in_pos/views/report_payment_new.xml @@ -0,0 +1,113 @@ + + + + + + diff --git a/discounts_in_pos/views/report_receipt_new.xml b/discounts_in_pos/views/report_receipt_new.xml new file mode 100644 index 000000000..d462ef824 --- /dev/null +++ b/discounts_in_pos/views/report_receipt_new.xml @@ -0,0 +1,112 @@ + + + + + + diff --git a/discounts_in_pos/views/report_saleslines_new.xml b/discounts_in_pos/views/report_saleslines_new.xml new file mode 100644 index 000000000..b76fa11a8 --- /dev/null +++ b/discounts_in_pos/views/report_saleslines_new.xml @@ -0,0 +1,125 @@ + + + + + + diff --git a/discounts_in_pos/views/templates.xml b/discounts_in_pos/views/templates.xml new file mode 100644 index 000000000..6db002498 --- /dev/null +++ b/discounts_in_pos/views/templates.xml @@ -0,0 +1,12 @@ + + + + + + + +