AllcountJS: POS-System (Point of Sale) erstellen

  • Tutorial
Wir sind weiterhin mit AllcountJS vertraut , einem Framework für die schnelle Entwicklung von Anwendungen auf der NodeJS-Plattform. In diesem Artikel werden wir uns ein Beispiel für die Implementierung einer benutzerdefinierten Schnittstelle mit AngualrJS und Jade sowie einige Konfigurationsoptionen ansehen, die wir noch nicht erwähnt haben.

POS (Point of Sale) - im wahrsten Sinne des Wortes der Point of Sale (Verkaufsort), aber normalerweise bezieht sich dieser Begriff auf den Arbeitsplatz des Kassierers zusammen mit dem Handelsgerät. Solche Terminals befinden sich an fast jedem Ort, an dem sie uns etwas verkaufen. Und jetzt erstellen wir eine einfache Anwendung, mit der Sie eine Liste von Produkten mit Salden verwalten und Verkaufsunterlagen erstellen können.

POS-Hauptbenutzeroberfläche

Das Ergebnis kann wie gewohnt in der Demo-Galerie angesehen werden .

Modell- und Geschäftslogik


Beginnen wir mit einer Beschreibung des Wichtigsten - der Positionen (Waren), die wir verkaufen werden.
Item: {
    fields: {
        name: Fields.text("Name"),
                stock: Fields.integer("Stock").computed('sum(transactions.quantity)'),
                price: Fields.money("Price"),
                transactions: Fields.relation("Transactions", "Transaction", "item")
    },
    referenceName: "name"
}

Das Interessanteste ist hier das Feld mit den Bestandsresten. Es sollte automatisch nach Menge aus Transaktionen gezählt werden. Genau das steht in unserer Konfiguration: Der ganzzahlige Feldbestand wird als Summe der Transaktionen für das Mengenfeld berechnet. Nur mit Klammern, Punkten und Anführungszeichen. Das Feld "Transaktionen" enthält eine Liste aller Transaktionen, an denen diese Position beteiligt ist.

Wenn Sie nach Konfiguration lesen: "Transaktionen" ist ein Feld vom Typ " Beziehung ", das der fehlenden Entität "Transaktion" im Feld "Position" zugeordnet ist. Die Eigenschaft "referenceName" bestimmt, welche der Entitätsfelder in den Namen der Links angezeigt werden, die zu dieser Entität führen.

Nun fügen wir den Transaktionstyp hinzu - Transaktionen, die den Eingang und die Entsorgung von Waren beschreiben.
Transaction: {
    fields: {
        item: Fields.reference("Item", "Item"),
        order: Fields.reference("Order", "Order"),
        orderItem: Fields.reference("Order item", "OrderItem"),
        quantity: Fields.integer("Quantity")
    },
    showInGrid: ['item', 'order', 'quantity']
} 

Wie Sie sehen, bezieht sich das Feld "Artikel" auf die Entität "Artikel" und unterstützt die obige Beziehung. Andernfalls gibt es nichts Besonderes außer der Eigenschaft "showInGrid" - sie legt die Liste der Felder fest, die im Raster angezeigt werden.

Als nächstes beschreiben wir den zentralen Teil unseres POS:
Order: {
    fields: {
        number: Fields.integer("Order #"),
        date: Fields.date("Date"),
        total: Fields.money("Total").computed('sum(orderItems.finalPrice)'),
        orderItems: Fields.relation("Items", "OrderItem", "order")
    },
    beforeSave: function (Entity, Dates, Crud) {
        if (!Entity.date) {
            Entity.date = Dates.nowDate();
        }
        return Crud.crudFor('OrderCounter').find({}).then(function (last) {
            if (!Entity.number) {
                Entity.number = last[0].number;
                return Crud.crudFor('OrderCounter').updateEntity({id: last[0].id, number: last[0].number + 1});
            }
        })
    },
    beforeDelete: function (Entity, Crud, Q) {
        var crud = Crud.crudFor('OrderItem');
        return crud.find({filtering: {order: Entity.id}}).then(function (items) {
            return Q.all(items.map(function (i) {
                return crud.deleteEntity(i.id)
            }));
        });
    },
    referenceName: "number",
    views: {
        PointOfSale: {
            customView: 'pos'
        }
    }
}

Ich nenne es zentral, weil wir darin unsere Hauptansicht "PointOfSale" definiert haben, die in der Datei "pos.jade" beschrieben ist, aber wir werden später darauf eingehen. Wahrscheinlich haben Sie auch die Funktionen "beforeSave" und "beforeDelete" bemerkt. Hierbei handelt es sich um Handler, die beim Eintreten entsprechender Ereignisse ausgelöst werden: vor dem Speichern und vor dem Löschen. Weitere Informationen hierzu finden Sie in unserer Dokumentation im Abschnitt CRUD-Hooks. Hier werden diese Funktionen benötigt, um den Bestellzähler (Bestellung) nach Abschluss der Bestellung zu aktualisieren und Bestellpositionen zusammen mit der Bestellung selbst zu löschen.

Berechnete Felder und Beziehungsfelder befinden sich in diesem Codeteil. Das berechnete Feld wird verwendet, um die Gesamtkosten der Bestellung zu berechnen, und das Beziehungsfeld "orderItems" kann als Liste der in dieser Bestellung enthaltenen Artikel interpretiert werden. Es ist der Entität OrderItem zugeordnet:
OrderItem: {
    fields: {
        order: Fields.reference("Order", "Order"),
        item: Fields.fixedReference("Item", "Item").required(),
        quantity: Fields.integer("Quantity").required(),
        finalPrice: Fields.money("Final price").readOnly().addToTotalRow()
    },
    showInGrid: ['item', 'quantity', 'finalPrice'],
    beforeSave: function (Crud, Entity) {
        return Crud.crudFor('Item').readEntity(Entity.item.id).then(function (item) {
            Entity.finalPrice = Entity.quantity * item.price;
        })
    },
    afterSave: function (Crud, Entity) {
        var crud = Crud.crudForEntityType('Transaction');
        return removeTransaction(Crud, Entity).then(function () {
            return crud.createEntity({
                order: Entity.order,
                orderItem: {id: Entity.id},
                item: Entity.item,
                quantity: Entity.quantity * -1
            })
        })
    },
    beforeDelete: function (Crud, Entity) {
        return removeTransaction(Crud, Entity);
    }
}

Wie Sie wahrscheinlich bemerkt haben, hat jeder Entitätstyp, der an Feldern mit Beziehungen teilnimmt, die Eigenschaft "showInGrid". Im Raster müssen nur bestimmte Felder der Entität angezeigt werden. Normalerweise möchten wir dem Benutzer keine übergeordnete Entität in Bezug auf den Benutzer zeigen, da er diese Präsentation über sie eingegeben hat. Das kann man natürlich ändern.

Achten Sie auf das endgültige Preisfeld - indem Sie .addToTotalRow () aufrufen, haben wir es in der letzten Zeile zur Summierung markiert.

Es gibt auch mehrere CRUD-Hooks: Vor dem Speichern aktualisieren wir den Gesamtbetrag der Auftragsposition in Abhängigkeit von der Menge. Nach dem Speichern löschen wir ihn, erstellen eine neue Transaktion neu und löschen die Transaktion auch, wenn wir die Auftragsposition löschen. Nach dem DRY- Prinzipverwenden wir die Funktion, um Transaktionen zu löschen:
function removeTransaction(Crud, Entity) {
    var crud = Crud.crudForEntityType('Transaction');
    return crud.find({filtering: {orderItem: Entity.id}}).then(function (transactions) {
        if (transactions.length) {
            return crud.deleteEntity(transactions[0].id);
        }
    });
}

Und für die fortlaufende Nummerierung von Bestellungen haben wir einen Bestellzähler:
OrderCounter: {
    fields: {
        number: Fields.integer("Counter")
    }
}

Primäre POS-Schnittstelle


Denken Sie daran, wie Sie mit überlasteten Schnittstellen zu kämpfen hatten. Und da Sie wahrscheinlich glücklich waren, nur ein paar große farbige Knöpfe zu drücken, die die ganze Arbeit erledigen. Unsere Anpassungsmöglichkeiten machen die Benutzeroberfläche einfach und unkompliziert.
Wir haben oben erwähnt, dass die Ansicht „PointOfSale“ für die Anwendung von zentraler Bedeutung ist. Die Hauptarbeit des Benutzers mit der Anwendung findet darin statt - Erstellen von Aufträgen. Es ist in der pos.jade-Datei festgelegt:
pos.jade
extends main
include mixins
block vars
    - var hasToolbar = false
block content
    div(ng-app='allcount', ng-controller='EntityViewController')
        +defaultList()
        .container.screen-container(ng-cloak)
            .row(ng-controller="PosController")
                .col-md-8
                    .items-bar.row.btn-toolbar(lc-list="'Item'", paging="{}")
                        .col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items")
                            button.btn.btn-lg.btn-block.btn-default(ng-click="addItem(item)")
                                p {{item.name}}
                                p {{(item.price / 100) | currency}}
                    .container-fluid
                        h1 Total: {{viewState.editForm.entity().total/100 | currency}}
                    .row.btn-toolbar
                        .col-md-4
                            button.btn.btn-lg.btn-danger.btn-block(ng-click="deleteEntity()", ng-disabled="!viewState.formEntityId") Cancel
                        .col-md-4(ng-hide='viewState.isFormEditing')
                            +startFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg
                        .col-md-4(ng-show='viewState.isFormEditing')
                            +doneFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg
                        .col-md-4
                            button.btn.btn-lg.btn-success.btn-block(ng-click="viewState.mode = 'list'; viewState.formEntityId = undefined", ng-disabled="!viewState.formEntityId") Finish
                .col-md-4
                    +defaultEditForm()(ng-show="true")
                        +defaultFormTemplate()
block js
    +entityJs()
    script.
        angular.module('allcount').controller('PosController', ['$scope', 'lcApi', '$q', function ($scope, lcApi, $q) {
            $scope.addItem = function (item) {
                var promise;
                if (!$scope.viewState.formEntityId) {
                    promise = lcApi.createEntity({entityTypeId: 'Order'}, {}).then(function (orderId) {
                        $scope.navigateTo(orderId)
                        return orderId;
                    })
                } else {
                    promise = $q.when($scope.viewState.formEntityId);
                }
                promise.then(function (orderId) {
                    return lcApi.findRange({entityTypeId: 'OrderItem'}, {filtering: {order: orderId}}).then(function (items) {
                        var existingOrderItem = _.find(items, function (i) {
                            return i.item.id === item.id;
                        })
                        return (existingOrderItem ?
                                        lcApi.updateEntity({entityTypeId: 'OrderItem'}, {
                                            id: existingOrderItem.id,
                                            quantity: 1 + existingOrderItem.quantity
                                        }) :
                                        lcApi.createEntity({entityTypeId: 'OrderItem'}, {
                                            order: {id: orderId},
                                            item: item,
                                            quantity: 1
                                        })
                        ).then(function () {
                                    return $scope.editForm.reloadEntity();
                                })
                    })
                })
            }
        }])
    style.
        .items-bar .btn-block {
            margin-bottom: 10px;
        }


Lassen Sie uns herausfinden, was im Inneren passiert.

Zu Beginn sehen wir diese beiden Zeilen:
    extends main
    include mixins

"Main" und "Mixins" sind integrierte Vorlagen. Das erste ist das Fundament des Markups, das einfach obligatorisch ist, solange Sie nichts völlig Ungewöhnliches tun möchten. Der zweite bietet Ihnen unabhängige Jade-Schnipsel für Schnittstellenelemente, wie z. B. Tabellen, Tabellenzeilen, Felder, Keller, Beschriftungen usw.

Ein paar Worte zu den Blöcken innerhalb des "main": Es gibt Blöcke "vars", "content" und "js", deren Namen im Wesentlichen für sich sprechen:
"vars" befindet sich ganz oben im
"head" - innerhalb der Überschrift
"content" Der Teil des Hauptbereichs der Seite
"js" ist der letzte im Hauptteil der Seite.

Das Flag "hasToolbar" ist eine Variable, die festlegt, ob unter der Navigationsleiste ein doppelter Einzug eingefügt werden soll.
Als nächstes werden wir unsere Haupt-UI-Anwendung beschreiben:
  div(ng-app='allcount', ng-controller='EntityViewController') 

Bitte beachten Sie, dass wir "allcount" als Namen für die Anwendung verwenden müssen. Dies ist erforderlich, um auf die verschiedenen Funktionen von AllacountJS Angular wie Steuerungen und Anweisungen zugreifen zu können.

"+ DefaultList ()" fügt die "lc-list" -Komponente hinzu, die wir zum Anzeigen von Schnittstellenelementen vom Listentyp verwenden können. Diese „Liste“ befindet sich jedoch im Formularstatus und wird nicht als Liste angezeigt. Sie enthält jedoch die Schaltfläche „Bearbeiten“, die zum Bearbeiten der aktuellen Reihenfolge erforderlich ist.
       .container.screen-container(ng-cloak)
       .row(ng-controller="PosController")

Hier haben wir einen Container mit einem POS-Controller, den wir später beschreiben werden. Und dann haben wir zwei Spalten:
        .col-md-8
           ...
         .col-md-4     
           …

In der ersten (großen) Spalte wird eine Liste der verfügbaren Elemente angezeigt:
 .items-bar.row.btn-toolbar(lc-list="'Item'", paging="{}")
             .col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items")
               button.btn.btn-lg.btn-block.btn-default(ng-click="addItem(item)")
                 p {{item.name}}
                 p {{(item.price / 100) | currency}}

Das Attribut lc-list = "'Item'" ist eine Anweisung und wird benötigt, um eine Liste der Elemente abzurufen. Das Attribut paging = {} gibt an, dass hier kein Paging erforderlich ist.

Innere div:
 .col-lg-4.col-md-6.col-xs-12(ng-repeat="item in items")

Durchläuft Positionen mit der Anweisung „ng-repeat“ und zeigt große Schaltflächen für eine bestimmte Position an.
Artikel in Artikeln
Unter der Liste der Positionen befindet sich der Gesamtbetrag in Benutzerwährung:
           .container-fluid
             h1 Total: {{viewState.editForm.entity().total/100 | currency}}

Und eine Zeile mit Knöpfen:
    .row.btn-toolbar
             .col-md-4
               button.btn.btn-lg.btn-danger.btn-block(ng-click="deleteEntity()", ng-disabled="!viewState.formEntityId") Cancel
             .col-md-4(ng-hide='viewState.isFormEditing')
               +startFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg
             .col-md-4(ng-show='viewState.isFormEditing')  
               +doneFormEditingButton()(ng-disabled="!viewState.formEntityId").btn-block.btn-lg
             .col-md-4  
               button.btn.btn-lg.btn-success.btn-block(ng-click="viewState.mode = 'list'; viewState.formEntityId = undefined", ng-disabled="!viewState.formEntityId") Finish

Symbolleiste "Summe" und "Schaltflächen"
Wir haben auch einen Ausschnitt aus der Vorlage im Formular für die aktuelle Bestellung in der rechten Spalte:
             +defaultEditForm()(ng-show="true")
             +defaultFormTemplate()

Bearbeiten und Formularvorlage
Am Ende haben wir so einen Block mit dem Code:
    block js
      +entityJs()
      script.
        angular.module('allcount').controller('PosController', ['$scope', 'lcApi', '$q', function ($scope, lcApi, $q) {
          $scope.addItem = function (item) {
            var promise;
            if (!$scope.viewState.formEntityId) {
              promise = lcApi.createEntity({entityTypeId: 'Order'}, {}).then(function (orderId) {
                $scope.navigateTo(orderId)
                return orderId;
              })
            } else {
              promise = $q.when($scope.viewState.formEntityId);
            }
            promise.then(function (orderId) {
              return lcApi.findRange({entityTypeId: 'OrderItem'}, {filtering: {order: orderId}}).then(function (items) {
                var existingOrderItem = _.find(items, function (i) {
                  return i.item.id === item.id;
                })
                return (existingOrderItem ? 
                  lcApi.updateEntity({entityTypeId: 'OrderItem'}, {id: existingOrderItem.id, quantity: 1 + existingOrderItem.quantity}) : 
                  lcApi.createEntity({entityTypeId: 'OrderItem'}, {order: {id: orderId}, item: item, quantity: 1})
                  ).then(function () {
                    return $scope.editForm.reloadEntity();
                  })
              })
            })
          }
        }])
      style.
        .items-bar .btn-block {
          margin-bottom: 10px;
        }

Darin setzen wir unseren Hauptcontroller, der dem POS Leben einhauchen wird. Zum größten Teil teilt er unserer Vision mit, wie einer Bestellung neue Werbebuchungen hinzugefügt werden. Grundsätzlich wird hier der Schlüsselmechanismus AllcountJS für AngularJS verwendet - der Anbieter „lcApi“. Mit dessen Hilfe erfolgt eine Suche, Erstellung und Änderung des Auftrages und der Auftragspositionen.

Total


Was wir getan haben: das Modell, die Geschäftslogik und die benutzerfreundliche Oberfläche beschrieben.
Nun füge alles zusammen. Die endgültige Konfigurationsdatei sieht folgendermaßen aus (aus dem Menü und den darin nicht erwähnten Beispieldaten):
app.js
A.app({
    appName: "POS and inventory",
    appIcon: "calculator",
    onlyAuthenticated: true,
    menuItems: [
        {
            name: "Transactions",
            entityTypeId: "Transaction",
            icon: "send-o"
        }, {
            name: "Items",
            entityTypeId: "Item",
            icon: "cubes"
        }, {
            name: "Orders",
            entityTypeId: "Order",
            icon: "shopping-cart"
        },
        {
            name: "POS",
            entityTypeId: "PointOfSale",
            icon: "calculator"
        }
    ],
    entities: function (Fields) {
        return {
            Transaction: {
                fields: {
                    item: Fields.reference("Item", "Item"),
                    order: Fields.reference("Order", "Order"),
                    orderItem: Fields.reference("Order item", "OrderItem"),
                    quantity: Fields.integer("Quantity")
                },
                showInGrid: ['item', 'order', 'quantity']
            },
            Item: {
                fields: {
                    name: Fields.text("Name"),
                    stock: Fields.integer("Stock").computed('sum(transactions.quantity)'),
                    price: Fields.money("Price"),
                    transactions: Fields.relation("Transactions", "Transaction", "item")
                },
                referenceName: "name"
            },
            Order: {
                fields: {
                    number: Fields.integer("Order #"),
                    date: Fields.date("Date"),
                    total: Fields.money("Total").computed('sum(orderItems.finalPrice)'),
                    orderItems: Fields.relation("Items", "OrderItem", "order")
                },
                beforeSave: function (Entity, Dates, Crud) {
                    if (!Entity.date) {
                        Entity.date = Dates.nowDate();
                    }
                    return Crud.crudFor('OrderCounter').find({}).then(function (last) {
                        if (!Entity.number) {
                            Entity.number = last[0].number;
                            return Crud.crudFor('OrderCounter').updateEntity({
                                id: last[0].id,
                                number: last[0].number + 1
                            });
                        }
                    })
                },
                beforeDelete: function (Entity, Crud, Q) {
                    var crud = Crud.crudFor('OrderItem');
                    return crud.find({filtering: {order: Entity.id}}).then(function (items) {
                        return Q.all(items.map(function (i) {
                            return crud.deleteEntity(i.id)
                        }));
                    });
                },
                referenceName: "number",
                views: {
                    PointOfSale: {
                        customView: 'pos'
                    }
                }
            },
            OrderItem: {
                fields: {
                    order: Fields.reference("Order", "Order"),
                    item: Fields.fixedReference("Item", "Item").required(),
                    quantity: Fields.integer("Quantity").required(),
                    finalPrice: Fields.money("Final price").readOnly().addToTotalRow()
                },
                showInGrid: ['item', 'quantity', 'finalPrice'],
                beforeSave: function (Crud, Entity) {
                    return Crud.crudFor('Item').readEntity(Entity.item.id).then(function (item) {
                        Entity.finalPrice = Entity.quantity * item.price;
                    })
                },
                afterSave: function (Crud, Entity) {
                    var crud = Crud.crudForEntityType('Transaction');
                    return removeTransaction(Crud, Entity).then(function () {
                        return crud.createEntity({
                            order: Entity.order,
                            orderItem: {id: Entity.id},
                            item: Entity.item,
                            quantity: Entity.quantity * -1
                        })
                    })
                },
                beforeDelete: function (Crud, Entity) {
                    return removeTransaction(Crud, Entity);
                }
            },
            OrderCounter: {
                fields: {
                    number: Fields.integer("Counter")
                }
            },
        }
    },
    migrations: function (Migrations) {
        return [
            {
                name: "demo-records-1",
                operation: Migrations.insert("Item", [
                    {id: "1", name: "Snickers", price: 299},
                    {id: "2", name: "Coffee", price: 199},
                    {id: "3", name: "Tea", price: 99}
                ])
            },
            {
                name: "demo-records-2",
                operation: Migrations.insert("Transaction", [
                    {id: "1", item: {id: "1"}, quantity: "50"},
                    {id: "2", item: {id: "2"}, quantity: "100"},
                    {id: "3", item: {id: "3"}, quantity: "200"}
                ])
            },
            {
                name: "order-counter",
                operation: Migrations.insert("OrderCounter", [
                    {id: "2", number: 1}
                ])
            }
        ]
    }
});
function removeTransaction(Crud, Entity) {
    var crud = Crud.crudForEntityType('Transaction');
    return crud.find({filtering: {orderItem: Entity.id}}).then(function (transactions) {
        if (transactions.length) {
            return crud.deleteEntity(transactions[0].id);
        }
    });
}


Ich stelle fest, dass wenn Ihre Browsersprache Englisch ist, die Preise in Dollar angezeigt werden und wenn Russisch - in Rubel.

Starten Sie


Wenn Sie bereits mit AllcountJS vertraut sind, können Sie diesen Code problemlos ausführen. Im Übrigen beschreibe ich kurz, wie das geht:
  • Erstellen Sie ein neues Verzeichnis
  • Erstellen Sie die Datei app.js und fügen Sie den obigen Code ein
  • Legen Sie als nächstes die Datei pos.jade ab
  • Installieren Sie allcountjs-cli (wie im vorherigen Artikel oder auf der offiziellen Website beschrieben)
  • Starten Sie den Server allcountjs -c app.js (während der MongoDB-Dienst bereits ausgeführt werden sollte)
  • Die Anwendung ist unter http: // localhost: 9080 verfügbar


Ein echter POS?


Deshalb haben wir eine einfache Anwendung für das POS-Terminal erstellt. Wie kann es zum Arbeiter gemacht werden? Sie müssen ein Analogon der PointOfSale-Ansicht für Ihr Mobilgerät schreiben und die Webanwendung mit ionic auf Ihr Mobiltelefon packen und dann die Integration mit einem der mobilen Erfassungsdienste (z. B. 2can oder iBox) vornehmen. Jetzt verfügen Sie über ein echtes POS-Terminal.

Ich hoffe du warst interessiert. Für Rückmeldungen sind wir dankbar. Hier oder in gitter und gitter ru .

Jetzt auch beliebt: