diff options
-rw-r--r-- | brand/brand.go | 39 | ||||
-rw-r--r-- | brand/db_actions.go | 69 | ||||
-rw-r--r-- | brand/router.go (renamed from brand/brand_router.go) | 13 | ||||
-rw-r--r-- | client/client.go | 75 | ||||
-rw-r--r-- | client/db_actions.go | 62 | ||||
-rw-r--r-- | client/router.go (renamed from client/client_router.go) | 13 | ||||
-rw-r--r-- | database/database.go | 66 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | invoice/db_actions.go | 119 | ||||
-rw-r--r-- | invoice/invoice.go | 114 | ||||
-rw-r--r-- | invoice/router.go (renamed from invoice/invoice_router.go) | 52 | ||||
-rw-r--r-- | item/db_actions.go | 82 | ||||
-rw-r--r-- | item/item.go | 75 | ||||
-rw-r--r-- | item/router.go (renamed from item/item_router.go) | 13 | ||||
-rw-r--r-- | main.go | 2 | ||||
-rw-r--r-- | web/templates/invoice.html | 15 | ||||
-rw-r--r-- | web/templates/partials/item.html | 3 | ||||
-rw-r--r-- | web/templates/partials/item_list.html | 7 |
19 files changed, 790 insertions, 32 deletions
diff --git a/brand/brand.go b/brand/brand.go new file mode 100644 index 0000000..7aaf5e6 --- /dev/null +++ b/brand/brand.go @@ -0,0 +1,39 @@ +/* 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 brand + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "github.com/MikunoNaka/OpenBills-server/database" +) + +// initialise a database connection for this package +// not sure if I should do this but I am... +var db *mongo.Collection = database.DB.Collection("Brands") + +/* An item may or may not be + * assigned to a brand + * + * brands can be used to group products + * to perform certain actions + */ +type Brand struct { + Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"` + Name string `bson:"Name" json:"Name"` +} diff --git a/brand/db_actions.go b/brand/db_actions.go new file mode 100644 index 0000000..eb5961c --- /dev/null +++ b/brand/db_actions.go @@ -0,0 +1,69 @@ +/* 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 brand + +import ( + "context" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "github.com/MikunoNaka/OpenBills-server/database" +) + +var items *mongo.Collection = database.DB.Collection("Items") + +// Add brand to db +func saveBrand(b Brand) (primitive.ObjectID, error) { + res, err := db.InsertOne(context.TODO(), b) + return res.InsertedID.(primitive.ObjectID), err +} + +// Delete brand from DB +func deleteBrand(id primitive.ObjectID) error { + // delete brand + _, err := db.DeleteOne(context.TODO(), bson.M{"_id": id}) + if err != nil { + return err + } + + // delete items associated with this brand + _, err = items.DeleteMany(context.TODO(), bson.M{"Brand._id": id}) + return err +} + +// modify brand in DB +func modifyBrand(id primitive.ObjectID, nb Brand) error { + _, err := db.UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", nb}}) + return err +} + +/* GetBrands queries the database and + * returns brands based on the given filter + * if filter is nil every brand is returned + */ +func getBrands(filter bson.M) ([]Brand, error) { + var brands []Brand + + cursor, err := db.Find(context.TODO(), filter) + if err != nil { + return brands, err + } + + err = cursor.All(context.TODO(), &brands) + return brands, err +} diff --git a/brand/brand_router.go b/brand/router.go index 5d9a163..75c4eb4 100644 --- a/brand/brand_router.go +++ b/brand/router.go @@ -20,7 +20,6 @@ package brand import ( "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson/primitive" - "github.com/MikunoNaka/OpenBills-lib/brand" "log" "net/http" ) @@ -31,7 +30,7 @@ func Routes(route *gin.Engine) { { b.GET("/all", func(ctx *gin.Context) { // TODO: add functionality to filter results - brands, err := brand.GetBrands(nil) + brands, err := getBrands(nil) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to read brands from DB: %v\n", err.Error()) @@ -42,9 +41,9 @@ func Routes(route *gin.Engine) { }) b.POST("/new", func(ctx *gin.Context) { - var b brand.Brand + var b Brand ctx.BindJSON(&b) - _, err := brand.SaveBrand(b) + _, err := saveBrand(b) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to add new brand %v to DB: %v\n", b, err.Error()) @@ -64,9 +63,9 @@ func Routes(route *gin.Engine) { return } - var b brand.Brand + var b Brand ctx.BindJSON(&b) - err = brand.ModifyBrand(objectId, b) + err = modifyBrand(objectId, b) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to modify brand %v: %v\n", objectId, err.Error()) @@ -86,7 +85,7 @@ func Routes(route *gin.Engine) { return } - err = brand.DeleteBrand(objectId) + err = deleteBrand(objectId) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to delete brand %v: %v\n", objectId, err.Error()) diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..caa3076 --- /dev/null +++ b/client/client.go @@ -0,0 +1,75 @@ +/* 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 client + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "github.com/MikunoNaka/OpenBills-server/database" +) + +// initialise a database connection for this package +// not sure if I should do this but I am... +var db *mongo.Collection = database.DB.Collection("Clients") + +/* each invoice has a client + * you should be able to: + * - add, modify, delete a client + * - add client to invoices + * - get all invoices associated with client, etc + */ + +/* each contact has one name + * but multiple contact addresses + * it is assumed that the first one + * has the highest priority + */ + +type Contact struct { + Name string `bson:"Name" json:"Name"` + Phones []string `bson:"Phones" json:"Phones"` + Emails []string `bson:"Emails" json:"Emails"` + Website string `bson:"Website" json:"Website"` +} + +type Address struct { + /* "Text" means the actual address lines. + * If address is 123, xyz colony, myCity, myState the Text + * will be 123, xyz colony, and + * State and City will be myCity and myState + * + * A multiline string is expected. + */ + Text string `bson:"Text" json:"Text"` + City string `bson:"City" json:"City"` + State string `bson:"State" json:"State"` + PostalCode string `bson:"PostalCode" json:"PostalCode"` + Country string `bson:"Country" json:"Country"` +} + +type Client struct { + Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"` + Name string `bson:"Name" json:"Name"` + Contact Contact `bson:"Contact" json:"Contact"` + GSTIN string `bson:"GSTIN" json:"GSTIN"` + /* if shipping address is empty it means that + * the billing address is also shipping address + */ + BillingAddress Address `bson:"BillingAddress" json:"BillingAddress"` + ShippingAddresses []Address `bson:"ShippingAddresses,omitempty" json:"ShippingAddresses"` +} diff --git a/client/db_actions.go b/client/db_actions.go new file mode 100644 index 0000000..bf32d97 --- /dev/null +++ b/client/db_actions.go @@ -0,0 +1,62 @@ +/* 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 client + +import ( + "context" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +/* TODO: Handle errors properly + * Send an API error response instead of log.Fatal + */ + +// Add client to db +func saveClient(c Client) (primitive.ObjectID, error) { + res, err := db.InsertOne(context.TODO(), c) + return res.InsertedID.(primitive.ObjectID), err +} + +// Delete client from DB +func deleteClient(id primitive.ObjectID) error { + _, err := db.DeleteOne(context.TODO(), bson.M{"_id": id}) + return err +} + +// modify client in DB +func modifyClient(id primitive.ObjectID, nc Client) error { + _, err := db.UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", nc}}) + return err +} + +/* GetClients queries the database and + * returns clients based on the given filter + * if filter is nil every client is returned + */ +func getClients(filter bson.M) ([]Client, error) { + var clients []Client + + cursor, err := db.Find(context.TODO(), filter) + if err != nil { + return clients, err + } + + err = cursor.All(context.TODO(), &clients) + return clients, err +} diff --git a/client/client_router.go b/client/router.go index 2fd14fc..46e339a 100644 --- a/client/client_router.go +++ b/client/router.go @@ -19,7 +19,6 @@ package client import ( "github.com/gin-gonic/gin" - "github.com/MikunoNaka/OpenBills-lib/client" "log" "net/http" "go.mongodb.org/mongo-driver/bson/primitive" @@ -30,7 +29,7 @@ func Routes(route *gin.Engine) { { c.GET("/all", func(ctx *gin.Context) { // TODO: add functionality to filter results - clients, err := client.GetClients(nil) + clients, err := getClients(nil) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to read clients from DB: %v\n", err.Error()) @@ -41,9 +40,9 @@ func Routes(route *gin.Engine) { }) c.POST("/new", func(ctx *gin.Context) { - var c client.Client + var c Client ctx.BindJSON(&c) - _, err := client.SaveClient(c) + _, err := saveClient(c) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to add new client %v to DB: %v\n", c, err.Error()) @@ -63,9 +62,9 @@ func Routes(route *gin.Engine) { return } - var c client.Client + var c Client ctx.BindJSON(&c) - err = client.ModifyClient(objectId, c) + err = modifyClient(objectId, c) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to modify client %v: %v\n", objectId, err.Error()) @@ -85,7 +84,7 @@ func Routes(route *gin.Engine) { return } - err = client.DeleteClient(objectId) + err = deleteClient(objectId) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to delete client %v: %v\n", objectId, err.Error()) diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..a970ee1 --- /dev/null +++ b/database/database.go @@ -0,0 +1,66 @@ +/* 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 database + +import ( + "context" + "log" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +/* This creates a new client and sets + * it to the value of MongoClient which + * can be imported by other packages + * + * I am not at all sure if this is the best + * actually no, even if this is a decent way to do it. + * But yea this seems to work + * (remember to close the connections!) + */ +var DB *mongo.Database +var client *mongo.Client + +func init() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var err error + client, err = mongo.Connect(ctx, options.Client().ApplyURI("mongodb://127.0.0.1:27017")) + if err != nil { + log.Fatal(err) + } + + log.Println("Successfully connected to MongoDB database.") + DB = client.Database("OpenBillsDB") +} + +func DisconnectDB() { + if client == nil { + return + } + + err := client.Disconnect(context.TODO()) + if err != nil { + log.Fatal(err) + } + + log.Println("Successfully closed connection with MongoDB.") +} @@ -3,7 +3,6 @@ module github.com/MikunoNaka/OpenBills-server go 1.19 require ( - github.com/MikunoNaka/OpenBills-lib v1.1.1 github.com/gin-gonic/gin v1.8.1 go.mongodb.org/mongo-driver v1.10.2 ) @@ -1,5 +1,3 @@ -github.com/MikunoNaka/OpenBills-lib v1.1.1 h1:mIpvg7S4qMsJFXZ2mMP9CfEoUs+kNByyQhGqI7IkLok= -github.com/MikunoNaka/OpenBills-lib v1.1.1/go.mod h1:uAM49uISC12jAgqstIgehBzSd9QBKUZDde6INRvLyGU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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()) diff --git a/item/db_actions.go b/item/db_actions.go new file mode 100644 index 0000000..36f8364 --- /dev/null +++ b/item/db_actions.go @@ -0,0 +1,82 @@ +/* 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 item + +import ( + "context" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "github.com/MikunoNaka/OpenBills-server/database" + "github.com/MikunoNaka/OpenBills-server/brand" + "go.mongodb.org/mongo-driver/mongo" +) + +var brands *mongo.Collection = database.DB.Collection("Brands") + +// Add item to db +func saveItem(i Item) (primitive.ObjectID, error) { + res, err := db.InsertOne(context.TODO(), i) + return res.InsertedID.(primitive.ObjectID), err +} + +// Delete item from DB +func deleteItem(id primitive.ObjectID) error { + _, err := db.DeleteOne(context.TODO(), bson.M{"_id": id}) + return err +} + +// modify item in DB +func modifyItem(id primitive.ObjectID, ni Item) error { + _, err := db.UpdateOne(context.TODO(), bson.D{{"_id", id}}, bson.D{{"$set", ni}}) + return err +} + +/* GetItems queries the database and + * returns items based on the given filter + * if filter is nil every item is returned + */ +func getItems(filter bson.M) ([]Item, error) { + var items []Item + + cursor, err := db.Find(context.TODO(), filter) + if err != nil { + return items, err + } + + err = cursor.All(context.TODO(), &items) + if err != nil { + return items, err + } + + for id, i := range items { + // continue if item doesn't have a brand + if (i.Brand.Id == primitive.ObjectID{}) { + continue + } + + var b brand.Brand + + err := brands.FindOne(context.TODO(), bson.M{"_id": i.Brand.Id}).Decode(&b) + if err != nil { + return items, err + } + items[id].Brand = b + } + + return items, err +} diff --git a/item/item.go b/item/item.go new file mode 100644 index 0000000..12b7d03 --- /dev/null +++ b/item/item.go @@ -0,0 +1,75 @@ +/* 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 item + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "github.com/MikunoNaka/OpenBills-server/database" + "github.com/MikunoNaka/OpenBills-server/brand" +) + +// initialise a database connection for this package +// not sure if I should do this but I am... +var db *mongo.Collection = database.DB.Collection("Items") + +/* each invoice must contain at least one item + * you should be able to: + * - add, modify, delete an item + * - add item to invoice + */ + +/* An item is any product + * or service that can be sold + * Items may have a max and min quanity + * and some default fields like GST and Unit Price + * that don't need to be entered manually + * + * the front-end may or may not implement + * the default fields + * + * Items can be assigned brands + * and certain actions can be performed + * on the products of a brand altogether + */ +type Item struct { + Id primitive.ObjectID `bson:"_id,omitempty" json:"Id"` + Brand brand.Brand `bson:"Brand,omitempty" json:"Brand"` + UnitOfMeasure string `bson:"UnitOfMeasure" json:"UnitOfMeasure"` + HasDecimalQuantity bool `bson:"HasDecimalQuantity, json:"HasDecimalQuantity"` + // just the defaults, can be overridden in an invoice + Name string `bson:"Name" json:"Name"` + Description string `bson:"Description" json:"Description"` + HSN string `bson:"HSN" json:"HSN"` + UnitPrice float64 `bson:"UnitPrice" json:"UnitPrice"` + // default tax percentage + GSTPercentage float64 `bson:"GSTPercentage" json:"GSTPercentage"` + MaxQuantity float64 `bson:"MaxQuantity" json:"MaxQuantity"` + MinQuantity float64 `bson:"MinQuantity" json:"MinQuantity"` +} + +// Item but with extra fields an invoice might require +type InvoiceItem struct { + Item + /* Each product must have a quantity + * but it is upto the backend to enforce that + */ + // TODO: float64 isn't ideal, find a better way + Quantity float64 `bson:"Quantitiy" json:"Quantity"` + DiscountPercentage float64 `bson:"DiscountPercentage,omitempty" json:"DiscountPercentage"` +} diff --git a/item/item_router.go b/item/router.go index d86af68..299e943 100644 --- a/item/item_router.go +++ b/item/router.go @@ -19,7 +19,6 @@ package item import ( "github.com/gin-gonic/gin" - "github.com/MikunoNaka/OpenBills-lib/item" "go.mongodb.org/mongo-driver/bson/primitive" "log" "net/http" @@ -31,7 +30,7 @@ func Routes(route *gin.Engine) { // TODO: add functionality to filter results // /all returns all the saved items i.GET("/all", func(ctx *gin.Context) { - items, err := item.GetItems(nil) + items, err := getItems(nil) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to read items from DB: %v\n", err.Error()) @@ -42,9 +41,9 @@ func Routes(route *gin.Engine) { }) i.POST("/new", func(ctx *gin.Context) { - var i item.Item + var i Item ctx.BindJSON(&i) - _, err := item.SaveItem(i) + _, err := saveItem(i) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to add new item %v to DB: %v\n", i, err.Error()) @@ -64,9 +63,9 @@ func Routes(route *gin.Engine) { return } - var i item.Item + var i Item ctx.BindJSON(&i) - err = item.ModifyItem(objectId, i) + err = modifyItem(objectId, i) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to modify item %v: %v\n", objectId, err.Error()) @@ -86,7 +85,7 @@ func Routes(route *gin.Engine) { return } - err = item.DeleteItem(objectId) + err = deleteItem(objectId) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ERROR: Failed to delete item %v: %v\n", objectId, err.Error()) @@ -22,7 +22,7 @@ import ( "github.com/MikunoNaka/OpenBills-server/item" "github.com/MikunoNaka/OpenBills-server/client" "github.com/MikunoNaka/OpenBills-server/invoice" - "github.com/MikunoNaka/OpenBills-lib/database" + "github.com/MikunoNaka/OpenBills-server/database" "github.com/gin-gonic/gin" ) diff --git a/web/templates/invoice.html b/web/templates/invoice.html new file mode 100644 index 0000000..77825fe --- /dev/null +++ b/web/templates/invoice.html @@ -0,0 +1,15 @@ +<!doctype html> +<html class="no-js" lang=""> + <head> + <meta charset="utf-8"> + <meta http-equiv="x-ua-compatible" content="ie=edge"> + <title>OpenBills Testing</title> + <meta name="description" content=""> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <link rel="apple-touch-icon" href="/apple-touch-icon.png"> + </head> + <body> + {{ template "partials/item_list.html" .}} + </body> +</html> diff --git a/web/templates/partials/item.html b/web/templates/partials/item.html new file mode 100644 index 0000000..04eabba --- /dev/null +++ b/web/templates/partials/item.html @@ -0,0 +1,3 @@ +{{ define "partials/item.html" }} +<div>{{ .Name }}</div> +{{ end }} diff --git a/web/templates/partials/item_list.html b/web/templates/partials/item_list.html new file mode 100644 index 0000000..f12a34a --- /dev/null +++ b/web/templates/partials/item_list.html @@ -0,0 +1,7 @@ +{{ define "partials/item_list.html.tmpl" }} +<div class="items"> + {{ range .Items }} + {{ template "partials/item.html" .}} + {{ end }} +</div> +{{ end }} |