summaryrefslogtreecommitdiff
path: root/invoice
diff options
context:
space:
mode:
authorVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-11-11 21:22:31 +0530
committerVidhu Kant Sharma <vidhukant@vidhukant.xyz>2022-11-11 21:22:31 +0530
commit2f4a92b0f1d02096427a2d1c97746bb52cdcc38a (patch)
treec27d44f8746316d158bd77c0796462a024efce2e /invoice
parentd43b356fc302d61728a08f08dbdf474906e3fa82 (diff)
Merged OpenBills-lib code into OpenBills-server
Diffstat (limited to 'invoice')
-rw-r--r--invoice/db_actions.go119
-rw-r--r--invoice/invoice.go114
-rw-r--r--invoice/router.go (renamed from invoice/invoice_router.go)52
3 files changed, 278 insertions, 7 deletions
diff --git a/invoice/db_actions.go b/invoice/db_actions.go
new file mode 100644
index 0000000..a4880fa
--- /dev/null
+++ b/invoice/db_actions.go
@@ -0,0 +1,119 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package invoice
+
+import (
+ "context"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+// add invoice to db
+func saveInvoice(i Invoice) (primitive.ObjectID, error) {
+ res, err := db.Collection("Invoices").InsertOne(context.TODO(), i)
+ return res.InsertedID.(primitive.ObjectID), err
+}
+
+// add transporter to db
+func saveTransporter(t Transporter) (primitive.ObjectID, error) {
+ res, err := db.Collection("Transporters").InsertOne(context.TODO(), t)
+ return res.InsertedID.(primitive.ObjectID), err
+}
+
+// add transport vehicle to db
+func saveTransport(t *Transport) (primitive.ObjectID, error) {
+ res, err := db.Collection("Transports").InsertOne(context.TODO(), t)
+ return res.InsertedID.(primitive.ObjectID), err
+}
+
+// Delete invoice from DB
+func deleteInvoice(id primitive.ObjectID) error {
+ _, err := db.Collection("Invoices").DeleteOne(context.TODO(), bson.M{"_id": id})
+ return err
+}
+
+// Delete transporter from DB
+func deleteTransporter(id primitive.ObjectID) error {
+ _, err := db.Collection("Transporters").DeleteOne(context.TODO(), bson.M{"_id": id})
+ return err
+}
+
+// Delete transport vehicle from DB
+func deleteTransport(id primitive.ObjectID) error {
+ _, err := db.Collection("Transports").DeleteOne(context.TODO(), bson.M{"_id": id})
+ return err
+}
+
+// modify invoice in DB
+func modifyInvoice(id primitive.ObjectID, ni Invoice) error {
+ _, err := db.Collection("Invoices").UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", ni}})
+ return err
+}
+
+// modify transporter in DB
+func modifyTransporter(id primitive.ObjectID, nt Transporter) error {
+ _, err := db.Collection("Transporters").UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", nt}})
+ return err
+}
+
+// modify transport in DB
+func modifyTransport(id primitive.ObjectID, nt Transport) error {
+ _, err := db.Collection("Transports").UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", nt}})
+ return err
+}
+
+/* GetInvoices queries the database and
+ * returns invoices based on the given filter
+ * if filter is nil every invoice is returned
+ */
+func getInvoices(filter bson.M) ([]Invoice, error) {
+ var invoices []Invoice
+
+ cursor, err := db.Collection("Invoices").Find(context.TODO(), filter)
+ if err != nil {
+ return invoices, err
+ }
+
+ err = cursor.All(context.TODO(), &invoices)
+ return invoices, err
+}
+
+func getTransporters(filter bson.M) ([]Transporter, error) {
+ var transporters []Transporter
+
+ cursor, err := db.Collection("Transporters").Find(context.TODO(), filter)
+ if err != nil {
+ return transporters, err
+ }
+
+ err = cursor.All(context.TODO(), &transporters)
+ return transporters, err
+}
+
+func getTransports(filter bson.M) ([]Transport, error) {
+ var transports []Transport
+
+ cursor, err := db.Collection("Transports").Find(context.TODO(), filter)
+ if err != nil {
+ return transports, err
+ }
+
+ err = cursor.All(context.TODO(), &transports)
+ return transports, err
+}
diff --git a/invoice/invoice.go b/invoice/invoice.go
new file mode 100644
index 0000000..d195ea3
--- /dev/null
+++ b/invoice/invoice.go
@@ -0,0 +1,114 @@
+/* OpenBills-server - Server for libre billing software OpenBills-web
+ * Copyright (C) 2022 Vidhu Kant Sharma <vidhukant@vidhukant.xyz>
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package invoice
+
+import (
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+ "github.com/MikunoNaka/OpenBills-server/client"
+ "github.com/MikunoNaka/OpenBills-server/item"
+ "github.com/MikunoNaka/OpenBills-server/database"
+ "time"
+)
+
+// initialise a database connection for this package
+// not sure if I should do this but I am...
+var db *mongo.Database = database.DB
+
+/* you should be able to:
+ * - add, modify, delete an invoice
+ * - add client to invoice
+ * - add items to invoice
+ */
+
+/* Transporter details can be stored in
+ * the DB. That is decided by the frontend.
+ * You can optionally store Transporter
+ * and Transport details which are often used
+ */
+type Transporter struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"`
+ Name string `bson:"Name" json:"Name"`
+ GSTIN string `bson:"GSTIN" json:"GSTIN"`
+ // Issued ID for the transporter if any
+ TransporterId string `bson:"TransporterId,omitempty" json:"TransporterId"`
+}
+
+// transport vehicle details
+type Transport struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"`
+ Transporter Transporter `bson:"Transporter,omitempty" json:"Transporter"`
+ VehicleNum string `bson:"VehicleNum" json:"VehicleNum"`
+ Note string `bson:"Note" json:"Note"`
+ TransportMethod string `bson:"TransportMethod" json:"TransportMethod"`
+}
+
+/* The *legendary* Invoice struct
+ * Each Recipient, Item in invoice, Address
+ * every detail that can change in the future is
+ * saved in the database so even if values change
+ * the invoice will have the old details
+ *
+ * The _id of the items/recipients will also be stored
+ * so user can look at the new values of those fields
+ * if needed. This system is better because if
+ * item is deleted from the Db it won't mess
+ * up the invoice collection
+ *
+ * Things like IGST, CGST, Discount, Quantity, etc
+ * should be calculated on runtime.
+ *
+ * usually an invoice would store the currency
+ * for payment. OpenBills does NOT support
+ * international billing. The Db will hold the config
+ * for the default currency, etc.
+ */
+// TODO: add place of supply
+type Invoice struct {
+ Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"` // not the same as invoice number
+ InvoiceNumber int `bson:"InvoiceNumber" json:"InvoiceNumber"`
+ CreatedAt time.Time `bson:"CreatedAt" json:"CreatedAt"`
+ LastUpdated time.Time `bson:"LastUpdated,omitempty" json:"LastUpdated"`
+ Recipient client.Client `bson:"Recipient" json:"Recipient"`
+ Paid bool `bson:"Paid" json:"Paid"`
+ TransactionId string `bson:"TransactionId" json:"TransactionId"`
+ Transport Transport `bson:"Transport" json:"Transport"`
+ // user can apply a discount on the whole invoice
+ // TODO: float64 isn't the best for this
+ DiscountPercentage float64 `bson:"DiscountPercentage" json:"DiscountPercentage"`
+ /* client may have multiple shipping
+ * addresses but invoice only has one.
+ * Empty ShippingAddress means shipping
+ * address same as billing address
+ */
+ BillingAddress client.Address `bson:"BillingAddress" json:"BillingAddress"`
+ ShippingAddress client.Address `bson:"ShippingAddress,omitempty" json:"ShippingAddress"`
+ Items []item.InvoiceItem `bson:"Items" json:"Items"`
+ // user can attach notes to the invoice
+ // frontend decides if recipient sees this or not
+ Note string `bson:"Note" json:"Note"`
+
+ /* Invoices can be drafts
+ * I personally like this functionality
+ * because we can constantly save the
+ * invoice to the DB as a draft
+ * and if OpenBills crashes or is disconnected
+ * we still have the progress
+ */
+ Draft bool `bson:"Draft" json:"Draft"`
+}
diff --git a/invoice/invoice_router.go b/invoice/router.go
index 88c6308..4e33e18 100644
--- a/invoice/invoice_router.go
+++ b/invoice/router.go
@@ -19,10 +19,11 @@ package invoice
import (
"github.com/gin-gonic/gin"
- "github.com/MikunoNaka/OpenBills-lib/invoice"
"log"
"net/http"
+ "strconv"
"go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/bson"
)
func Routes(route *gin.Engine) {
@@ -30,7 +31,7 @@ func Routes(route *gin.Engine) {
{
i.GET("/all", func(ctx *gin.Context) {
// TODO: add functionality to filter results
- invoices, err := invoice.GetInvoices(nil)
+ invoices, err := getInvoices(nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to read invoices from DB: %v\n", err.Error())
@@ -40,6 +41,43 @@ func Routes(route *gin.Engine) {
ctx.JSON(http.StatusOK, invoices)
})
+ // preview invoice
+ i.GET("/preview/:invoiceNumber", func(ctx *gin.Context) {
+ num := ctx.Param("invoiceNumber")
+ numInt, _ := strconv.Atoi(num)
+
+ invoice, err := getInvoices(bson.M{"InvoiceNumber": numInt})
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to read invoice %v from DB: %v\n", numInt, err.Error())
+ return
+ }
+
+ if len(invoice) == 0 {
+ ctx.JSON(http.StatusNotFound, gin.H{"error": "no invoice with this invoice number"})
+ log.Printf("WARN: No invoice with number %v found", numInt)
+ return
+ }
+
+ ctx.HTML(http.StatusOK, "invoice.html", gin.H{
+ "Invoice": invoice[0],
+ })
+ })
+
+ i.POST("/new", func(ctx *gin.Context) {
+ var i Invoice
+ ctx.BindJSON(&i)
+ _, err := saveInvoice(i)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ log.Printf("ERROR: Failed to add new invoice %v to DB: %v\n", i, err.Error())
+ return
+ }
+
+ log.Printf("Successfully created new Invoice: %v", i)
+ ctx.JSON(http.StatusOK, nil)
+ })
+
i.DELETE("/:invoiceId", func(ctx *gin.Context) {
id := ctx.Param("invoiceId")
objectId, err := primitive.ObjectIDFromHex(id)
@@ -49,7 +87,7 @@ func Routes(route *gin.Engine) {
return
}
- err = invoice.DeleteInvoice(objectId)
+ err = deleteInvoice(objectId)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to delete invoice %v: %v\n", objectId, err.Error())
@@ -65,7 +103,7 @@ func Routes(route *gin.Engine) {
{
transport.GET("/all", func(ctx *gin.Context) {
// TODO: add functionality to filter results
- transports, err := invoice.GetTransports(nil)
+ transports, err := getTransports(nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to read transport vehicles from DB: %v\n", err.Error())
@@ -84,7 +122,7 @@ func Routes(route *gin.Engine) {
return
}
- err = invoice.DeleteTransport(objectId)
+ err = deleteTransport(objectId)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to delete transport vehicle %v: %v\n", objectId, err.Error())
@@ -100,7 +138,7 @@ func Routes(route *gin.Engine) {
{
transporter.GET("/all", func(ctx *gin.Context) {
// TODO: add functionality to filter results
- transporters, err := invoice.GetTransporters(nil)
+ transporters, err := getTransporters(nil)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to read transporters from DB: %v\n", err.Error())
@@ -119,7 +157,7 @@ func Routes(route *gin.Engine) {
return
}
- err = invoice.DeleteTransporter(objectId)
+ err = deleteTransporter(objectId)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
log.Printf("ERROR: Failed to delete transporter %v: %v\n", objectId, err.Error())