diff --git a/app/mainwindow (SFConflict oscar.alvarez.montero@gmail.com 2020-06-28-10-33-14).py b/app/mainwindow (SFConflict oscar.alvarez.montero@gmail.com 2020-06-28-10-33-14).py new file mode 100644 index 0000000..a3a124a --- /dev/null +++ b/app/mainwindow (SFConflict oscar.alvarez.montero@gmail.com 2020-06-28-10-33-14).py @@ -0,0 +1,2644 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +import sys +import os +import logging +from decimal import Decimal + +from datetime import datetime, timedelta, date +from collections import OrderedDict +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QTouchEvent +from PyQt5.QtWidgets import (QLabel, QTextEdit, QHBoxLayout, QVBoxLayout, + QWidget, QGridLayout, QLineEdit, QDoubleSpinBox) + +from app.commons.action import Action +from app.commons.forms import GridForm, FieldMoney, ComboBox +from app.commons.messages import MessageBar +from app.commons.image import Image +from app.commons.dialogs import QuickDialog +from app.commons.table import TableView +from app.commons.model import TableModel, Modules +from app.commons.search_window import SearchWindow +from app.commons.frontwindow import FrontWindow +from app.commons.menu_buttons import MenuDash + +from .proxy import FastModel +from .localdb import LocalStore +from .reporting import Receipt +from .buttonpad import Buttonpad +from .manage_tables import ManageTables +from .states import STATES, RE_SIGN +from .tools import get_icon, to_float, to_numeric +from .constants import (PATH_PRINTERS, DELTA_LOCALE, STRETCH, alignRight, + alignLeft, alignCenter, alignHCenter, alignVCenter, DIALOG_REPLY_NO, + DIALOG_REPLY_YES, ZERO, FRACTIONS, RATE_CREDIT_LIMIT, SCREENS, FILE_BANNER, + CONVERSION_DIGITS) + + +class MainWindow(FrontWindow): + + def __init__(self, connection, params): + title = "PRESIK | SMART POS" + global CONNECTION + self.conn = connection + CONNECTION = connection + super(MainWindow, self).__init__(connection, params, title) + print('Screen Size: > ', self.screen_size) + self.set_style(SCREENS[self.screen_size]) + + self.is_clear_right_panel = True + self.payment_ctx = {} + self.set_keys() + self.stock_context = None + + self.ctx = self._context + self.ctx['params'] = params + response = self.load_modules() + if response is not True: + d = self.dialog(response) + d.exec_() + super(MainWindow, self).close() + return + self.setup_sale_line() + self.setup_payment() + self.set_domains() + self.create_gui() + self.message_bar.load_stack(self.stack_msg) + + if not hasattr(self, 'auto_print_commission'): + self.auto_print_commission = False + + self.active_usb_printers = [] + + if os.name == 'posix' and os.path.exists(PATH_PRINTERS): + self.set_printers_usb(PATH_PRINTERS) + + self.set_printing_context() + if not self.tablet_mode: + self.create_statusbar() + + self.window().showMaximized() + self.create_dialog_search_products() + + if not self.tablet_mode: + self.grabKeyboard() + self.reader_thread = None + self._current_line_id = None + self._amount_text = '' + self._sign = None + self.create_dialogs() + self.createNewSale() + if not hasattr(self, 'active_weighing'): + self.active_weighing = False + elif self.active_weighing is True: + from .electronic_scale import ScaleReader + self.reader_thread = ScaleReader() + self.reader_thread.sigSetWeight.connect(self.set_weight_readed) + + self.do_invoice = DoInvoice(self, self._context) + self.do_invoice.sigDoInvoice.connect(self.__do_invoice_thread) + self.set_cache_company() + self.set_cache_products() + + def set_domains(self): + self.domain_search_product = [ + ('code', '!=', None), + ('active', '=', True), + ('template.salable', '=', True), + ('template.account_category', '!=', None), + ] + + if self.shop['product_categories']: + self.domain_search_product.append( + ('account_category', 'in', self.shop['product_categories']) + ) + + def filter_cache(self, data, filter, target): + res = [] + for d in data: + for t in target: + if t in d[filter]: + res.append(d) + return res + + def set_cache_company(self): + self.store = LocalStore() + self.store.create_table_config() + self._local_config = self.store.get_config() + if not self._local_config: + company_id = self.device['shop']['company']['id'] + self._local_config = self.store.set_config([company_id]) + + def set_cache_products(self): + self.store.create_table_product() + local_products = self.store.get_local_products() + + config = self.store.get_config() + _sync_date = config[1] + products = self.Product.sync_get_products({'write_date': _sync_date, 'shop_id': self.ctx['shop']}) + self.store.update_products(products, local_products) + now = datetime.now() + self.store.set_config_sync(str(now)) + + def event(self, evento): + event_type = super(MainWindow, self).event(evento) + touch = QTouchEvent(event_type) + return event_type + + def set_printers_usb(self, PATH_PRINTERS): + for usb_dev in os.listdir(PATH_PRINTERS): + if 'lp' not in usb_dev: + continue + path_device = os.path.join(PATH_PRINTERS, usb_dev) + self.active_usb_printers.append(['usb', path_device]) + + def get_current_sale(self): + if hasattr(self, '_sale') and self._sale['id']: + sales = self.ModSale.find([ + ('id', '=', self._sale['id']) + ]) + if not sales: + return + return sales[0] + + def check_empty_sale(self): + sale = self.get_current_sale() + if sale and self.model_sale_lines.rowCount() == 0 \ + and sale['state'] == 'draft' and not sale['number']: + self.delete_current_sale() + + def close(self): + dialog = self.dialog('confirm_exit', response=True) + response = dialog.exec_() + if response == DIALOG_REPLY_YES: + self.check_empty_sale() + if self.active_weighing and self.reader_thread: + self.reader_thread.onClose() + super(MainWindow, self).close() + + def delete_current_sale(self): + if self._sale['id']: + self._PosSale.cancel_sale(self._sale['id'], self._context) + + def resize_window_tablet_dev(self): + self.resize(690, self.get_geometry()[1]) + + def set_stack_messages(self): + super(MainWindow, self).set_stack_messages() + self.stack_msg.update({ + 'system_ready': ('info', self.tr('SYSTEM READY...')), + 'confirm_exit': ('warning', self.tr('DO YOU WANT TO EXIT?')), + 'confirm_credit': ('question', self.tr('PLEASE CONFIRM YOUR PAYMENT TERM AS CREDIT?')), + 'sale_number_not_found': ('warning', self.tr('SALE ORDER / INVOICE NUMBER NOT FOUND!')), + 'sale_closed': ('error', self.tr('THIS SALE IS CLOSED, YOU CAN NOT TO MODIFY!')), + 'discount_not_valid': ('warning', self.tr('DISCOUNT VALUE IS NOT VALID!')), + 'add_payment_sale_draft': ('info', self.tr('YOU CAN NOT ADD PAYMENTS TO SALE ON DRAFT STATE!')), + 'enter_quantity': ('question', self.tr('ENTER QUANTITY...')), + 'enter_discount': ('question', self.tr('ENTER DISCOUNT...')), + 'enter_payment': ('question', self.tr('ENTER PAYMENT AMOUNT BY: %s')), + 'enter_new_price': ('question', self.tr('ENTER NEW PRICE...')), + 'order_successfully': ('info', self.tr('ORDER SUCCESUFULLY SENT.')), + 'order_failed': ('warning', self.tr('FAILED SEND ORDER!')), + 'missing_agent': ('warning', self.tr('MISSING AGENT!')), + 'missing_salesman': ('warning', + self.tr('THERE IS NOT SALESMAN FOR THE SALE!')), + 'sale_without_products': ('warning', self.tr('YOU CAN NOT CONFIRM A SALE WITHOUT PRODUCTS!')), + 'user_without_permission': ('error', self.tr('USER WITHOUT PERMISSION FOR SALE POS!')), + 'quantity_not_valid': ('error', self.tr('THE QUANTITY IS NOT VALID...!')), + 'user_not_permissions_device': ('error', self.tr('THE USER HAVE NOT PERMISSIONS FOR ACCESS' \ + ' TO DEVICE!')), + 'missing_party_configuration': ('warning', + self.tr('MISSING THE DEFAULT PARTY ON SHOP CONFIGURATION!')), + 'missing_journal_device': ('error', self.tr('MISSING SET THE JOURNAL ON DEVICE!')), + 'statement_closed': ('error', self.tr('THERE IS NOT A STATEMENT OPEN FOR THIS DEVICE!')), + 'product_not_found': ('warning', self.tr('PRODUCT NOT FOUND!')), + 'must_load_or_create_sale': ('warning', self.tr('FIRST YOU MUST CREATE/LOAD A SALE!')), + 'new_sale': ('warning', self.tr('DO YOU WANT CREATE NEW SALE?')), + 'cancel_sale': ('question', self.tr('ARE YOU WANT TO CANCEL SALE?')), + 'not_permission_delete_sale': ('info', self.tr('YOU HAVE NOT PERMISSIONS FOR DELETE THIS SALE!')), + 'not_permission_for_cancel': ('info', self.tr('YOU HAVE NOT PERMISSIONS FOR CANCEL THIS SALE!')), + 'customer_not_credit': ('info', self.tr('THE CUSTOMER HAS NOT CREDIT!')), + 'agent_not_found': ('warning', self.tr('AGENT NOT FOUND!')), + 'invalid_commission': ('warning', self.tr('COMMISSION NOT VALID!')), + 'credit_limit_exceed': ('info', self.tr('CREDIT LIMIT FOR CUSTOMER EXCEED!')), + 'credit_limit_capacity': ('info', self.tr('THE CUSTOMER CREDIT CAPACITY IS ABOVE 80%')), + 'not_can_force_assign': ('warning', self.tr('YOU CAN NOT FORCE ASSIGN!')), + 'send_electronic_failed': ('info', self.tr('FALLO EL ENVIO DE FACTURA!')), + 'invoice_done_failed': ('info', self.tr('FALLO FINALIZACIÓN DE FACTURA!')), + }) + + def load_modules(self): + modules = Modules(self, self.conn) + self._sale_pos_restaurant = None + self.Module = FastModel('ir.module', self.ctx) + self.Config = FastModel('sale.configuration', self.ctx) + self._config, = self.Config.find([('id', '=', 1)]) + + self.discount_method = self._config.get('discount_pos_method') + + self._commission_activated = self.Module.find([ + ('name', '=', 'commission'), + ('state', '=', 'activated'), + ]) + self._credit_limit_activated = self.Module.find([ + ('name', '=', 'account_credit_limit'), + ('state', '=', 'activated'), + ]) + + _product = { + 'name': 'product.product', + 'fields': [ + 'template.name', 'code', 'barcode', 'write_date', + 'description', 'template.sale_price_w_tax', + 'template.account_category' + ] + } + self.cache_local = self._config.get('cache_products_local') + + if self._config['show_location_pos']: + _product['fields'].append('location_') + + if self._config['show_stock_pos'] in ('value', 'icon'): + if self._config['show_stock_pos'] == 'value': + _product['fields'].append('quantity') + if self._config['show_brand']: + _product['fields'].append('brand.name') + + if self._config['encoded_sale_price']: + _product['fields'].extend(['image', 'image_icon', 'encoded_sale_price']) + + _PosSale = { + 'name': '_PosSale', + 'model': 'sale.sale', + 'fields': ['number', 'party', 'party.name', 'salesman', 'lines', + 'position', 'total_amount_cache', 'salesman.party.name', + 'payment_term', 'payment_term.name', 'invoices', + 'payments', 'untaxed_amount', 'state', 'tax_amount', + 'total_amount', 'residual_amount', 'paid_amount', 'invoice', + 'invoice.state', 'invoice_number', 'invoice.number', 'invoices', + 'delivery_charge', 'sale_date', 'invoice_type'], + 'methods': ( + 'get_printing_context', 'cancel_sale', 'get_amounts', + 'get_discount_total', 'process_sale', 'reconcile_invoice', + 'post_invoice', 'get_data', 'add_value', 'faster_add_product', + 'get_product_prices', 'add_payment', 'get_order2print', + 'get_sale_from_invoice', 'add_tax', 'check_state', 'to_quote', + 'to_draft', 'new_sale', 'on_change', 'get_salesman_in_party', + ) + } + + _Agent = { + 'name': '_Agent', + 'model': 'commission.agent', + 'fields': ('id', 'party.name', 'party.id_number', 'plan.percentage', + 'active'), + } + _Commission = { + 'name': '_Commission', + 'model': 'commission', + 'fields': ('id', 'origin', 'invoice_line', 'invoice_line.invoice'), + } + + _Tables = self._Tables = None + if self.enviroment == 'restaurant': + self._sale_pos_restaurant = self.Module.find([ + ('name', '=', 'sale_pos_frontend_rest'), + ('state', '=', 'activated'), + ]) + if self._sale_pos_restaurant: + _Tables = { + 'name': '_Tables', + 'model': 'sale.shop.table', + 'fields': ('name', 'shop', 'capacity', 'state') + } + _PosSale['fields'].extend(['table_assigned', + 'table_assigned.name', 'table_assigned.state']) + + if self._commission_activated: + _PosSale['fields'].extend(['agent', 'agent.party.name', 'commission']) + modules.set_models([_Agent, _Commission]) + + self.User = FastModel('res.user', self.ctx) + self._user, = self.User.find([('login', '=', self.user)]) + + if not self._user['sale_device']: + return 'user_not_permissions_device' + + self.ctx['user'] = self._user['id'] + + self.ModSale = FastModel('sale.sale', self.ctx) + self.ModSaleLine = FastModel('sale.line', self.ctx) + self.Product = FastModel('product.product', self.ctx) + self.Journal = FastModel('account.statement.journal', self.ctx) + self.Employee = FastModel('company.employee', self.ctx) + self.Device = FastModel('sale.device', self.ctx) + self.Category = FastModel('product.category', self.ctx) + self.PaymentTerm = FastModel('account.invoice.payment_term', self.ctx) + self.Party = FastModel('party.party', self.ctx) + self.Taxes = FastModel('account.tax', self.ctx) + self.ActionReport = FastModel('ir.action.report', self.ctx) + + models_to_work = [_PosSale] + + if _Tables: + models_to_work.append(_Tables) + + modules.set_models(models_to_work) + self.device, = self.Device.find([ + ('id', '=', self._user['sale_device']['id']), + ]) + + self.shop = self.device['shop'] + self.shop_taxes = self.shop['taxes'] + self.company = self.shop['company'] + self._journals = dict([(j['id'], j) for j in self.device['journals']]) + + self.employees = self.Employee.find([ + ('company', '=', self.company['id']), + ]) + self._payment_terms = self.PaymentTerm.get_payment_term_pos() + + self.type_pos_user = self._context.get('type_pos_user') + + if not self.type_pos_user: + return 'user_without_permission' + self.user_can_delete = self.type_pos_user in ('frontend_admin', 'cashier') + + self.product_categories = self.device['shop']['product_categories'] + self.salesman_required = self.device['shop']['salesman_pos_required'] + + self.default_party = self.shop['party'] + if not self.default_party: + return 'missing_party_configuration' + + self.default_journal = self.device['journal'] + if not self.default_journal: + return 'missing_journal_device' + + self.default_payment_term = self.shop['payment_term'] + self._password_admin = self._config.get('password_admin_pos') + + self._action_report, = self.ActionReport.find([ + ('report_name', '=', 'account.invoice'), + ]) + + if self._config['show_stock_pos'] in ('value', 'icon'): + self.stock_context = { + 'stock_date_end': date.today(), + 'locations': [self.shop['warehouse']['id']], + } + return True + + def create_dialogs(self): + self.create_dialog_position() + self.create_dialog_comment() + self.create_dialog_search_party() + self.create_dialog_payment() + self.create_dialog_salesman() + self.create_dialog_voucher() + self.create_dialog_print_invoice() + self.create_dialog_global_discount() + self.create_dialog_payment_term() + self.create_dialog_search_sales() + self.create_wizard_new_sale() + self.create_dialog_stock() + self.create_dialog_order() + self.create_dialog_force_assign() + self.create_dialog_taxes() + self.create_dialog_cancel_invoice() + self.create_dialog_sale_line() + if self._commission_activated: + self.create_dialog_agent() + if self.enviroment == 'restaurant' and self._sale_pos_restaurant: + self.create_dialog_manage_tables() + + def set_printing_context(self): + # Printing invoice context + if self.printer_sale_name: + if ">" in self.printer_sale_name: + self.printer_sale_name = str(str("\\") + self.printer_sale_name.replace('>', str('\\'))) + + ctx_printing = self._PosSale.get_printing_context( + [self.device['id']], self.user, self._context) + ctx_printing['row_characters'] = self.row_characters + ctx_printing['delta_locale'] = DELTA_LOCALE + + self.receipt_sale = Receipt(ctx_printing) + + # Printing order context + if self.print_order: + self.receipt_order = Receipt(ctx_printing) + self.set_default_printer() + + def set_default_printer(self, printer=None): + if self.active_usb_printers: + self.printer_sale_name = self.active_usb_printers[0] + if not printer and self.printer_sale_name: + printer = { + 'interface': self.printer_sale_name[0], + 'device': self.printer_sale_name[1], + } + if printer: + self.receipt_sale.set_printer(printer) + + def button_new_sale_pressed(self): + self.createNewSale() + + def button_send_to_pay_pressed(self): + # Return sale to draft state + self._PosSale.to_quote(self._sale['id'], self._context) + if self.model_sale_lines.rowCount() > 0: + if self.check_salesman(): + self.state_disabled() + + def button_to_draft_pressed(self): + # Return sale to draft state + if hasattr(self, '_sale'): + self._PosSale.to_draft(self._sale['id'], self._context) + self.state_disabled() + + def create_gui(self): + panels = QHBoxLayout() + panel_left = QVBoxLayout() + panel_right = QVBoxLayout() + + left_head = QHBoxLayout() + left_table = None + left_bottom = QHBoxLayout() + + self.message_bar = MessageBar() + self.label_input = QLabel() + self.label_input.setFocus() + self.label_input.setObjectName('label_input') + + if self.enviroment == 'restaurant': + values = self.get_product_by_categories() + menu_dash = MenuDash(self, values, 'on_selected_item') + + if not self.tablet_mode: + _label_invoice = QLabel(self.tr('INVOICE:')) + _label_invoice.setObjectName('label_invoice') + _label_invoice.setAlignment(alignRight | alignVCenter) + + self.field_invoice = QLineEdit() + self.field_invoice.setReadOnly(True) + self.field_invoice.setObjectName('field_invoice') + if self.tablet_mode: + self.field_invoice.setPlaceholderText(self.tr('INVOICE')) + + self.field_amount = FieldMoney(self, 'amount', {}) + self.field_amount.setObjectName('field_amount') + self.field_sign = QLabel(' ') + self.field_sign.setObjectName('field_sign') + + layout_message = QGridLayout() + layout_message.addLayout(self.message_bar, 1, 0, 1, 4) + + if not self.tablet_mode: + layout_message.addWidget(self.label_input, 2, 0, 1, 2) + layout_message.addWidget(_label_invoice, 2, 2) + layout_message.addWidget(self.field_invoice, 2, 3) + else: + layout_message.addWidget(self.label_input, 2, 0, 1, 2) + layout_message.addWidget(self.field_invoice, 2, 3) + + left_head.addLayout(layout_message, 0) + left_head.addWidget(self.field_sign, 0) + left_head.addWidget(self.field_amount, 0) + + info_fields = [ + ('party', { + 'name': self.tr('CUSTOMER'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('date', { + 'name': self.tr('DATE'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('salesman', { + 'name': self.tr('SALESMAN'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('payment_term', { + 'name': self.tr('PAYMENT TERM'), + 'readonly': True, + 'invisible': self.tablet_mode, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + }), + ('order_number', { + 'name': self.tr('No ORDER'), + 'placeholder': False, + 'readonly': True, + 'size': self.screen_size, + 'color': 'gray' + }), + ('invoice_type', { + 'name': self.tr('INVOICE TYPE'), + 'placeholder': False, + 'type': 'selection', + 'on_change': 'action_invoice_type_selection_changed', + 'values': [ + ('', ''), + ('C', self.tr('COMPUTADOR')), + ('M', self.tr('MANUAL')), + ('P', self.tr('POS')), + ('1', self.tr('VENTA ELECTRONICA')), + ('2', self.tr('VENTA DE EXPORTACION')), + ('3', self.tr('FACTURA POR CONTINGENCIA FACTURADOR')), + ('4', self.tr('FACTURA POR CONTINGENCIA DIAN')), + ('91', self.tr('NOTA CREDITO ELECTRONICA')), + ('92', self.tr('NOTA DEBITO ELECTRONICA')), + ], + 'size': self.screen_size, + 'color': 'gray' + }), + ] + self.field_invoice_type = None + + if self.tablet_mode or self._config['show_position_pos']: + info_fields.append(('position', { + 'name': self.tr('POSITION'), + 'readonly': True, + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + })) + if self._commission_activated and not self.tablet_mode \ + and self._config['show_agent_pos']: + info_fields.append(('agent', { + 'name': self.tr('AGENT'), + 'placeholder': self.tablet_mode, + 'readonly': True, + 'size': self.screen_size, + 'color': 'gray' + })) + + _cols = 2 + if self.enviroment == 'restaurant': + _cols = 1 + + self.field_delivery_charge = None + if self._config['show_delivery_charge']: + info_fields.append( + ('delivery_charge', { + 'name': self.tr('DELIVERY CHARGE'), + 'placeholder': False, + 'type': 'selection', + 'on_change': 'action_delivery_charge_selection_changed', + 'values': [ + ('', ''), + ('customer', self.tr('CUSTOMER')), + ('company', self.tr('COMPANY')), + ], + 'size': self.screen_size, + 'color': 'gray' + })) + + self.field_table_assigned = None + if self.enviroment == 'restaurant' and self._sale_pos_restaurant: + info_fields.append( + ('table_assigned', { + 'name': self.tr('ASSIGNED TABLE'), + 'placeholder': False, + 'size': self.screen_size, + 'color': 'gray' + })) + + self.grid_info = GridForm(self, OrderedDict(info_fields), col=_cols) + + col_sizes_tlines = [field['width'] for field in self.fields_sale_line] + left_table = TableView('model_sale_lines', self.model_sale_lines, + col_sizes_tlines, method_selected_row=self.sale_line_selected) + self.table_sale_lines = left_table + + for i, f in enumerate(self.model_sale_lines._fields, 0): + if f.get('invisible'): + self.table_sale_lines.hideColumn(i) + + _fields_amounts = [ + ('untaxed_amount', { + 'name': self.tr('SUBTOTAL'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + + }), + ('taxes_amount', { + 'name': self.tr('TAXES'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('discount', { + 'name': self.tr('DISCOUNT'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('total_amount', { + 'name': self.tr('TOTAL'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'blue' + }), + ('paid', { + 'name': self.tr('PAID'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'gray' + }), + ('pending', { + 'name': self.tr('PENDIENTE'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'blue' + }), + ('change', { + 'name': self.tr('CHANGE'), + 'readonly': True, + 'type': 'money', + 'size': self.screen_size, + 'color': 'orange' + }) + ] + + fields_amounts = OrderedDict(_fields_amounts) + self.grid_amounts = GridForm(self, fields_amounts, col=1) + + self.buttonpad = Buttonpad(self) + self.pixmap_pos = Image(self, 'pixmap_pos', FILE_BANNER) + self.table_payment = TableView('table_payment', self.table_payment_lines, + [250, STRETCH]) + + panel_left.addLayout(left_head, 0) + panel_left.addWidget(left_table, 1) + panel_right.addWidget(self.pixmap_pos, 0) + + left_bottom.addLayout(self.grid_info, 1) + if self.enviroment == 'restaurant': + panel_left.addLayout(self.buttonpad.functions, 1) + left_bottom.addLayout(self.grid_amounts, 0) + panel_right.addLayout(menu_dash, 1) + panel_right.addLayout(self.buttonpad.stacked, 0) + else: + panel_right.addLayout(self.grid_amounts, 1) + panel_right.addLayout(self.buttonpad.functions, 1) + panel_right.addLayout(self.buttonpad.stacked, 0) + panel_right.addWidget(self.table_payment) + + panel_left.addLayout(left_bottom, 0) + + panels.addLayout(panel_left, 1) + panels.addLayout(panel_right, 0) + + widget = QWidget() + widget.setLayout(panels) + self.setCentralWidget(widget) + + def create_statusbar(self): + values = OrderedDict([ + ('stb_shop', {'name': self.tr('SHOP'), 'value': self.shop['name']}), + ('stb_device', {'name': self.tr('DEVICE'), 'value': self.device['name']}), + ('stb_database', {'name': self.tr('DATABASE'), 'value': self.database}), + ('stb_user', {'name': self.tr('USER'), 'value': self.user}), + ('stb_printer', {'name': self.tr('PRINTER'), 'value': self.printer_sale_name}) + ]) + self.set_statusbar(values) + + def button_plus_pressed(self): + error = False + if self._input_text == '' and self._amount_text == '0': + return + if self._state in ('paid', 'disabled'): + return + if self._sign in ('*', '-', '/'): + if hasattr(self, '_sale_line') and self._sale_line \ + and self._sale_line.get('type') and self._state == 'add' \ + and self.model_sale_lines.rowCount() > 0: + if self._sign == '*': + self._process_quantity(self._amount_text) + else: + error = not(self._process_price(self._amount_text)) + elif self._state in ['add', 'cancel', 'accept']: + self.clear_right_panel() + self.add_product(code=self._input_text) + elif self._state == 'cash': + is_paid = self._process_pay(self.field_amount.text()) + if not is_paid: + self.clear_input_text() + self.clear_amount_text() + return + else: + logging.warning('Unknown command/text') + self._clear_context(error) + + def action_read_weight(self): + self.reader_thread.start() + + def set_weight_readed(self): + if not self.reader_thread or not self.reader_thread.best_weight: + return + + if self.reader_thread.best_weight: + self.amount_text_changed(self.reader_thread.best_weight) + self._process_quantity(self._amount_text) + self._clear_context(False) + self.reader_thread.fZERO() + + def _clear_context(self, error=False): + self.clear_input_text() + self.clear_amount_text() + self.clear_sign() + + if self._state not in ('warning', 'cash') and not error: + self.message_bar.set('system_ready') + else: + self.set_state('add') + + def _process_quantity(self, text): + eval_value = text.replace(',', '.') + rec = {} + try: + quantity = Decimal(eval_value) + _product = self._sale_line['product'] + if _product and _product.get('quantity'): + if not self._check_stock_quantity(_product, quantity): + return + + if self._current_line_id: + rec = self.ModSaleLine.faster_set_quantity({ + 'id': self._current_line_id, + 'quantity': float(quantity) + }) + except: + return self.message_bar.set('quantity_not_valid') + + self.message_bar.set('system_ready') + self.model_sale_lines.update_record(rec) + self.update_total_amount() + + def _process_price(self, text): + discount_valid = True + eval_value = text.replace(',', '') + + value = float(eval_value) + if self._sign == '-': + if self.discount_method == 'percentage' and value > 90: + discount_valid = False + # Do discount + else: + discount_valid = self.set_discount(eval_value) + elif self._sign == '/': + if value <= 0: + return + sale_line, = self.ModSaleLine.find([ + ('id', '=', self._current_line_id) + ]) + price_w_tax = sale_line['unit_price_w_tax'] + if price_w_tax <= Decimal(value): + # Change unit price + discount_valid = self.set_unit_price(value) + else: + eval_value = (1 - (value / price_w_tax)) * 100 + if self.discount_method == 'fixed': + eval_value = price_w_tax - value + discount_valid = self.set_discount(eval_value) + if not discount_valid: + self.message_bar.set('discount_not_valid') + return False + + self.message_bar.set('system_ready') + self.update_total_amount() + return True + + def _process_pay(self, text): + if not self.validate_done_sale(): + return + val = Decimal(text.replace(',', '')) + + if self._commission_activated: + if self._journals[self.field_journal_id]['kind'] == 'payment': + agent_id = None + if self.field_agent_id: + agent_id = self.field_agent_id + else: + agent_id = self.field_agent_ask.get_id() + + if not agent_id: + self.message_bar.set('missing_agent') + return False + + cash_received = Decimal(val) + self.set_amounts() + residual_amount = self._sale['residual_amount'] + if residual_amount < 0: + # The sale is paid + self._done_sale() + return True + + change = cash_received - residual_amount + + if residual_amount >= cash_received: + amount_to_add = cash_received + else: + amount_to_add = residual_amount + + all_money = cash_received + self._sale['paid_amount'] + # self.field_change.setText(change) + if change < ZERO: + self.field_pending.setText(abs(change)) + else: + self.field_pending.setText(str(ZERO)) + + self.set_amount_received(all_money) + res = self.add_payment(amount_to_add, all_money, change) + if res['residual_amount'] < 0: + # The sale is paid + self._done_sale() + return True + + self.field_journal_id = self.default_journal['id'] + + if res['msg'] == 'missing_money': + self.message_bar.set('enter_payment', self.default_journal['name']) + return False + + if change < ZERO: + self.message_bar.set('enter_payment', self.default_journal['name']) + + self.field_change.setText(change) + # self.set_amount_received(all_money) + + self._sale.update({ + 'residual_amount': res['residual_amount'] + }) + residual_amount = self._sale['residual_amount'] + + if self._sale['residual_amount'] <= 0: + # The sale is paid + self._done_sale() + return True + + def validate_done_sale(self): + if self.model_sale_lines.rowCount() == 0: + self.dialog('sale_without_products') + self.set_state('add') + self.message_bar.set('system_ready') + return + return True + + def _get_total_amount(self): + return self.model_sale_lines.get_sum('amount_w_tax') + + def set_discount_amount(self): + res = 0 + if self._sale['id']: + res = self.ModSale.get_discount_total({ + 'sale_id': self._sale['id'] + }) + self.field_discount.setText(res) + + def amount_text_changed(self, text=None): + if text: + self._amount_text += text + self.field_amount.setText(self._amount_text) + + def input_text_changed(self, text=None): + if text: + self._input_text += text + elif text == '': + self._input_text = '' + self.label_input.setText(self._input_text) + + def __do_invoice_thread(self): + try: + res = self.ModSale.faster_post_invoice({ + 'sale_id': self.sale_to_post['id'] + }) + except: + self.dialog('invoice_done_failed') + return + if not res['result']: + self.dialog('send_electronic_failed') + return + if self.sale_to_post['is_credit']: + return + + self.ModSale.reconcile_invoice({ + 'sale_id': self.sale_to_post['id'] + }) + + def _done_sale(self, is_credit=False): + self._sale['is_credit'] = is_credit + self.sale_to_post = self._sale + self.do_invoice.start() + if self._Tables and self._sale.get('table_assigned'): + self._Tables.write([self._sale['table_assigned']], + {'state': 'available'} + ) + + try: + if self.print_receipt == 'automatic': + _copies = self.device['shop']['invoice_copies'] + if not is_credit: + self.print_invoice(copies=_copies) + if self.print_order and self.print_auto_order: + self.action_print_order() + except: + logging.error(sys.exc_info()[0]) + + if not is_credit and self._commission_activated: + agent_id = self.field_agent_ask.get_id() + if self.auto_print_commission and agent_id: + self.print_equivalent_invoice(self._sale['id']) + + if self.type_pos_user not in ('cashier', 'order') \ + and self._config.get('new_sale_automatic'): + self.createNewSale() + else: + self.state_disabled() + return True + + def print_invoice(self, sale_id=None, copies=1): + if not sale_id: + sale_id = self._sale['id'] + data = self._PosSale.get_data(sale_id, self._context) + for i in range(copies): + self.receipt_sale.print_sale(data) + + def button_accept_pressed(self): + if not self._sale['id'] or not self.model_sale_lines.rowCount() > 0: + return + self.set_state('accept') + + def button_cash_pressed(self): + if not self.check_salesman(): + return + if not self.validate_payment_term(): + return + sale_id = self._sale['id'] + res, msg = self.ModSale.faster_process({'sale_id': sale_id}) + # Remove deprecation res['res'] + if msg: + # msg = Mesagge error + self.message_bar.set(msg) + return + + self.set_amounts(res) + + self.field_invoice.setText(res['invoice_number']) + self.field_amount.zero() + if self.type_pos_user == 'salesman': + self.print_invoice(sale_id) + res = self._print_order(sale_id, 'delivery') + self.createNewSale() + return + + self.message_bar.set('enter_payment', self.default_journal['name']) + self.set_state('cash') + self.buttonpad.setFocus() + + def action_reservations(self): + logging.info('Buscando reservas.....') + + def action_tables(self): + self.dialog_manage_tables.exec_() + + def action_table_assigned(self, id, name, prev_state, new_state): + table_assigned = id + + if self._sale.get('table_assigned') != id and prev_state == 'occupied': + return False + + if self._sale.get('table_assigned') == id and new_state == 'available': + name = '' + table_assigned = None + + self._sale['table_assigned'] = table_assigned + self._sale['table_assigned.state'] = new_state + + self._Tables.write([id], {'state': new_state}) + + self._PosSale.write([self._sale['id']], {'table_assigned': table_assigned}) + self.field_table_assigned.setText(name) + return True + + def action_tip(self): + if self._config['tip_product'] and self._config['tip_rate']: + total_amount = int(self._get_total_amount()) + self.add_product(code=self._config['tip_product']['code']) + self.button_plus_pressed() + eval_value = int((self._config['tip_rate'] / 100) * total_amount) + self.ModSaleLine.write( + [self._current_line_id], {'unit_price': Decimal(eval_value)} + ) + self._PosSale.write([self._sale['id']], {'tip': Decimal(eval_value)}) + self.update_total_amount() + + def action_salesman(self): + self.dialog_salesman.exec_() + + def action_tax(self): + self.dialog_tax.exec_() + + def action_payment(self): + if self._state != 'cash': + self.dialog('add_payment_sale_draft') + return + self.dialog_payment.exec_() + + def _check_credit_capacity(self, party): + if party['credit_limit_amount']: + if (party['credit_limit_amount'] * Decimal(RATE_CREDIT_LIMIT)) < (party['credit_amount'] + self._get_total_amount()): + self.dialog('credit_limit_capacity') + return True + + def validate_payment_term(self): + is_credit = self._payment_terms[str(self.field_payment_term_id)]['is_credit'] + party, = self.Party.find([('id', '=', self.party_id)]) + if is_credit: + if self.party_id == self.default_party['id']: + self.dialog('customer_not_credit') + return False + if self._credit_limit_activated: + if is_credit and not party['credit_limit_amount']: + self.dialog('customer_not_credit') + return False + + self._credit_amount = self.ModSale.get_credit_amount_party({'party_id': self.party_id}) + self._credit_limit_amount = party['credit_limit_amount'] + + if is_credit and self._credit_limit_amount and \ + self._credit_limit_amount < (self._credit_amount + self._get_total_amount()): + self.dialog('credit_limit_exceed') + return False + return True + + def action_payment_term_selection_changed(self): + is_credit = self._payment_terms[str(self.field_payment_term_id)]['is_credit'] + self._PosSale.write([self._sale['id']], {'payment_term': self.field_payment_term_id}) + if is_credit and self.type_pos_user != 'salesman': + if self.validate_credit_limit(): + self._done_sale(is_credit=True) + + def validate_credit_limit(self): + if self._credit_limit_amount and self._credit_limit_amount < (self._credit_amount + self._get_total_amount()): + self.dialog('credit_limit_exceed') + return False + else: + return True + + def action_journal_selection_changed(self): + self.message_bar.set('enter_payment', self.field_journal_name) + + def action_salesman_selection_changed(self): + self._PosSale.write([self._sale['id']], {'salesman': self.field_salesman_id}) + + def action_delivery_charge_selection_changed(self, index): + val = self.field_delivery_charge.get_id() + if val: + self._PosSale.write([self._sale['id']], {'delivery_charge': val}) + + def action_invoice_type_selection_changed(self, index): + val = self.field_invoice_type.get_id() + if val: + self._PosSale.write([self._sale['id']], {'invoice_type': val}) + + def action_tax_selection_changed(self): + res = self._PosSale.add_tax(self._sale['id'], self.field_tax_id, self._context) + self._sale.update(res) + self.set_amounts() + + def action_print_sale(self): + number = self.field_invoice.text() + if not number: + number = self.field_order_number.text() + if number: + self.field_invoice_number_ask.setText(number) + res = self.dialog_print_invoice.exec_() + if res == DIALOG_REPLY_NO: + return + number = self.field_invoice_number_ask.text() + printer_id = self.field_printer_ask.get_id() + type_doc = self.field_type_ask.get_id() + sale = {} + if number: + if type_doc == 'order': + sales = self.ModSale.find([('number', '=', number)]) + if sales: + sale = sales[0] + else: + sale = self._PosSale.get_sale_from_invoice([1], number, self._context) + + if not sale: + return self.message_bar.set('sale_number_not_found') + sale_id = sale['id'] + else: + sale_id = self._sale['id'] + + if printer_id == '1': + self.print_invoice(sale_id) + else: + self.print_odt_invoice(sale) + + def print_odt_invoice(self, sale): + if not sale.get('invoices'): + return + invoice_id = sale['invoices'][0] + model = u'account.invoice' + data = { + 'model': model, + 'action_id': self._action_report['id'], + 'id': invoice_id, + 'ids': [invoice_id], + } + ctx = {'date_format': u'%d/%m/%Y'} + ctx.update(self._context) + Action.exec_report(self.conn, u'account.invoice', + data, direct_print=True, context=ctx) + + def print_equivalent_invoice(self, sale_id): + sale, = self.ModSale.find([('id', '=', sale_id)]) + + # if not sale['invoices']: + # return + # + # invoice_id = sale['invoices'][0] + # commissions = self._Commission.find([ + # ('origin', '=', 'account.invoice,' + str(invoice_id)) + # ]) + # + # # if not commissions: + # # return + # # commission = commissions[0] + # # if not commission['invoice_line.invoice']: + # # return + # # + # # model = u'account.invoice' + # # data = { + # # 'model': model, + # # 'action_id': self._action_report_equivalent['id'], + # # 'id': commission['invoice_line.invoice'], + # # 'ids': [commission['invoice_line.invoice']], + # # } + # # ctx = {'date_format': u'%d/%m/%Y'} + # # ctx.update(self._context) + # # Action.exec_report(self.conn, u'account.invoice', + # # data, direct_print=True, context=ctx) + + def action_comment(self): + self.dialog_comment.exec_() + comment = self.field_comment_ask.text() + if comment: + self._PosSale.write([self._sale['id']], {'comment': comment}) + + def action_position(self): + self.dialog_position.exec_() + position = self.field_position_ask.text() + if hasattr(self, 'field_position') and position: + self.field_position.setText(position) + self._PosSale.write([self._sale['id']], {'position': position}) + + def action_agent(self): + self.dialog_agent.exec_() + res = self.field_commission_ask.text() + if not res: + return + commission = float(res) + sale, = self.ModSale.find([ + ('id', '=', self._sale['id']), + ]) + self.field_agent_id = self.field_agent_ask.get_id() + + if self.field_agent_id and commission: + agent, = self._Agent.find([ + ('id', '=', self.field_agent_id), + ]) + if commission <= agent['plan.percentage']: + self._PosSale.write([self._sale['id']], { + 'agent': self.field_agent_id, + 'commission': int(commission), + }) + else: + self.message_bar.set('invalid_commission') + return + self.message_bar.set('system_ready') + comm_string = str('[' + str(commission) + ']' + ' ') + (str(self.field_agent_ask.text())) + self.field_agent.setText(comm_string) + self._set_commission_amount(sale['untaxed_amount'], commission) + + def _set_commission_amount(self, untaxed_amount, commission): + untaxed_amount = int(untaxed_amount) + commission = int(commission) + total = ((untaxed_amount * commission) / 100) + self.field_commission_amount.setText(str(total)) + + def action_party(self): + self.dialog_search_parties.clear_rows() + self.dialog_search_parties.execute() + + def action_global_discount(self, sale_id=None): + self.dialog_global_discount.exec_() + discount = self.field_global_discount_ask.text() + if discount and discount.isdigit(): + if self.model_sale_lines.rowCount() > 0: + lines = [line['id'] for line in self.model_sale_lines._data] + self.set_discount(int(discount), lines) + + def _print_order(self, sale_id, kind, reversion=False): + result = False + try: + orders = self._PosSale.get_order2print(sale_id, reversion, False, self._context) + result = self.receipt_order.print_orders(orders, reversion, kind) + except: + logging.error('Printing order fail!') + return result + + def action_print_order(self, sale_id=None, reversion=False): + res = self.dialog_order.exec_() + if res == DIALOG_REPLY_NO: + return + + kind = 'delivery' + if self.enviroment == 'restaurant': + kind = 'command' + if not self.print_order: + return + if not sale_id and self._sale['id']: + sale_id = self._sale['id'] + + result = self._print_order(sale_id, kind) + # try: + # orders = self._PosSale.get_order2print(sale_id, reversion, + # False, self._context) + # result = self.receipt_order.print_orders(orders, reversion, kind) + # print(result, orders) + # except: + # print('Error: printing order fail!') + if result: + # Show dialog if send sale order was successful + self.dialog('order_successfully') + else: + self.dialog('order_failed') + + def action_payment_term(self): + if self._state == 'cash' or self.type_pos_user == 'salesman': + self.dialog_payment_term.exec_() + + def action_new_sale(self): + if not self._sale['id']: + return + if self._ask_new_sale(): + self.createNewSale() + if self.enviroment == 'restaurant': + self.wizard_new_sale() + + def wizard_new_sale(self): + # self.action_position() + # self.action_salesman() + pass + + def numpad_price_clicked(self): + code = self.label_input.text() + product = self._search_product(code) + if not product: + return + + def _ask_new_sale(self): + dialog = self.dialog('new_sale', response=True) + res = dialog.exec_() + if res == DIALOG_REPLY_NO: + return False + return True + + def action_cancel(self): + if self.type_pos_user == 'cashier': + self.dialog_cancel_invoice.exec_() + password = self.field_password_for_cancel_ask.text() + if password != self._password_admin: + return self.dialog('not_permission_for_cancel') + + if not self._sale['id']: + return + if self._state == 'cash' and not self.user_can_delete: + return self.dialog('not_permission_delete_sale') + if self.type_pos_user in ('order', 'salesman') and \ + self._sale.get('invoice_number'): + return self.dialog('not_permission_delete_sale') + dialog = self.dialog('cancel_sale', response=True) + response = dialog.exec_() + if response == DIALOG_REPLY_NO: + return + self._PosSale.cancel_sale(self._sale['id'], self._context) + self.field_password_for_cancel_ask.setText('') + self.set_state('cancel') + self.clear_right_panel() + self.createNewSale() + + def action_search_product(self): + if self._state == 'cash': + return + self.dialog_search_products.show() + + def action_search_sale(self): + delta = str(datetime.now() - timedelta(4)) + if self.type_pos_user == 'cashier': + dom = ['OR', [ + ('create_date', '>=', delta), + ('state', 'in', ['quotation', 'confirmed']), + ], [ + ('state', '=', 'processing'), + ('invoice.state', '=', 'draft'), + ('invoice.type', '=', 'out'), + ]] + elif self.type_pos_user in ('order', 'salesman'): + dom = [ + ('state', '=', 'draft'), + ('create_date', '>=', delta), + ] + else: + dom = [ + ('state', 'in', ['draft', 'quotation', 'confirmed']), + ('create_date', '>=', delta), + ] + + sales = self.ModSale.find(dom, order=[('id', 'DESC')]) + self.dialog_search_sales.set_from_values(sales) + + if self.enviroment == 'retail': + dom_draft = [ + ('create_date', '>=', delta), + ('state', '=', 'draft'), + ('invoice_number', '!=', None), + ] + sales_draft = self.ModSale.find(dom_draft) + self.dialog_search_sales.set_counter_control(sales_draft) + response = self.dialog_search_sales.execute() + if response == DIALOG_REPLY_NO: + return + + def on_selected_sale(self): + sale_id = self.dialog_search_sales.get_id() + if not sale_id: + return + self.load_sale(sale_id) + self.setFocus() + self.label_input.setFocus() + self.grabKeyboard() + + def on_selected_party(self): + party_id = self.dialog_search_parties.get_id() + if not party_id: + return + + party, = self.Party.find([('id', '=', party_id)]) + values = { + 'party': party_id, + 'invoice_address': party['addresses'][0]['id'], + 'shipment_address': party['addresses'][0]['id'], + } + + payment_term = party.get('customer_payment_term') + if payment_term: + values['payment_term'] = payment_term['id'] + self.field_payment_term.setText(str(payment_term['name'])) + self.field_payment_term_id = payment_term['id'] + else: + values['payment_term'] = self.default_payment_term['id'] + self.field_payment_term_id = self.default_payment_term['id'] + self.field_payment_term.setText(self.default_payment_term['name']) + + self._PosSale.write([self._sale['id']], values) + + self.party_id = party_id + self.field_party.setText(party['name']) + + if party.get('salesman'): + self.field_salesman_id = party['salesman']['id'] + self._PosSale.write([self._sale['id']], {'salesman': self.field_salesman_id}) + self.field_salesman.setText(party['salesman']['name']) + + # if self._credit_limit_activated: + # self._check_credit_capacity(party) + + self.message_bar.set('system_ready') + self.setFocus() + self.label_input.setFocus() + self.grabKeyboard() + + def load_sale(self, sale_id): + # loads only draft sales + self.is_clear_right_panel = True + self.clear_data() + self.clear_left_panel() + self.clear_right_panel() + sale, = self.ModSale.find([('id', '=', sale_id)]) + self._sale.update(sale) + self.table_payment_lines.reset() + self.field_order_number.setText(sale['number'] or '') + self._set_sale_date() + if hasattr(self, 'field_position'): + self.field_position.setText(sale['position'] or '') + if sale.get('payment_term'): + self.field_payment_term_id = sale['payment_term']['id'] + self.field_payment_term.setText(sale['payment_term']['name'] or '') + if sale.get('salesman'): + self.field_salesman.setText(sale['salesman']['name'] or '') + self.field_salesman_id = sale['salesman']['id'] + + res = self.ModSale.get_invoice_type({'sale_id': sale_id}) + if res: + self.field_invoice_type.set_from_id(res['invoice_type']) + + if sale.get('invoice_number'): + self.field_invoice.setText(sale['invoice_number'] or '') + else: + self.field_invoice.setText('') + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(True) + if self._sale.get('delivery_charge'): + self.field_delivery_charge.set_from_id(self._sale['delivery_charge']) + if sale.get('table_assigned'): + self.field_table_assigned.setText(sale['table_assigned.name'] or '') + + self.field_change.zero() + if self._commission_activated: + if hasattr(self, 'field_agent') and sale.get('agent') \ + and sale.get('commission'): + commission = sale.get('commission') + self.field_agent.setText('[' + str(int(commission)) + ']' + ' ' + sale['agent']['name']) + self.field_agent_id = sale['agent']['id'] + self.field_agent_ask.setText(sale['agent']['name']) + self.field_commission_ask.setText(str(commission)) + self._set_commission_amount(sale['untaxed_amount'], commission) + self.line_ids = [l['id'] for l in sale.get('lines')] + if sale.get('lines'): + lines = self.ModSaleLine.find([ + ('id', 'in', self.line_ids), + ]) + sale['lines'] = lines + for line in lines: + self.add_sale_line(line) + + if sale.get('payments'): + for payment in sale['payments']: + self.table_payment_lines.record_add(payment) + + self.set_state('add') + self.party_id = sale['party']['id'] + self.field_party.setText(sale['party']['name']) + self.set_amounts(sale) + self.set_amount_received() + self.field_amount.zero() + if self.type_pos_user in ('cashier', 'order', 'salesman'): + self.table_sale_lines.setEnabled(True) + + def set_change_amount(self): + amount_paid = self.table_payment_lines.get_sum('amount') + res = amount_paid - self._get_total_amount() + self.field_change.setText(res) + + def set_amount_received(self, cash_received=ZERO): + residual_amount = self._sale['residual_amount'] + if cash_received: + amount = cash_received + else: + amount = self._sale['paid_amount'] + self.field_pending.setText(residual_amount) + self.field_paid.setText(amount) + + def update_total_amount(self): + self.set_amounts() + + def set_amounts(self, res=None): + if not res: + res = self._PosSale.get_amounts([self._sale['id']], self._context) + + self._sale.update(res) + self.field_untaxed_amount.setText(res['untaxed_amount']) + self.field_taxes_amount.setText(res['tax_amount']) + self.field_total_amount.setText(res['total_amount']) + self.set_discount_amount() + + def _get_products_by_category(self, cat_id): + records = self.Product.find([ + ('code', '!=', None), + ('template.salable', '=', True), + ('template.categories', '=', cat_id), + ]) + return [ + [r['id'], r['code'], r['name'], r['template']['sale_price_w_tax']] + for r in records + ] + + def get_product_by_categories(self): + self.allow_categories = [pc for pc in self.product_categories if ( + not pc['parent'] and not pc['accounting'])] + + for cat in self.allow_categories: + cat['icon'] = get_icon(cat['name_icon']) + cat['items'] = self._get_products_by_category(cat['id']) + return self.allow_categories + + def _get_childs(self, parent_cat): + res = {} + for cat_id in parent_cat['childs']: + sub_categories = self.Category.find([ + ('parent', '=', parent_cat['id']) + ]) + + for sub_cat in sub_categories: + res.update(self._get_childs(sub_cat)) + res = { + 'id': parent_cat['id'], + 'name': parent_cat['name'], + 'childs': parent_cat['childs'], + 'records': [], + 'obj': parent_cat, + } + return res + + def get_category_items(self, records): + records_by_category = {} + + def _get_tree_categories(cat): + sub_categories = {} + if not cat['childs']: + sub_categories[cat['name']] = records_by_category.get(cat['id']) or [] + else: + for child in cat['childs']: + sub_categories.update(_get_tree_categories( + self.target_categories[child]['obj'])) + return sub_categories + + for record in records: + cat_id = record.get('template.account_category') + if cat_id not in records_by_category.keys(): + records_by_category[cat_id] = [] + + records_by_category[cat_id].append(record) + + res = {} + for ac in self.allow_categories: + res[ac['name']] = _get_tree_categories(ac) + return res + + def on_selected_product(self): + if self.dialog_search_products.current_row: + self._current_line_id = None + self.clear_right_panel() + self.add_product(product=self.dialog_search_products.current_row) + + def on_selected_icon_product(self): + if self.dialog_search_products.current_row: + code = self.dialog_search_products.current_row['code'] + products = self.Product.find([ + ('code', '=', code) + ]) + if not products: + return + + product = products[0] + image = Image(name='product_icon') + if not product['image']: + return + + b64image = product['image'].encode() + image.set_image(b64image, kind='bytes') + image.activate() + + def on_selected_stock_product(self): + if self.dialog_search_products.current_row: + code = self.dialog_search_products.current_row['code'] + res = self.Product.get_stock_by_locations({'code': code}) + self.dialog_product_stock.update_values(res) + self.dialog_product_stock.show() + + def on_selected_item(self, product_id): + if product_id: + self.clear_right_panel() + self.add_product(id=product_id) + + def create_dialog_manage_tables(self): + if not self._Tables: + return + tables = self._Tables.find([ + ('shop', '=', self.shop['id']) + ]) + self.tables = ManageTables(self, tables, self.action_table_assigned) + self.dialog_manage_tables = QuickDialog(self, 'action', widgets=[self.tables]) + + def create_dialog_search_sales(self): + headers = OrderedDict() + headers['id'] = {'desc': self.tr('ID'), 'type': 'char'} + headers['number'] = {'desc': self.tr('NUMBER'), 'type': 'char'} + headers['invoice_number'] = {'desc': self.tr('INVOICE'), 'type': 'char'} + headers['party.name'] = {'desc': self.tr('PARTY'), 'type': 'char'} + headers['sale_date'] = {'desc': self.tr('DATE'), 'type': 'char'} + headers['salesman.name'] = {'desc': self.tr('SALESMAN'), 'type': 'char'} + headers['position'] = {'desc': self.tr('POSITION'), 'type': 'char'} + headers['total_amount_cache'] = {'desc': self.tr('TOTAL AMOUNT'), 'type': 'number'} + + widths = [20, 100, 150, 100, 90, 150, 100, 100] + title = self.tr('SEARCH SALES...') + methods = { + 'on_selected_method': 'on_selected_sale', + 'on_return_method': 'on_selected_sale' + } + self.dialog_search_sales = SearchWindow(self, headers, None, methods, + filter_column=[1, 2, 3, 4], cols_width=widths, title=title, fill=True) + + self.dialog_search_sales.activate_counter() + + def create_dialog_search_products(self): + _cols_width = [10, 80] + headers = OrderedDict() + headers['id'] = {'desc': self.tr('ID'), 'type': 'char'} + headers['code'] = {'desc': self.tr('CODE'), 'type': 'char'} + if self._config.get('show_stock_pos') in ['icon', 'value']: + headers['quantity'] = {'desc': self.tr('STOCK'), 'type': 'number'} + if self._config['show_stock_pos'] == 'icon': + headers['quantity']['icon'] = 'stock' + headers['quantity']['type'] = 'icon' + _cols_width.append(60) + + headers['name'] = {'desc': self.tr('NAME'), 'type': 'char'} + _cols_width.append(350) + + if self._config.get('show_description_pos'): + headers['description'] = {'desc': self.tr('DESCRIPTION'), 'type': 'char'} + _cols_width.append(300) + + if self._config.get('show_brand'): + headers['template.brand'] = {'desc': self.tr('BRAND'), 'type': 'char'} + _cols_width.append(100) + + price = {'desc': self.tr('PRICE'), 'type': 'number'} + if not self._config.get('encoded_sale_price'): + headers['template.sale_price_w_tax'] = price + else: + price['type'] = 'char' + headers['encoded_sale_price'] = price + + _cols_width.append(100) + + if self._config.get('show_location_pos'): + headers['location.name'] = {'desc': self.tr('LOCATION'), 'type': 'char'} + _cols_width.append(100) + + if self._config['show_product_image']: + headers['image'] = {'desc': self.tr('IMAGE'), 'icon': 'image', 'type': 'icon'} + _cols_width.append(30) + + methods = { + 'on_selected_method': 'on_selected_product', + 'on_return_method': 'on_search_product', + 'image': self.on_selected_icon_product, + 'quantity': self.on_selected_stock_product + } + + self.dialog_search_products = SearchWindow(self, headers, None, + methods, cols_width=_cols_width, fill=True) + + def create_dialog_search_party(self): + headers = OrderedDict() + headers['id'] = {'desc': self.tr('ID'), 'type': 'char'} + headers['id_number'] = {'desc': self.tr('ID NUMBER'), 'type': 'char'} + headers['name'] = {'desc': self.tr('NAME'), 'type': 'char'} + headers['street'] = {'desc': self.tr('ADDRESS'), 'type': 'char'} + headers['phone'] = {'desc': self.tr('PHONE'), 'type': 'char'} + + title = self.tr('SEARCH CUSTOMER') + methods = { + 'on_selected_method': 'on_selected_party', + 'on_return_method': 'on_search_party', + } + self.dialog_search_parties = SearchWindow(self, headers, None, + methods, filter_column=[], cols_width=[60, 120, 270, 190, 90], + title=title, fill=True) + + def create_dialog_payment(self): + data = { + 'name': 'journal', + 'values': sorted([(str(j), self._journals[j]['name']) + for j in self._journals]), + 'heads': [self.tr('ID'), self.tr('PAYMENT MODE:')], + } + string = self.tr('SELECT PAYMENT MODE:') + self.dialog_payment = QuickDialog(self, 'selection', string, data) + + def create_dialog_stock(self): + data = { + 'name': 'stock', + 'values': [], + 'heads': [self.tr('WAREHOUSE'), self.tr('QUANTITY')], + } + label = self.tr('STOCK BY PRODUCT:') + self.dialog_product_stock = QuickDialog(self.dialog_search_products, + 'selection', label, data, readonly=True) + + def create_dialog_salesman(self): + data = { + 'name': 'salesman', + 'values': [(str(e['id']), e['party']['name']) + for e in self.employees], + 'heads': [self.tr('Id'), self.tr('Salesman')], + } + string = self.tr('CHOOSE SALESMAN') + self.dialog_salesman = QuickDialog(self, 'selection', string, data) + + def create_dialog_taxes(self): + if self.shop_taxes: + taxes = [(str(e['id']), e['name']) for e in self.shop_taxes] + else: + taxes = [] + data = { + 'name': 'tax', + 'values': taxes, + 'heads': [self.tr('Id'), self.tr('Salesman')], + } + string = self.tr('CHOOSE TAX') + self.dialog_tax = QuickDialog(self, 'selection', string, data) + + def create_dialog_payment_term(self): + data = { + 'name': 'payment_term', + 'values': [(p_id, self._payment_terms[p_id]['name']) + for p_id in self._payment_terms], + 'heads': [self.tr('ID'), self.tr('PAYMENT TERM')], + } + string = self.tr('SELECT PAYMENT TERM') + self.dialog_payment_term = QuickDialog(self, 'selection', string, data) + + def on_search_product(self): + target = self.dialog_search_products.filter_field.text() + if not target: + return + target_words = target.split(' ') + domain = [ + ('template.active', '=', True), + ('active', '=', True), + ] + + for tw in target_words: + if len(tw) <= 1: + continue + clause = ['OR', + ('template.name', 'ilike', '%{:}%'.format(tw)), + ('description', 'ilike', '%{:}%'.format(tw)), + ] + domain.append(clause) + if self.shop.get('product_categories'): + categories_id = [ct['id'] for ct in self.shop['product_categories']] + domain.append(('template.account_category', 'in', categories_id)) + + if not domain: + return + if self.cache_local: + products = self.store.find_product_elastic(domain, limit=100) + else: + products = self.Product.find(domain, limit=100, ctx=self.stock_context) + self.dialog_search_products.set_from_values(products) + + def on_search_party(self): + target = self.dialog_search_parties.filter_field.text() + if not target: + return + target_words = target.split(' ') + domain = [('id_number', '!=', None)] + for tw in target_words: + if len(tw) <= 2: + continue + or_clause = ['OR', + ('name', 'ilike', '%' + tw + '%'), + ('contact_mechanisms.value', 'like', tw + '%'), + ('id_number', 'like', tw + '%'), + ] + domain.append(or_clause) + + parties = self.Party.find(domain) + self.dialog_search_parties.set_from_values(parties) + + def create_dialog_print_invoice(self): + view = [ + ('invoice_number_ask', {'name': self.tr('INVOICE NUMBER')}), + ('printer_ask', { + 'name': self.tr('PRINTER'), + 'type': 'selection', + 'values': [ + (1, 'POS'), + (2, 'LASER') + ], + }), + ('type_ask', { + 'name': self.tr('TYPE'), + 'type': 'selection', + 'values': [ + ('invoice', self.tr('INVOICE')), + ('order', self.tr('ORDER')) + ], + }), + ] + self.dialog_print_invoice = QuickDialog(self, 'action', data=view) + + def create_dialog_cancel_invoice(self): + view = [ + ('password_for_cancel_ask', { + 'name': self.tr('INSERT PASSWORD FOR CANCEL'), + 'password': True + }), + ] + self.dialog_cancel_invoice = QuickDialog(self, 'action', data=view) + + def create_dialog_global_discount(self): + field = 'global_discount_ask' + data = {'name': self.tr('GLOBAL DISCOUNT')} + self.dialog_global_discount = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_force_assign(self): + field = 'password_force_assign_ask' + data = {'name': self.tr('PASSWORD FORCE ASSIGN')} + self.dialog_force_assign = QuickDialog(self, 'action', data=[(field, data)]) + self.field_password_force_assign_ask.setEchoMode(QLineEdit.Password) + + def create_dialog_voucher(self): + field = 'voucher_ask' + data = {'name': self.tr('VOUCHER NUMBER')} + self.dialog_voucher = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_order(self): + # field = 'Send Order' + # data = {'name': self.tr('COPY NUMBER')} + string = self.tr('DO YOU WANT TO CONFIRM THE SEND ORDER?') + self.dialog_order = QuickDialog(self, 'action', string, data=[]) + + def create_dialog_position(self): + field = 'position_ask' + data = {'name': self.tr('POSITION')} + self.dialog_position = QuickDialog(self, 'action', data=[(field, data)]) + + def create_dialog_agent(self): + view = [ + ('agent_ask', { + 'name': self.tr('AGENT'), + 'type': 'relation', + 'model': self._Agent, + 'domain': [('active', '=', True)], + 'fields': [ + ('id', self.tr('ID')), + ('party.name', self.tr('NAME')), + ('party.id_number', self.tr('ID NUMBER')), + ]}), + ('commission_ask', {'name': self.tr('COMMISSION')}), + ('commission_amount', {'name': self.tr('AMOUNT'), + 'readonly': True}), + ] + self.dialog_agent = QuickDialog(self, 'action', data=view, size=(600, 400)) + + def create_wizard_new_sale(self): + pass + + def create_dialog_comment(self): + field = 'comment_ask' + data = {'name': self.tr('COMMENTS'), 'widget': 'text'} + self.dialog_comment = QuickDialog(self, 'action', data=[(field, data)]) + + def clear_data(self): + self._sale = { + 'total_amount': 0 + } + self.party_name = None + self._sale_line = {'id': None} + self._total_amount = {} + self._sale_lines_taxes = {} + self.field_journal_id = self.default_journal['id'] + + def clear_left_panel(self): + self.message_bar.set('system_ready') + self.field_party.setText('') + self.field_salesman.setText('') + self.field_salesman_id = None + self.field_invoice_type.set_from_id('') + self.field_party_id = None + self.field_agent_id = None + self.field_payment_term_id = self.default_payment_term['id'] + self.field_payment_term.setText(self.default_payment_term['name']) + self.field_date.setText('') + self.field_global_discount_ask.setText('') + self.field_amount.zero() + self.field_order_number.setText('') + self.current_comment = '' + if self.field_delivery_charge: + self.field_delivery_charge.set_from_id('') + if self.field_table_assigned: + self.field_table_assigned.setText('') + if hasattr(self, 'field_comment_ask'): + # self.field_comment_ask.document().clear() + pass + if hasattr(self, 'field_points'): + self.field_points.setText('') + if self._commission_activated and hasattr(self, 'field_agent'): + self.field_agent.setText('') + self.field_agent_ask.setText('') + self.field_commission_ask.setText('') + self.field_commission_amount.setText('') + if hasattr(self, 'field_position'): + self.field_position.setText('') + self.field_position_ask.setText('') + self.model_sale_lines.reset() + self.clear_input_text() + self.clear_amount_text() + + def clear_right_panel(self): + if self.is_clear_right_panel: + return + self.field_invoice.setText('') + self.field_untaxed_amount.zero() + self.field_taxes_amount.zero() + self.field_total_amount.zero() + self.field_change.zero() + self.field_paid.zero() + self.field_discount.zero() + self.table_payment_lines.reset() + self.is_clear_right_panel = True + + def state_enabled(self): + pass + + def state_disabled(self): + self.payment_ctx = {} + self.clear_left_panel() + self.table_sale_lines.setDisabled(True) + self.set_state('disabled') + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(False) + + def createNewSale(self): + self.check_empty_sale() + if self.type_pos_user == 'cashier': + self.state_disabled() + return + self.set_state('add') + self.input_text_changed('') + self.amount_text_changed('0') + self.clear_sign() + self.global_timer = 0 + self.payment_ctx = {} + self.is_clear_right_panel = False + self.table_sale_lines.setEnabled(True) + self.clear_data() + self.clear_left_panel() + self._sale = self._PosSale.new_sale([], { + 'shop': self.shop['id'], + 'invoice_type': 'P', + 'company': self.company['id'], + 'party': self.default_party['id'], + 'sale_device': self.device['id'], + 'payment_term': self.default_payment_term['id'] + }, self._context) + + self.field_invoice_type.set_from_id(self._sale['invoice_type']) + # FIXME ADD MORE + self._sale.update({ + 'total_amount': 0 + }) + self.party_id = self.default_party['id'] + if self._sale.get('id'): + self._set_sale_date() + self.field_order_number.setText(self._sale['number']) + if self.field_delivery_charge: + self.field_delivery_charge.set_enabled(True) + + def _set_sale_date(self): + if self._sale.get('sale_date'): + local_date = self._sale['sale_date'] + if isinstance(local_date, date): + local_date = local_date.isoformat() + self.field_date.setText(local_date) + + def _search_product(self, code): + domain = [ + ('template.salable', '=', True), + ('template.account_category', '!=', None), + ] + domain.append(['OR', + ('barcode', '=', code), + ('code', '=', code), + ]) + products = self.Product.find(domain) + if not products or len(products) > 1: + self.message_bar.set('product_not_found') + return False + else: + product = products[0] + return product + + def check_salesman(self): + if self.salesman_required and not self.field_salesman_id: + dialog = self.dialog('missing_salesman') + dialog.exec_() + return False + return True + + def add_product(self, id=None, code=None, product=None): + if self._state == 'disabled': + self.message_bar.set('must_load_or_create_sale') + return + + product_id = None + if id: + product_id = id + elif code: + # REMOVE ME + product = self._search_product(code) + if product: + product_id = product['id'] + elif product: + product_id = product['id'] + + if not product_id: + self._state = 'warning' + return + + data = { + 'sale_id': self._sale['id'], + 'product_id': product_id, + 'qty': 1 + } + res = self.ModSale.faster_add_product(data) + self._sale_line = res + self._current_line_id = res['id'] + self.add_sale_line(res) + self._sale_line['product'] = product + + self.update_total_amount() + self.set_state('add') + + def _check_stock_quantity(self, product, request_qty): + if self._password_admin and product['quantity'] < request_qty: + self.dialog_force_assign.exec_() + password = self.field_password_force_assign_ask.text() + self.field_password_force_assign_ask.setText('') + if password != self._password_admin: + self.message_bar.set('not_can_force_assign') + return False + return True + + def add_sale_line(self, record): + if not record.get('unit.symbol'): + record['unit.symbol'] = record['unit']['symbol'] + if isinstance(record['product'], dict): + if not record.get('product.template.name'): + record['product.template.name'] = record['product']['template']['name'] + if not record.get('product.code'): + record['product.code'] = record['product']['code'] + rec = self.model_sale_lines.add_record(record) + self.field_amount.setText(rec['amount_w_tax']) + + def sale_line_selected(self, product): + if self._state == 'cash': + return + self._current_line_id = product['id'] + self.label_product.setText(product['product.template.name']) + self.row_field_description.setText(product['description']) + self.row_field_qty.setValue(float(product['quantity'])) + self.row_field_price.setText(str(product['unit_price_w_tax'])) + # self.row_field_note.setText(str(product['note'])) + + self.dialog_product_edit.show() + self.row_field_note.setFocus() + + def create_dialog_sale_line(self): + self.state_line = {} + + vbox_product = QVBoxLayout() + grid = QGridLayout() + qty = 2 + + self.label_product = QLabel() + self.label_product.setAlignment(alignCenter) + self.label_product.setObjectName('label_product') + vbox_product.addWidget(self.label_product) + self.row_field_description = QLineEdit() + self.row_field_description.setObjectName('row_field_description') + self.row_field_description.textChanged.connect( + lambda: self.update_sale_line('description') + ) + grid.addWidget(self.row_field_description, 1, 1, 1, 2) + + if self._config.get('show_fractions'): + label_fraction = QLabel(self.tr('FRACTION:')) + label_fraction.setObjectName('label_fraction') + grid.addWidget(label_fraction, 2, 1) + self.field_combobox_fraction = ComboBox(self, 'fraction', + {'values': FRACTIONS}) + grid.addWidget(self.field_combobox_fraction, 2, 2) + self.field_combobox_fraction.currentIndexChanged.connect( + lambda: self.update_sale_line('qty_fraction') + ) + + label_qty = QLabel(self.tr('QUANTITY:')) + label_qty.setObjectName('label_qty') + grid.addWidget(label_qty, 3, 1) + self.row_field_qty = QDoubleSpinBox() + self.row_field_qty.setObjectName('row_field_qty') + self.row_field_qty.setMinimum(0) + self.row_field_qty.setMaximum(100000) + if self._config.get('decimals_digits_quantity'): + qty = self._config['decimals_digits_quantity'] + + self.row_field_qty.setDecimals(qty) + self.row_field_qty.setAlignment(alignCenter) + grid.addWidget(self.row_field_qty, 3, 2) + self.row_field_qty.valueChanged.connect( + lambda: self.update_sale_line('quantity') + ) + + label_price = QLabel(self.tr('UNIT PRICE:')) + label_price.setObjectName('label_price') + grid.addWidget(label_price, 4, 1) + self.row_field_price = FieldMoney(self, 'row_field_price', {}, readonly=False) + self.row_field_price.setObjectName('row_field_price') + grid.addWidget(self.row_field_price, 4, 2) + self.row_field_price.textChanged.connect( + lambda: self.update_sale_line('unit_price') + ) + + self.row_field_note = QTextEdit('') + self.row_field_note.setObjectName('row_field_note') + grid.addWidget(self.row_field_note, 5, 1, 5, 2) + self.row_field_note.textChanged.connect( + lambda: self.update_sale_line('note') + ) + + vbox_product.addLayout(grid) + self.dialog_product_edit = QuickDialog(self, 'action', widgets=[vbox_product]) + self.dialog_product_edit.accepted.connect(self.dialog_product_edit_accepted) + + def update_sale_line(self, field): + value = None + self.state_line['id'] = self._current_line_id + if field == 'quantity': + value = Decimal(self.row_field_qty.value()) + if field == 'unit_price': + value = self.row_field_price.text() + if field == 'qty_fraction': + qty = self.field_combobox_fraction.get_id() + self.row_field_qty.setValue(float(qty)) + value = self.field_combobox_fraction.get_label() + self.state_line['quantity'] = qty + + price_ = self.ModSale.get_product_prices({ + 'ids': [self._sale_line['product']['id']], + 'quantity': float(qty), + 'sale_id': self._sale['id'], + }) + if price_ and price_.get('unit_price_w_tax'): + price_list = str(price_['unit_price_w_tax']) + self.row_field_price.setText(price_list) + self.state_line['unit_price'] = price_list + + if field == 'description': + value = self.row_field_description.text() + if field == 'note': + value = self.row_field_note.toPlainText() + + if value: + self.state_line[field] = value + + def dialog_product_edit_accepted(self): + if not self.state_line: + return + + _record = None + + if self.state_line.get('quantity'): + quantity = self.state_line.pop('quantity') + _record = self.ModSaleLine.faster_set_quantity({ + 'id': self._current_line_id, + 'quantity': to_float(quantity, 2) + }) + + if self.state_line.get('unit_price'): + unit_price = self.state_line.pop('unit_price') + self._sign = '/' + self._process_price(unit_price) + self._sign = None + _record = self.ModSaleLine.write([self._current_line_id], {}) + + if self.state_line.get('description'): + _record = self.ModSaleLine.write([self._current_line_id], { + 'description': self.state_line['description'] + }) + + if self.state_line.get('note'): + _record = self.ModSaleLine.write([self._current_line_id], { + 'note': self.state_line['note'] + }) + + if _record: + if not _record.get('unit.symbol'): + _record['unit.symbol'] = _record['unit']['symbol'] + if not _record.get('product.template.name'): + _record['product.template.name'] = _record['product']['template']['name'] + if not _record.get('product.code'): + _record['product.code'] = _record['product']['code'] + self.model_sale_lines.update_record(_record) + + self.update_total_amount() + self.state_line = {} + # self.field_combobox_fraction.set_from_id(1) + + def setup_sale_line(self): + product_code = { + 'name': 'product.code', + 'align': alignRight, + 'description': self.tr('COD'), + 'width': 80 + } + product = { + 'name': 'product.template.name', + 'align': alignLeft, + 'description': self.tr('NAME'), + 'width': STRETCH + } + description = { + 'name': 'description', + 'align': alignLeft, + 'description': self.tr('DESCRIPTION'), + 'width': 180 + } + uom = { + 'name': 'unit.symbol', + 'align': alignHCenter, + 'description': self.tr('UNIT'), + 'width': 50 + } + qty = { + 'name': 'quantity', + 'format': '{:3,.%sf}', + 'align': alignRight, + 'description': self.tr('QTY'), + 'digits': ('unit.symbol', CONVERSION_DIGITS), + 'width': 80 + } + discount = { + 'name': 'discount', + 'format': '{0:.0%}', + 'align': alignRight, + 'description': self.tr('DISC'), + 'width': 50 + } + amount = { + 'name': 'amount_w_tax', + 'format': '{:5,.1f}', + 'align': alignRight, + 'description': self.tr('SUBTOTAL'), + 'width': 100 + } + note = { + 'name': 'note', + 'align': alignLeft, + 'description': self.tr('NOTE'), + 'invisible': True, + 'width': 100 + } + unit_price = { + 'name': 'unit_price_w_tax', + 'format': '{:5,.1f}', + 'align': alignLeft, + 'description': self.tr('UNIT PRICE W TAX'), + 'invisible': True, + 'width': 100 + } + + qty_fraction = { + 'name': 'qty_fraction', + 'align': alignHCenter, + 'description': self.tr('FRAC'), + 'width': 50 + } + + self.fields_sale_line = [product, uom, qty, discount, + amount, note, unit_price] + + if self.enviroment == 'retail': + self.fields_sale_line.insert(0, product_code) + + if self._config.get('show_description_pos'): + self.fields_sale_line.insert(2, description) + + if self._config.get('show_fractions'): + self.fields_sale_line.insert(4, qty_fraction) + + self.model_sale_lines = TableModel('sale.line', self.fields_sale_line) + + def setup_payment(self): + pay_fields = [{ + 'name': 'statement', + 'align': alignLeft, + 'description': self.tr('STATEMENT JOURNAL'), + }, { + 'name': 'amount', + 'align': alignRight, + 'format': '{:5,.1f}', + 'description': self.tr('AMOUNT'), + }, { + 'name': 'voucher', + 'align': alignCenter, + 'description': self.tr('VOUCHER'), + }] + self.table_payment_lines = TableModel('account.statement.line', pay_fields) + + def action_table(self): + self.table_sale_lines.setFocus() + + def on_change_line_selected(self, key): + self.table_sale_lines.moved_selection(key) + + def action_delete_line(self): + if self.model_sale_lines.rowCount() <= 0 or self._state == 'cash': + return + + self.table_sale_lines.setFocus() + + removed_item = self.table_sale_lines.delete_item() + self.ModSaleLine.delete([removed_item['id']]) + self.set_amounts() + self.update_total_amount() + # self.clear_right_panel() + + self._current_line_id = None + self.setFocus() + self.label_input.setFocus() + + if self.enviroment == 'restaurant': + if removed_item and self.print_order: + self.action_print_order(self._sale['id'], removed_item) + if self._config['tip_product']['code'] == removed_item['product.code']: + self._PosSale.write([self._sale['id']], {'tip': None}) + + def set_discount(self, eval_value, lines_ids=[]): + res = False + try: + value = round(float(str(eval_value)), 6) + except ValueError: + logging.warning('ValueError > ', ValueError) + return + + if float(value) <= 0: + return + + if not lines_ids: + target_lines = [self._current_line_id] + else: + target_lines = lines_ids + + records = self.ModSaleLine.faster_set_discount({ + 'line_ids': target_lines, + 'value': value + }) + + for rec in records: + self.model_sale_lines.update_record(rec) + + if records: + res = True + self.set_amounts() + return res + + def set_unit_price(self, value): + rec = self.ModSaleLine.set_faster_unit_price({ + 'id': self._current_line_id, + 'value': value, + }) + + if rec: + self.model_sale_lines.update_record(rec) + self.update_total_amount() + return True + return False + + def add_payment(self, amount, cash_received=0, change=0): + voucher_number = None + if self._journals[self.field_journal_id]['require_voucher']: + self.dialog_voucher.exec_() + voucher_number = self.field_voucher_ask.text() + if voucher_number is None or voucher_number == '': + return self.add_payment(amount) + + res = self.ModSale.faster_add_payment({ + 'sale_id': self._sale['id'], + 'journal_id': self.field_journal_id, + 'amount': to_numeric(amount), + 'voucher_number': voucher_number, + 'cash_received': to_numeric(cash_received), + 'change': to_numeric(change) + }) + + if res.get('msg') not in ('missing_money', 'ok'): + self.dialog(res['msg']) + return res + + self.table_payment_lines.add_record(res) + return res + + def create_dialog_help(self): + from .help import Help + help = Help(self) + help.show() + + def set_keys(self): + self.keys_numbers = list(range(Qt.Key_0, Qt.Key_9 + 1)) + self.keys_alpha = list(range(Qt.Key_A, Qt.Key_Z + 1)) + self.keys_period = [Qt.Key_Period] + self.show_keys = self.keys_numbers + self.keys_alpha + self.keys_period + + self.keys_special = [Qt.Key_Asterisk, Qt.Key_Comma, + Qt.Key_Minus, Qt.Key_Slash] + self.keys_input = [Qt.Key_Backspace] + self.keys_input.extend(self.keys_special) + self.keys_input.extend(self.show_keys) + self.keys_input.extend(self.keys_numbers) + self.keys_input.extend([Qt.Key_Return, Qt.Key_Plus]) + + def set_state(self, state='add'): + self._state = state + state = STATES[state] + self._re = state['re'] + if not self.type_pos_user == 'order': + if not self.buttonpad.stacked.stacked.currentWidget(): + return + if state['button']: + self.buttonpad.stacked.stacked.setCurrentWidget( + getattr(self.buttonpad.stacked, state['button']) + ) + if not self.tablet_mode: + self.buttonpad.stacked.stacked.currentWidget().setVisible(True) + else: + self.buttonpad.stacked.stacked.currentWidget().setVisible(False) + + def key_pressed(self, text): + if not self._sign and self._state != 'cash': + if self._re.match(self._input_text + text): + self.input_text_changed(text) + else: + if RE_SIGN['quantity'].match(self._amount_text + text): + self.amount_text_changed(text) + + def clear_sign(self): + self._sign = None + self.field_sign.setText(' {0} '.format(' ')) + + def sign_text_changed(self, sign): + self._sign = sign + self.field_sign.setText(' {0} '.format(sign)) + if hasattr(self, '_sale_line') and self._sale_line: + if sign == '-': + self.message_bar.set('enter_discount') + elif sign == '/': + self.message_bar.set('enter_new_price') + elif sign == '*': + self.message_bar.set('enter_quantity') + if self.active_weighing and self._sale_line['unit_symbol'] != 'u': + self.action_read_weight() + + def key_special_pressed(self, value): + self.clear_amount_text() + self.clear_input_text() + if value not in ['-', '/', '*']: + return + self.sign_text_changed(value) + + def key_backspace_pressed(self): + if self._sign or self._state == 'cash': + self._amount_text = self._amount_text[:-1] + self.amount_text_changed() + else: + self._input_text = self._input_text[:-1] + self.input_text_changed() + + def set_text(self, text): + if not self._state == 'cash': + self.input_text_changed(text) + else: + self.amount_text_changed(text) + + def clear_input_text(self): + self.input_text_changed('') + + def clear_amount_text(self): + self._amount_text = '0' + self.amount_text_changed() + + def keyPressEvent(self, event): + self._keyStates[event.key()] = True + key = event.key() + + if self._state == 'add' and key not in self.keys_input and \ + key not in (Qt.Key_Enter, Qt.Key_End): + # Clear ui context when keys function are pressed + self._clear_context() + + if key in (Qt.Key_Return, Qt.Key_Plus): + self.button_plus_pressed() + elif key in self.show_keys: + # No allow change quantity o discount in state == cash + if self._state == 'cash' and key not in self.keys_numbers: + return + self.key_pressed(event.text()) + elif key in self.keys_special: + if self._state == 'cash' or not self._current_line_id: + return + self.key_special_pressed(event.text()) + elif key == Qt.Key_Backspace: + self.key_backspace_pressed() + elif key == Qt.Key_Escape: + self.close() + elif key == Qt.Key_F1: + self.create_dialog_help() + elif key == Qt.Key_F9: + self.action_search_sale() + elif key == Qt.Key_F11: + self.action_new_sale() + elif key == Qt.Key_F7: + self.action_print_sale() + elif self._state == 'disabled': + self.message_bar.set('must_load_or_create_sale') + return + elif key in (Qt.Key_Enter, Qt.Key_End): + if self.type_pos_user in ['order', 'salesman']: + return + if self._state == 'add': + self.button_accept_pressed() + elif self._state in ['accept', 'cash']: + self.button_cash_pressed() + elif key == Qt.Key_F2: + self.action_search_product() + elif key == Qt.Key_F3: + self.action_payment() + elif key == Qt.Key_F4: + self.action_party() + elif key == Qt.Key_F5: + self.action_global_discount() + elif key == Qt.Key_F6: + self.action_print_order() + elif key == Qt.Key_F8: + self.action_payment_term() + elif key == Qt.Key_F10: + self.action_table() + elif key == Qt.Key_F12: + self.action_cancel() + elif key == Qt.Key_Home: + self.action_salesman() + elif key == Qt.Key_Down or key == Qt.Key_Up: + self.on_change_line_selected(key) + elif key == Qt.Key_Delete: + self.action_delete_line() + elif key == Qt.Key_Insert: + self.action_position() + elif key == Qt.Key_Semicolon and self._commission_activated: + sale = self.get_current_sale() + if sale['state'] == 'draft' and self._state not in ['accept', 'cash']: + self.action_agent() + elif key == Qt.Key_QuoteDbl: + self.action_comment() + elif key == Qt.Key_Question: + self.action_tax() + else: + pass + + @property + def state(self): + return self._state + + +class DoInvoice(QThread): + """ + Process invoices using a thread + """ + sigDoInvoice = pyqtSignal() + + def __init__(self, main, context): + QThread.__init__(self) + + def run(self): + self.sigDoInvoice.emit() diff --git a/app/mainwindow.py b/app/mainwindow.py index fd10713..9c01ae9 100644 --- a/app/mainwindow.py +++ b/app/mainwindow.py @@ -382,7 +382,6 @@ class MainWindow(FrontWindow): self._action_report, = self.ActionReport.find([ ('report_name', '=', 'account.invoice'), ]) - if self._config['show_stock_pos'] in ('value', 'icon'): self.stock_context = { 'stock_date_end': date.today(), @@ -1911,7 +1910,8 @@ class MainWindow(FrontWindow): ('commission_amount', {'name': self.tr('AMOUNT'), 'readonly': True}), ] - self.dialog_agent = QuickDialog(self, 'action', data=view, size=(600, 400)) + print() + self.dialog_agent = QuickDialog(self, 'action', data=view) def create_wizard_new_sale(self): pass diff --git a/app/share/icon.png b/app/share/icon.png new file mode 100644 index 0000000..0487e14 Binary files /dev/null and b/app/share/icon.png differ diff --git a/app/share/pos-icon.svg b/app/share/pos-icon.svg index 8a60771..cb36bf6 100644 --- a/app/share/pos-icon.svg +++ b/app/share/pos-icon.svg @@ -14,9 +14,9 @@ height="96" id="svg5453" version="1.1" - inkscape:version="0.91 r13725" + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="pos-icon.svg" - inkscape:export-filename="/mnt/DataX/Dev/TRYTON/TRYTON-3.6/PRESIK-DEV/POS/pos_client_qt5/pos/interface/pos-icon.png" + inkscape:export-filename="/media/DATA/Seafile/DEVELOPMENT/TRYTON/TRYTON-5.0/FRONTENDS/psk_pos/app/share/icon.png" inkscape:export-xdpi="86" inkscape:export-ydpi="86"> + id="guide4146" + inkscape:locked="false" /> + id="guide4148" + inkscape:locked="false" /> + id="guide4150" + inkscape:locked="false" /> + id="guide4152" + inkscape:locked="false" /> + id="guide4154" + inkscape:locked="false" /> + id="guide4156" + inkscape:locked="false" /> + id="guide4158" + inkscape:locked="false" /> + id="guide4160" + inkscape:locked="false" /> + id="guide4162" + inkscape:locked="false" /> + id="guide4164" + inkscape:locked="false" /> + id="guide4166" + inkscape:locked="false" /> + id="guide4168" + inkscape:locked="false" /> + id="guide4170" + inkscape:locked="false" /> + id="guide4172" + inkscape:locked="false" /> + id="guide4174" + inkscape:locked="false" /> + id="guide4176" + inkscape:locked="false" />