def _open_till(self, store): till = Till(store=store, station=api.get_current_station(store)) till.open_till() TillOpenEvent.emit(till=till) self.assertEquals(till, Till.get_current(store)) return till
def before_start(self, store): till = Till.get_current(store) if till is None: till = Till(store=store, station=get_current_station(store)) till.open_till() assert till == Till.get_current(store)
def testTillOpenOnce(self): station = get_current_station(self.store) till = Till(store=self.store, station=station) till.open_till() till.close_till() self.assertRaises(TillError, till.open_till)
def testTillClose(self): station = self.create_station() till = Till(store=self.store, station=station) till.open_till() self.assertEqual(till.status, Till.STATUS_OPEN) till.close_till() self.assertEqual(till.status, Till.STATUS_CLOSED) self.assertRaises(TillError, till.close_till)
def test_needs_closing(self): till = Till(station=self.create_station(), store=self.store) self.failIf(till.needs_closing()) till.open_till() self.failIf(till.needs_closing()) till.opening_date = localnow() - datetime.timedelta(1) self.failUnless(till.needs_closing()) till.close_till() self.failIf(till.needs_closing())
def test_get_balance(self): till = Till(store=self.store, station=self.create_station()) till.open_till() old = till.get_balance() till.add_credit_entry(currency(10), u"") self.assertEqual(till.get_balance(), old + 10) till.add_debit_entry(currency(5), u"") self.assertEqual(till.get_balance(), old + 5)
def test_get_debits_total(self): till = Till(store=self.store, station=self.create_station()) till.open_till() old = till.get_debits_total() till.add_debit_entry(currency(10), u"") self.assertEqual(till.get_debits_total(), old - 10) # This should not affect the debit till.add_credit_entry(currency(5), u"") self.assertEqual(till.get_debits_total(), old - 10)
def test_till_history_report(self): from stoqlib.gui.dialogs.tillhistory import TillHistoryDialog dialog = TillHistoryDialog(self.store) till = Till(station=get_current_station(self.store), store=self.store) till.open_till() sale = self.create_sale() sellable = self.create_sellable() sale.add_sellable(sellable, price=100) method = PaymentMethod.get_by_name(self.store, u"bill") payment = method.create_payment(Payment.TYPE_IN, sale.group, sale.branch, Decimal(100)) TillEntry( value=25, identifier=20, description=u"Cash In", payment=None, till=till, branch=till.station.branch, date=datetime.date(2007, 1, 1), store=self.store, ) TillEntry( value=-5, identifier=21, description=u"Cash Out", payment=None, till=till, branch=till.station.branch, date=datetime.date(2007, 1, 1), store=self.store, ) TillEntry( value=100, identifier=22, description=sellable.get_description(), payment=payment, till=till, branch=till.station.branch, date=datetime.date(2007, 1, 1), store=self.store, ) till_entry = list(self.store.find(TillEntry, till=till)) today = datetime.date.today().strftime("%x") for item in till_entry: if today in item.description: date = datetime.date(2007, 1, 1).strftime("%x") item.description = item.description.replace(today, date) item.date = datetime.date(2007, 1, 1) dialog.results.append(item) self._diff_expected(TillHistoryReport, "till-history-report", dialog.results, list(dialog.results))
def on_Edit__activate(self, action): try: Till.get_current(self.store) except TillError as e: warning(str(e)) return store = api.new_store() views = self.results.get_selected_rows() sale = store.fetch(views[0].sale) retval = run_dialog(SalePaymentsEditor, self, store, sale) if store.confirm(retval): self.refresh() store.close()
def _cancel_last_document(self): try: self._validate_printer() except DeviceError as e: warning(str(e)) return store = new_store() last_doc = self._get_last_document(store) if not self._confirm_last_document_cancel(last_doc): store.close() return if last_doc.last_till_entry: self._cancel_last_till_entry(last_doc, store) elif last_doc.last_sale: # Verify till balance before cancel the last sale. till = Till.get_current(store) if last_doc.last_sale.total_amount > till.get_balance(): warning(_("You do not have this value on till.")) store.close() return cancelled = self._printer.cancel() if not cancelled: info(_("Cancelling sale failed, nothing to cancel")) store.close() return else: self._cancel_last_sale(last_doc, store) store.commit()
def _receive(self): with api.new_store() as store: till = Till.get_current(store) assert till in_payment = self.results.get_selected() payment = store.fetch(in_payment.payment) assert self._can_receive(payment) retval = run_dialog(SalePaymentConfirmSlave, self, store, payments=[payment], show_till_info=False) if not retval: return try: TillAddCashEvent.emit(till=till, value=payment.value) except (TillError, DeviceError, DriverError) as e: warning(str(e)) return till_entry = till.add_credit_entry(payment.value, _(u'Received payment: %s') % payment.description) TillAddTillEntryEvent.emit(till_entry, store) if store.committed: self.search.refresh()
def open_till(self): """Opens the till """ try: current_till = Till.get_current(self.store) except TillError as e: warning(str(e)) return False if current_till is not None: warning(_("You already have a till operation opened. " "Close the current Till and open another one.")) return False store = api.new_store() try: model = run_dialog(TillOpeningEditor, self._parent, store) except TillError as e: warning(str(e)) model = None retval = store.confirm(model) store.close() if retval: self._till_status_changed(closed=False, blocked=False) return retval
def needs_closing(self): """Checks if the last opened till was closed and asks the user if he wants to close it :returns: - CLOSE_TILL_BOTH if both DB and ECF needs closing. - CLOSE_TILL_DB if only DB needs closing. - CLOSE_TILL_ECF if only ECF needs closing. - CLOSE_TILL_NONE if both ECF and DB are consistent (they may be closed, or open for the current day) """ ecf_needs_closing = HasPendingReduceZ.emit() last_till = Till.get_last(self.store) if last_till: db_needs_closing = last_till.needs_closing() else: db_needs_closing = False if db_needs_closing and ecf_needs_closing: return CLOSE_TILL_BOTH elif db_needs_closing and not ecf_needs_closing: return CLOSE_TILL_DB elif ecf_needs_closing and not db_needs_closing: return CLOSE_TILL_ECF else: return CLOSE_TILL_NONE
def create_sale(self, id_=None, branch=None, client=None): from stoqlib.domain.sale import Sale from stoqlib.domain.till import Till till = Till.get_current(self.store) if till is None: till = self.create_till() till.open_till() salesperson = self.create_sales_person() group = self.create_payment_group() if client: group.payer = client.person sale = Sale( coupon_id=0, open_date=TransactionTimestamp(), salesperson=salesperson, branch=branch or get_current_branch(self.store), cfop=sysparam(self.store).DEFAULT_SALES_CFOP, group=group, client=client, store=self.store, ) if id_: sale.id = id_ sale.identifier = id_ return sale
def test_sales_person_report(self): sysparam.set_bool(self.store, "SALE_PAY_COMMISSION_WHEN_CONFIRMED", True) salesperson = self.create_sales_person() product = self.create_product(price=100) sellable = product.sellable sale = self.create_sale() sale.salesperson = salesperson sale.add_sellable(sellable, quantity=1) self.create_storable(product, get_current_branch(self.store), stock=100) CommissionSource(sellable=sellable, direct_value=Decimal(10), installments_value=1, store=self.store) sale.order() method = PaymentMethod.get_by_name(self.store, u"money") till = Till.get_last_opened(self.store) method.create_payment(Payment.TYPE_IN, sale.group, sale.branch, sale.get_sale_subtotal(), till=till) sale.confirm() sale.group.pay() salesperson = salesperson commissions = list(self.store.find(CommissionView)) commissions[0].identifier = 1 commissions[1].identifier = 139 self._diff_expected(SalesPersonReport, "sales-person-report", commissions, salesperson) # Also test when there is no salesperson selected self._diff_expected(SalesPersonReport, "sales-person-report-without-salesperson", commissions, None)
def _update_till_status(self, closed, blocked): # Three different situations; # # - Till is closed # - Till is opened # - Till was not closed the previous fiscal day (blocked) self.set_sensitive([self.TillOpen], closed) self.set_sensitive([self.TillClose, self.PaymentReceive], not closed or blocked) widgets = [self.TillVerify, self.TillAddCash, self.TillRemoveCash, self.SearchTillHistory, self.app_vbox] self.set_sensitive(widgets, not closed and not blocked) if closed: text = _(u"Till closed") self.clear() self.setup_focus() elif blocked: text = _(u"Till blocked from previous day") else: till = Till.get_current(self.store) text = _(u"Till opened on %s") % till.opening_date.strftime('%x') self.till_status_label.set_text(text) self._update_toolbar_buttons() self._update_total()
def testSalesPersonReport(self): sysparam(self.store).SALE_PAY_COMMISSION_WHEN_CONFIRMED = 1 salesperson = self.create_sales_person() product = self.create_product(price=100) sellable = product.sellable sale = self.create_sale() sale.salesperson = salesperson sale.add_sellable(sellable, quantity=1) self.create_storable(product, get_current_branch(self.store), stock=100) CommissionSource(sellable=sellable, direct_value=Decimal(10), installments_value=1, store=self.store) sale.order() method = PaymentMethod.get_by_name(self.store, u'money') till = Till.get_last_opened(self.store) method.create_inpayment(sale.group, sale.branch, sale.get_sale_subtotal(), till=till) sale.confirm() sale.set_paid() salesperson_name = salesperson.person.name commissions = list(self.store.find(CommissionView)) commissions[0].identifier = 1 commissions[1].identifier = 139 self._diff_expected(SalesPersonReport, 'sales-person-report', commissions, salesperson_name)
def testGetCurrentTillClose(self): station = get_current_station(self.store) self.assertEqual(Till.get_current(self.store), None) till = Till(store=self.store, station=station) till.open_till() self.assertEqual(Till.get_current(self.store), till) till.close_till() self.assertEqual(Till.get_current(self.store), None)
def close_till(self, close_db=True, close_ecf=True): """Closes the till There are 3 possibilities for parameters combination: * *total close*: Both *close_db* and *close_ecf* are ``True``. The till on both will be closed. * *partial close*: Both *close_db* and *close_ecf* are ``False``. It's more like a till verification. The actual user will do it to check and maybe remove money from till, leaving it ready for the next one. Note that this will not emit 'till-status-changed' event, since the till will not really close. * *fix conflicting status*: *close_db* and *close_ecf* are different. Use this only if you need to fix a conflicting status, like if the DB is open but the ECF is closed, or the other way around. :param close_db: If the till in the DB should be closed :param close_ecf: If the till in the ECF should be closed :returns: True if the till was closed, otherwise False """ is_partial = not close_db and not close_ecf manager = get_plugin_manager() # This behavior is only because of ECF if not is_partial and not self._previous_day: if (manager.is_active('ecf') and not yesno(_("You can only close the till once per day. " "You won't be able to make any more sales today.\n\n" "Close the till?"), Gtk.ResponseType.NO, _("Close Till"), _("Not now"))): return elif not is_partial: # When closing from a previous day, close only what is needed. close_db = self._close_db close_ecf = self._close_ecf if close_db: till = Till.get_last_opened(self.store) assert till store = api.new_store() editor_class = TillVerifyEditor if is_partial else TillClosingEditor model = run_dialog(editor_class, self._parent, store, previous_day=self._previous_day, close_db=close_db, close_ecf=close_ecf) if not model: store.confirm(model) store.close() return # TillClosingEditor closes the till retval = store.confirm(model) store.close() if retval and not is_partial: self._till_status_changed(closed=True, blocked=False) return retval
def test_add_debit_entry(self): till = Till(store=self.store, station=self.create_station()) till.open_till() self.assertEqual(till.get_balance(), 0) till.add_debit_entry(10) self.assertEqual(till.get_balance(), -10)
def test_till_daily_movement(self): date = datetime.date(2013, 1, 1) # create sale payment sale = self.create_sale() sellable = self.create_sellable() sale.add_sellable(sellable, price=100) sale.identifier = 1000 sale.order() method = PaymentMethod.get_by_name(self.store, u'money') till = Till.get_last_opened(self.store) payment = method.create_payment(Payment.TYPE_IN, sale.group, sale.branch, sale.get_sale_subtotal(), till=till) sale.confirm() sale.group.pay() sale.confirm_date = date payment.identifier = 1010 payment.paid_date = date # create lonely input payment payer = self.create_client() address = self.create_address() address.person = payer.person method = PaymentMethod.get_by_name(self.store, u'money') group = self.create_payment_group() branch = self.create_branch() payment_lonely_input = method.create_payment(Payment.TYPE_IN, group, branch, Decimal(100)) payment_lonely_input.description = u"Test receivable account" payment_lonely_input.group.payer = payer.person payment_lonely_input.set_pending() payment_lonely_input.pay() payment_lonely_input.identifier = 1001 payment_lonely_input.paid_date = date # create purchase payment drawee = self.create_supplier() address = self.create_address() address.person = drawee.person method = PaymentMethod.get_by_name(self.store, u'money') group = self.create_payment_group() branch = self.create_branch() payment = method.create_payment(Payment.TYPE_OUT, group, branch, Decimal(100)) payment.description = u"Test payable account" payment.group.recipient = drawee.person payment.set_pending() payment.pay() payment.identifier = 1002 payment.paid_date = date # create lonely output payment self._diff_expected(TillDailyMovementReport, 'till-daily-movement-report', self.store, date)
def create_model(self, store): till = Till.get_current(self.store) return Settable(employee=None, payment=None, # FIXME: should send in consts.now() open_date=None, till=till, balance=till.get_balance(), value=currency(0))
def test_add_entry_out_payment(self): till = Till(store=self.store, station=self.create_station()) till.open_till() payment = self._create_outpayment() self.assertEqual(till.get_balance(), 0) till.add_entry(payment) self.assertEqual(till.get_balance(), -10)
def test_till_open_other_station(self): till = Till(station=self.create_station(), store=self.store) till.open_till() till = Till(station=get_current_station(self.store), store=self.store) till.open_till() self.assertEqual(Till.get_last_opened(self.store), till)
def testCreateInPayment(self): payment = self.createInPayment() self.failUnless(isinstance(payment, Payment)) self.assertEqual(payment.value, Decimal(100)) self.assertEqual(payment.till, Till.get_current(self.store)) payment_without_till = self.createInPayment(till=None) self.failUnless(isinstance(payment, Payment)) self.assertEqual(payment_without_till.value, Decimal(100)) self.assertEqual(payment_without_till.till, None)
def testAddEntryInPayment(self): till = Till(store=self.store, station=self.create_station()) till.open_till() payment = self._create_inpayment() self.assertEqual(till.get_balance(), 0) till.add_entry(payment) self.assertEqual(till.get_balance(), 10)
def when_done(self, store): # This is sort of hack, set the opening/closing dates to the date before # it's run, so we can open/close the till in the tests, which uses # the examples. till = Till.get_current(store) # Do not leave anything in the till. till.add_debit_entry(till.get_balance(), _(u"Amount removed from Till")) till.close_till() yesterday = localtoday() - datetime.timedelta(1) till.opening_date = yesterday till.closing_date = yesterday
def _get_till_balance(self): """Returns the balance of till operations""" try: till = Till.get_current(self.store) except TillError: till = None if till is None: return currency(0) return till.get_balance()
def _cancel_last_sale(self, last_doc, store): if last_doc.last_sale.status == Sale.STATUS_RETURNED: return sale = store.fetch(last_doc.last_sale) value = sale.total_amount reason = _(u"Cancelling last document on ECF") sale.cancel(reason, force=True) till = Till.get_current(store) # TRANSLATORS: cash out = sangria till.add_debit_entry(value, _(u"Cash out: last sale cancelled")) last_doc.last_sale = None info(_("Document was cancelled"))
def on_Renegotiate__activate(self, action): try: Till.get_current(self.store) except TillError as e: warning(str(e)) return receivable_views = self.results.get_selected_rows() if not self._can_renegotiate(receivable_views): warning(_('Cannot renegotiate selected payments')) return store = api.new_store() groups = list(set([store.fetch(v.group) for v in receivable_views])) retval = run_dialog(PaymentRenegotiationWizard, self, store, groups) if store.confirm(retval): # FIXME: Storm is not expiring the groups correctly. # Figure out why. See bug 5087 self.refresh() self._update_widgets() store.close()
def _update_till_status(self, closed, blocked): # Three different situations: # # - Till is closed # - Till is opened # - Till was not closed the previous fiscal day (blocked) self.set_sensitive([self.TillOpen], closed) self.set_sensitive([self.TillClose], not closed or blocked) widgets = [self.TillVerify, self.TillAddCash, self.TillRemoveCash, self.SearchTillHistory, self.search_holder, self.PaymentReceive] self.set_sensitive(widgets, not closed and not blocked) def large(s): return '<span weight="bold" size="xx-large">%s</span>' % ( stoq_api.escape(s), ) if closed: text = large(_(u"Till closed")) self.search_holder.hide() self.footer_hbox.hide() self.large_status.show() self.clear() self.setup_focus() # Adding the label on footer without the link self.small_status.set_text(text) if not blocked: text += '\n\n<span size="large"><a href="open-till">%s</a></span>' % ( stoq_api.escape(_('Open till'))) self.status_link.set_markup(text) elif blocked: self.search_holder.hide() self.footer_hbox.hide() text = large(_(u"Till blocked")) self.status_link.set_markup(text) self.small_status.set_text(text) else: self.search_holder.show() self.footer_hbox.show() self.large_status.hide() till = Till.get_current(self.store, api.get_current_station(self.store)) text = _(u"Till opened on %s") % till.opening_date.strftime('%x') self.small_status.set_text(text) self._update_toolbar_buttons() self._update_total() if sysparam.get_bool('SHOW_TOTAL_PAYMENTS_ON_TILL'): self._update_payment_total()
def _update_widgets(self, sale_view): sale = sale_view and sale_view.sale try: till = Till.get_current(self.store) except TillError: till = None can_edit = bool(sale and sale.can_edit()) # We need an open till to return sales if sale and till: can_return = sale.can_return() or sale.can_cancel() else: can_return = False self._sale_toolbar.return_sale_button.set_sensitive(can_return) self._sale_toolbar.edit_button.set_sensitive(can_edit)
def _cancel_last_sale(self, last_doc, store): if last_doc.last_sale.status == Sale.STATUS_RETURNED: return sale = store.fetch(last_doc.last_sale) value = sale.total_amount sale.cancel(force=True) comment = _(u"Cancelling last document on ECF") SaleComment(store=store, sale=sale, comment=comment, author=api.get_current_user(store)) till = Till.get_current(store) # TRANSLATORS: cash out = sangria till.add_debit_entry(value, _(u"Cash out: last sale cancelled")) last_doc.last_sale = None info(_("Document was cancelled"))
def _open_till(self, store, initial_cash_amount=0): station = get_current_station(store) last_till = Till.get_last(store) if not last_till or last_till.status != Till.STATUS_OPEN: # Create till and open till = Till(store=store, station=station) till.open_till() till.initial_cash_amount = decimal.Decimal(initial_cash_amount) else: # Error, till already opened assert False
def next_step(self): self.wizard.create_payments = self.create_payments.read() if self.wizard.create_payments: try: till = Till.get_current(self.store) except TillError as e: warning(str(e)) return self if not till: warning( _("You need to open the till before doing this operation.") ) return self return DecreaseItemStep(self.wizard, self, self.store, self.model)
def __init__(self, store, model=None, previous_day=False, close_db=True, close_ecf=True): """ Create a new TillClosingEditor object. :param previous_day: If the till wasn't closed previously """ self._previous_day = previous_day self.till = Till.get_last(store) if close_db: assert self.till self._close_db = close_db self._close_ecf = close_ecf BaseEditor.__init__(self, store, model) self._setup_widgets()
def __init__(self, store, model=None, previous_day=False, close_db=True, close_ecf=True): """ Create a new TillClosingEditor object. :param previous_day: If the till wasn't closed previously """ self._previous_day = previous_day self.till = Till.get_last(store, api.get_current_station(store)) if close_db: assert self.till self._close_db = close_db self._close_ecf = close_ecf self._blind_close = sysparam.get_bool('TILL_BLIND_CLOSING') BaseEditor.__init__(self, store, model) self._setup_widgets()
def get(self): # Retrieve Till data with api.new_store() as store: till = Till.get_last(store) if not till: return None till_data = { 'status': till.status, 'opening_date': till.opening_date.strftime('%Y-%m-%d'), 'closing_date': (till.closing_date.strftime('%Y-%m-%d') if till.closing_date else None), 'initial_cash_amount': str(till.initial_cash_amount), 'final_cash_amount': str(till.final_cash_amount), # Get payments data that will be used on 'close_till' action. 'entry_types': till.status == 'open' and self._get_till_summary(store, till) or [], } return till_data
def _cancel_last_till_entry(self, last_doc, store): last_till_entry = store.fetch(last_doc.last_till_entry) value = last_till_entry.value till = Till.get_current(store) try: self._printer.cancel_last_coupon() if last_till_entry.value > 0: till_entry = till.add_debit_entry( # TRANSLATORS: cash out = sangria, cash in = suprimento value, _(u"Cash out: last cash in cancelled")) self._set_last_till_entry(till_entry, store) else: till_entry = till.add_credit_entry( # TRANSLATORS: cash out = sangria, cash in = suprimento -value, _(u"Cash in: last cash out cancelled")) self._set_last_till_entry(till_entry, store) info(_("Document was cancelled")) except Exception: info(_("Cancelling failed, nothing to cancel")) return
def open_till(self): """Opens the till """ if Till.get_current(self.store) is not None: warning("You already have a till operation opened. " "Close the current Till and open another one.") return False store = api.new_store() try: model = run_dialog(TillOpeningEditor, self._parent, store) except TillError as e: warning(str(e)) model = None retval = store.confirm(model) store.close() if retval: self._till_status_changed(closed=False, blocked=False) return retval
def testSalesPersonReport(self): sysparam(self.store).SALE_PAY_COMMISSION_WHEN_CONFIRMED = 1 salesperson = self.create_sales_person() product = self.create_product(price=100) sellable = product.sellable sale = self.create_sale() sale.salesperson = salesperson sale.add_sellable(sellable, quantity=1) self.create_storable(product, get_current_branch(self.store), stock=100) CommissionSource(sellable=sellable, direct_value=Decimal(10), installments_value=1, store=self.store) sale.order() method = PaymentMethod.get_by_name(self.store, u'money') till = Till.get_last_opened(self.store) method.create_payment(Payment.TYPE_IN, sale.group, sale.branch, sale.get_sale_subtotal(), till=till) sale.confirm() sale.group.pay() salesperson_name = salesperson.person.name commissions = list(self.store.find(CommissionView)) commissions[0].identifier = 1 commissions[1].identifier = 139 self._diff_expected(SalesPersonReport, 'sales-person-report', commissions, salesperson_name)
def _check_needs_closing(self): needs_closing = self.needs_closing() # DB and ECF are ok if needs_closing is CLOSE_TILL_NONE: self._previous_day = False # We still need to check if the till is open or closed. till = Till.get_current(self.store, api.get_current_station(self.store)) self._till_status_changed(closed=not till, blocked=False) return True close_db = needs_closing in (CLOSE_TILL_DB, CLOSE_TILL_BOTH) close_ecf = needs_closing in (CLOSE_TILL_ECF, CLOSE_TILL_BOTH) # DB or ECF is open from a previous day self._till_status_changed(closed=False, blocked=True) self._previous_day = True # Save this statuses in case the user chooses not to close now. self._close_db = close_db self._close_ecf = close_ecf manager = get_plugin_manager() if close_db and (close_ecf or not manager.is_active('ecf')): msg = _("You need to close the till from the previous day before " "creating a new order.\n\nClose the Till?") elif close_db and not close_ecf: msg = _("The till in Stoq is opened, but in ECF " "is closed.\n\nClose the till in Stoq?") elif close_ecf and not close_db: msg = _("The till in stoq is closed, but in ECF " "is opened.\n\nClose the till in ECF?") if yesno(msg, Gtk.ResponseType.NO, _("Close Till"), _("Not now")): return self.close_till(close_db, close_ecf) return False
def on_confirm(self): till = self.model.till # Using api.get_default_store instead of self.store # or it will return self.model.till last_opened = Till.get_last_opened(api.get_default_store()) if (last_opened and last_opened.opening_date.date() == till.opening_date.date()): warning(_("A till was opened earlier this day.")) self.retval = False return try: TillOpenEvent.emit(till=till) except (TillError, DeviceError) as e: warning(str(e)) self.retval = False return value = self.proxy.model.value if value: TillAddCashEvent.emit(till=till, value=value) till_entry = till.add_credit_entry(value, _(u'Initial Cash amount')) _create_transaction(self.store, till_entry)
def create_sale(self, id_=None, branch=None, client=None): from stoqlib.domain.sale import Sale from stoqlib.domain.till import Till till = Till.get_current(self.store) if till is None: till = self.create_till() till.open_till() salesperson = self.create_sales_person() group = self.create_payment_group() if client: group.payer = client.person sale = Sale(coupon_id=0, open_date=TransactionTimestamp(), salesperson=salesperson, branch=branch or get_current_branch(self.store), cfop=sysparam(self.store).DEFAULT_SALES_CFOP, group=group, client=client, store=self.store) if id_: sale.id = id_ sale.identifier = id_ return sale
def close_till(self, close_db=True, close_ecf=True): """Closes the till There are 3 possibilities for parameters combination: * *total close*: Both *close_db* and *close_ecf* are ``True``. The till on both will be closed. * *partial close*: Both *close_db* and *close_ecf* are ``False``. It's more like a till verification. The actual user will do it to check and maybe remove money from till, leaving it ready for the next one. Note that this will not emit 'till-status-changed' event, since the till will not really close. * *fix conflicting status*: *close_db* and *close_ecf* are different. Use this only if you need to fix a conflicting status, like if the DB is open but the ECF is closed, or the other way around. :param close_db: If the till in the DB should be closed :param close_ecf: If the till in the ECF should be closed :returns: True if the till was closed, otherwise False """ is_partial = not close_db and not close_ecf manager = get_plugin_manager() # This behavior is only because of ECF if not is_partial and not self._previous_day: if (manager.is_active('ecf') and not yesno( _("You can only close the till once per day. " "You won't be able to make any more sales today.\n\n" "Close the till?"), Gtk.ResponseType.NO, _("Close Till"), _("Not now"))): return elif not is_partial: # When closing from a previous day, close only what is needed. close_db = self._close_db close_ecf = self._close_ecf if close_db: till = Till.get_last_opened(self.store, api.get_current_station(self.store)) assert till store = api.new_store() editor_class = TillVerifyEditor if is_partial else TillClosingEditor model = run_dialog(editor_class, self._parent, store, previous_day=self._previous_day, close_db=close_db, close_ecf=close_ecf) if not model: store.confirm(model) store.close() return # TillClosingEditor closes the till retval = store.confirm(model) store.close() if retval and not is_partial: self._till_status_changed(closed=True, blocked=False) return retval
def create_till(self): from stoqlib.domain.till import Till station = get_current_station(self.store) return Till(store=self.store, station=station)
def post(self, store): # FIXME: Check branch state and force fail if no override for that product is present. self.ensure_printer() data = request.get_json() client_id = data.get('client_id') products = data['products'] payments = data['payments'] client_category_id = data.get('price_table') document = raw_document(data.get('client_document', '') or '') if document: document = format_document(document) if client_id: client = store.get(Client, client_id) elif document: person = Person.get_by_document(store, document) client = person and person.client else: client = None # Create the sale branch = api.get_current_branch(store) group = PaymentGroup(store=store) user = api.get_current_user(store) sale = Sale( store=store, branch=branch, salesperson=user.person.sales_person, client=client, client_category_id=client_category_id, group=group, open_date=localnow(), coupon_id=None, ) # Add products for p in products: sellable = store.get(Sellable, p['id']) item = sale.add_sellable(sellable, price=currency(p['price']), quantity=decimal.Decimal(p['quantity'])) # XXX: bdil has requested that when there is a special discount, the discount does # not appear on the coupon. Instead, the item wil be sold using the discount price # as the base price. Maybe this should be a parameter somewhere item.base_price = item.price # Add payments sale_total = sale.get_total_sale_amount() money_payment = None payments_total = 0 for p in payments: method_name = p['method'] tef_data = p.get('tef_data', {}) if method_name == 'tef': p['provider'] = tef_data['card_name'] method_name = 'card' method = PaymentMethod.get_by_name(store, method_name) installments = p.get('installments', 1) or 1 due_dates = list(create_date_interval( INTERVALTYPE_MONTH, interval=1, start_date=localnow(), count=installments)) payment_value = currency(p['value']) payments_total += payment_value p_list = method.create_payments( Payment.TYPE_IN, group, branch, payment_value, due_dates) if method.method_name == 'money': # FIXME Frontend should not allow more than one money payment. this can be changed # once https://gitlab.com/stoqtech/private/bdil/issues/75 is fixed? if not money_payment or payment_value > money_payment.value: money_payment = p_list[0] elif method.method_name == 'card': for payment in p_list: card_data = method.operation.get_card_data_by_payment(payment) card_type = p['mode'] # Stoq does not have the voucher comcept, so register it as a debit card. if card_type == 'voucher': card_type = 'debit' device = self._get_card_device(store, 'TEF') provider = self._get_provider(store, p['provider']) if tef_data: card_data.nsu = tef_data['aut_loc_ref'] card_data.auth = tef_data['aut_ext_ref'] card_data.update_card_data(device, provider, card_type, installments) card_data.te.metadata = tef_data # If payments total exceed sale total, we must adjust money payment so that the change is # correctly calculated.. if payments_total > sale_total and money_payment: money_payment.value -= (payments_total - sale_total) assert money_payment.value >= 0 # Confirm the sale group.confirm() sale.order() till = Till.get_last(store) sale.confirm(till) # Fiscal plugins will connect to this event and "do their job" # It's their responsibility to raise an exception in case of # any error, which will then trigger the abort bellow # FIXME: Catch printing errors here and send message to the user. SaleConfirmedRemoteEvent.emit(sale, document) # This will make sure we update any stock or price changes products may # have between sales return True
def test_till_close_more_than_balance(self): station = self.create_station() till = Till(store=self.store, station=station) till.open_till() till.add_debit_entry(currency(20), u"") self.assertRaises(ValueError, till.close_till)
def test_get_cash_amount(self): till = Till(store=self.store, station=self.create_station()) till.open_till() old = till.get_cash_amount() # money operations till.add_credit_entry(currency(10), u"") self.assertEqual(till.get_cash_amount(), old + 10) till.add_debit_entry(currency(5), u"") self.assertEqual(till.get_cash_amount(), old + 5) # non-money operations payment1 = self._create_inpayment() till.add_entry(payment1) self.assertEqual(till.get_cash_amount(), old + 5) payment2 = self._create_outpayment() till.add_entry(payment2) self.assertEqual(till.get_cash_amount(), old + 5) # money payment method operation payment = self.create_payment() payment.due_date = till.opening_date payment.set_pending() TillEntry(description=u'test', value=payment.value, till=till, branch=till.station.branch, payment=payment, store=self.store) payment.pay() self.assertEqual(till.get_cash_amount(), old + 5 + payment.value)
def _createUnclosedTill(self): till = Till(station=get_current_station(self.store), store=self.store) till.open_till() yesterday = localtoday() - datetime.timedelta(1) till.opening_date = yesterday
def current_till(store, example_creator, current_station): return Till.get_current(store, current_station) or example_creator.create_till()
def confirm(self, sale, store, savepoint=None, subtotal=None): """Confirms a |sale| on fiscalprinter and database If the sale is confirmed, the store will be committed for you. There's no need for the callsite to call store.confirm(). If the sale is not confirmed, for instance the user cancelled the sale or there was a problem with the fiscal printer, then the store will be rolled back. :param sale: the |sale| to be confirmed :param trans: a store :param savepoint: if specified, a database savepoint name that will be used to rollback to if the sale was not confirmed. :param subtotal: the total value of all the items in the sale """ # Actually, we are confirming the sale here, so the sale # confirmation process will be available to others applications # like Till and not only to the POS. payments_total = sale.group.get_total_confirmed_value() sale_total = sale.get_total_sale_amount() payment = get_formatted_price(payments_total) amount = get_formatted_price(sale_total) msg = _(u"Payment value (%s) is greater than sale's total (%s). " "Do you want to confirm it anyway?") % (payment, amount) if (sale_total < payments_total and not yesno(msg, gtk.RESPONSE_NO, _(u"Confirm Sale"), _(u"Don't Confirm"))): return False model = run_dialog(ConfirmSaleWizard, self._parent, store, sale, subtotal=subtotal, total_paid=payments_total) if not model: CancelPendingPaymentsEvent.emit() store.rollback(name=savepoint, close=False) return False if sale.client and not self.is_customer_identified(): self.identify_customer(sale.client.person) if not self.totalize(sale): store.rollback(name=savepoint, close=False) return False if not self.setup_payments(sale): store.rollback(name=savepoint, close=False) return False if not self.close(sale, store): store.rollback(name=savepoint, close=False) return False # FIXME: This used to be done inside sale.confirm. Maybe it would be # better to do a proper error handling till = Till.get_current(store) assert till try: sale.confirm(till=till) except SellError as err: warning(str(err)) store.rollback(name=savepoint, close=False) return False if not self.print_receipts(sale): warning(_("The sale was cancelled")) sale.cancel(force=True) print_cheques_for_payment_group(store, sale.group) # Only finish the transaction after everything passed above. store.confirm(model) # Try to print only after the transaction is commited, to prevent # losing data if something fails while printing group = sale.group booklets = list(group.get_payments_by_method_name(u'store_credit')) bills = list(group.get_payments_by_method_name(u'bill')) if (booklets and yesno( _("Do you want to print the booklets for this sale?"), gtk.RESPONSE_YES, _("Print booklets"), _("Don't print"))): try: print_report(BookletReport, booklets) except ReportError: warning(_("Could not print booklets")) if (bills and BillReport.check_printable(bills) and yesno( _("Do you want to print the bills for this sale?"), gtk.RESPONSE_YES, _("Print bills"), _("Don't print"))): try: print_report(BillReport, bills) except ReportError: # TRANSLATORS: bills here refers to "boletos" in pt_BR warning(_("Could not print bills")) return True
def test_till_open_previously_opened(self): yesterday = localnow() - datetime.timedelta(1) # Open a till, set the opening_date to yesterday till = Till(station=get_current_station(self.store), store=self.store) till.open_till() till.opening_date = yesterday till.add_credit_entry(currency(10), u"") till.close_till() till.closing_date = yesterday new_till = Till(station=get_current_station(self.store), store=self.store) self.assertTrue(new_till._get_last_closed_till()) new_till.open_till() self.assertEqual(new_till.initial_cash_amount, till.final_cash_amount)
def test_needs_closing(self): till = Till(station=self.create_station(), store=self.store) self.assertFalse(till.needs_closing()) # Till is opened today, no need to close till.open_till() self.assertFalse(till.needs_closing()) # till was onpened yesterday. Should close till.opening_date = localnow() - datetime.timedelta(1) self.assertTrue(till.needs_closing()) # Till was opened yesterday, but there is a tolerance tolerance = int((localnow() - localtoday()).seconds / (60 * 60)) + 1 with self.sysparam(TILL_TOLERANCE_FOR_CLOSING=tolerance): self.assertFalse(till.needs_closing()) # Till is now closed, no need to close again till.close_till() self.assertFalse(till.needs_closing())
def create_model(self, store): till = Till.get_current(store, api.get_current_station(store)) return Settable(value=currency(0), reason=u'', till=till, balance=till.get_balance())
def test_get_last_closed(self): till = Till(store=self.store, station=get_current_station(self.store)) till.open_till() till.close_till() self.assertEqual(Till.get_last_closed(self.store), till)
def create_payment(self, payment_type, payment_group, branch, value, due_date=None, description=None, base_value=None, till=ValueUnset, payment_number=None): """Creates a new payment according to a payment method interface :param payment_type: the kind of payment, in or out :param payment_group: a :class:`PaymentGroup` subclass :param branch: the :class:`branch <stoqlib.domain.person.Branch>` associated with the payment, for incoming payments this is the branch receiving the payment and for outgoing payments this is the branch sending the payment. :param value: value of payment :param due_date: optional, due date of payment :param details: optional :param description: optional, description of the payment :param base_value: optional :param till: optional :param payment_number: optional :returns: a :class:`payment <stoqlib.domain.payment.Payment>` """ store = self.store if due_date is None: due_date = TransactionTimestamp() if payment_type == Payment.TYPE_IN: query = And(Payment.group_id == payment_group.id, Payment.method_id == self.id, Payment.payment_type == Payment.TYPE_IN, Payment.status != Payment.STATUS_CANCELLED) payment_count = store.find(Payment, query).count() if payment_count == self.max_installments: raise PaymentMethodError( _('You can not create more inpayments for this payment ' 'group since the maximum allowed for this payment ' 'method is %d') % self.max_installments) elif payment_count > self.max_installments: raise DatabaseInconsistency( _('You have more inpayments in database than the maximum ' 'allowed for this payment method')) if not description: description = self.describe_payment(payment_group) # If till is unset, do some clever guessing if till is ValueUnset: # We only need a till for inpayments if payment_type == Payment.TYPE_IN: till = Till.get_current(store) elif payment_type == Payment.TYPE_OUT: till = None else: raise AssertionError(payment_type) payment = Payment(store=store, branch=branch, payment_type=payment_type, due_date=due_date, value=value, base_value=base_value, group=payment_group, method=self, category=None, till=till, description=description, payment_number=payment_number) self.operation.payment_create(payment) return payment