Skip to content

Instantly share code, notes, and snippets.

@ghaffaru
Created August 17, 2021 16:49
Show Gist options
  • Select an option

  • Save ghaffaru/5092ed16e535e2bfe2ad57aa7bc7e435 to your computer and use it in GitHub Desktop.

Select an option

Save ghaffaru/5092ed16e535e2bfe2ad57aa7bc7e435 to your computer and use it in GitHub Desktop.
<template>
<div>
<div :class="$vuetify.theme.isDark ? 'bg-black' : 'bg-white'" v-if="!not_allowed" class="col-md-10 mx-auto col-sm-12 card">
<v-overlay
:value="loading"
:dark="false"
>
<v-progress-circular
:size="50"
color="green"
indeterminate
></v-progress-circular>
</v-overlay>
<div class="card-header">
<v-row>
<v-col cols="12" sm="2">
<v-btn text color="blue darken-4" to="/sales"><v-icon>mdi-arrow-left</v-icon> {{ $t("main.back") }}</v-btn>
</v-col>
<v-col cols="12" sm="10">
<h3 class="font-weight-light">{{ $t("quick_sale.create_new_sale") }}</h3>
</v-col>
</v-row>
</div>
<v-row>
<v-col cols="12" sm="12" class="mx-auto">
<v-row>
<v-col cols="12" sm="12" class="text-center">
<h1 class="font-weight-black">{{ $t("main.amount_due") }}: {{amount_due | toMoney | currency_symbol}}</h1>
<h4 class="font-weight-light">
{{ $t("main.subtotal") }}: {{gross_amount | toMoney | currency_symbol}} |
{{ $t("main.taxes") }}: {{tax_amount | toMoney | currency_symbol}} |
{{ $t("main.discount") }}: ({{discount_amount | toMoney | currency_symbol}})
</h4>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="3" class="text-right">
<v-switch color="success" :label="$t('main.print_receipt')" v-model="print_receipt"></v-switch>
</v-col>
<v-col cols="12" sm="3">
<v-menu
v-model="startDate_menu"
:close-on-content-click="false"
:nudge-right="40"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on }">
<v-text-field
v-model="entrydate"
:label="$t('main.entry_date')"
prepend-icon="event"
readonly
outlined=""
v-on="on"
></v-text-field>
</template>
<v-date-picker v-model="entrydate" @input="startDate_menu = false"></v-date-picker>
</v-menu>
</v-col>
<v-col cols="12" sm="4">
<account-dropdown
:accountTypes="['Assets']"
:subAccounts="payment_accounts"
:label="$t('main.payment_account')"
:hint="$t('main.payment_account_hint')"
@accountChange="(id) => handleAccountChange(id, 'payment')"
></account-dropdown>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="12" class="mx-auto text-center" >
<h5 v-if="!sales_items.length>0" class="font-italic align-center">{{ $t("quick_sale.add_item") }}</h5>
<v-form ref="item_form" class="mb-2">
<v-simple-table v-if="sales_items" class="table-hover">
<tbody>
<tr v-for="(item, index) in sales_items" v-bind:key="index">
<td>
<v-autocomplete
:disabled="saving_progress"
:items="items"
item-value="id"
item-text="name"
:label="$t('main.item')"
cache-items
eager
v-model="item.item"
@change="apply_item(index)"
return-object
:no-data-text="$t('main.empty_product_service')"
@focus="new_product= items.length===0"
></v-autocomplete>
</td>
<td>
<v-textarea
rows="2"
small
:label="$t('main.description')"
v-model="item.description"
auto-grow
>
</v-textarea>
</td>
<td>
<v-text-field
@keyup="compute_amount(item)"
@change="item.rawUnit_price ? item.rawUnit_price=item.rawUnit_price : item.rawUnit_price =0; compute_amount(item)"
type="number"
:label="$t('main.price')"
v-model="item.rawUnit_price"
:prefix="$store.state.user.user_infor.current_business.currency.symbol"
>
</v-text-field>
</td>
<td>
<v-text-field
@keyup="compute_amount(item)"
@change="item.invoice_quantity ? item.invoice_quantity=item.invoice_quantity : item.invoice_quantity=1; compute_amount(item)"
type="number"
:label="$t('main.quantity')"
v-model="item.invoice_quantity"
:rules="required_rules"
>
</v-text-field>
</td>
<td class="text-center">
{{item.invoice_amount | toMoney}} <br> {{ $t("main.amount") }}
</td>
<td>
<v-select
:items="item.taxes"
item-value="id"
item-text="display_name"
v-model="item.tax_id"
clearable
@change="compute_amount(item)"
:hint="item.tax_amount | toMoney |currency_symbol"
persistent-hint
:label="$t('main.tax')"
>
</v-select>
</td>
<td>
<v-row>
<v-col cols="12" sm="12" class="p-0">
<v-btn class="text-muted" @click="moveItem_up(index,item)" v-if="index != 0 " icon small><v-icon>mdi-arrow-up</v-icon></v-btn>
</v-col>
<v-col cols="12" sm="12" class="p-0">
<v-btn @click="remove_item(index)" color="red" icon small><v-icon>mdi-close</v-icon></v-btn>
</v-col>
<v-col cols="12" sm="12" class="p-0">
<v-btn @click="moveItem_down(index,item)" class="text-muted" v-if="index < (sales_items.length-1)" icon small><v-icon>mdi-arrow-down</v-icon></v-btn>
</v-col>
</v-row>
</td>
</tr>
</tbody>
</v-simple-table>
</v-form>
<v-btn :disabled="quantity_error" rounded x-large outlined color="green" @click="add_line" >{{ sales_items.length>0 ? 'Add another item' : 'Add item' }}</v-btn>
<v-btn
small
:disabled="quantity_error"
rounded
x-large
outlined
color="green"
text
dark
@click="$store.state.new_item = true; retailbyDefaule=true;"
><v-icon>mdi-plus</v-icon> {{ $t("main.new_item") }}</v-btn>
</v-col>
<tag-input
:message="$t('tags.sale_tag')"
color="green"
@tags="setTags"
:tagFill="tagFill"
>
</tag-input>
</v-row>
<v-row class="mb-2">
<v-col cols="12" sm="10" class="mx-auto" v-if="sales_items.length">
<v-textarea
outlined
:label="$t('main.note')"
v-model="note"
rows="2"
auto-grow
>
</v-textarea>
</v-col>
</v-row>
</v-col>
</v-row>
<div class="card-footer">
<v-row>
<v-spacer></v-spacer>
<v-col cols="12" sm="6">
<v-menu
v-model="discount_dlg"
:close-on-content-click="false"
:nudge-width="200"
offset-x
ref="discount_menu"
>
<template v-slot:activator="{ on }">
<v-btn
x-large
outlined
color="green"
dark
rounded
v-on="on"
>
{{ $t("main.add_discount") }}
</v-btn>
</template>
<v-card
style="max-width: 400px"
class="p-1"
>
<v-list>
<v-list-item>
<v-list-item-title> {{ $t("main.apply_discount") }}</v-list-item-title>
</v-list-item>
<v-list-item>
<v-list-item-action>
<v-switch :label="$t('main.enter_amount')" @change="discount_percentageSwitch = ! discount_percentageSwitch" v-model="discount_amountSwitch" color="blue darken-4"></v-switch>
</v-list-item-action>
<v-list-item-title>
<v-text-field
class="m-2"
outlined
@keyup="compute_total_amount"
v-model="discount_amount"
:disabled="!discount_amountSwitch"
:placeholder="$t('main.amount')"
></v-text-field>
</v-list-item-title>
</v-list-item>
<v-list-item >
<v-list-item-action>
<v-switch label=" %" @change="discount_amountSwitch = !discount_amountSwitch" v-model="discount_percentageSwitch" color="purple"></v-switch>
</v-list-item-action>
<v-list-item-title>
<v-text-field
class="m-2"
outlined
@keyup="compute_discountPercentage"
v-model="discount_percentage"
:disabled="!discount_percentageSwitch"
@change="compute_discountPercentage"
placeholder="%"
></v-text-field>
</v-list-item-title>
</v-list-item>
<v-list-item>
<v-btn @click="$refs.discount_menu.isActive =false;" small text color="success"> {{ $t("main.done") }}</v-btn>
</v-list-item>
</v-list>
</v-card>
</v-menu>
<span v-if="discount_percentageSwitch" style="font-size: 18px;" class="font-weight-light ">{{discount_percentage}}%: {{discount_amount | toMoney | currency_symbol}} </span>
<span v-if="discount_amountSwitch" style="font-size: 18px;" class="font-weight-light ">{{ discount_amount | toMoney | currency_symbol}} </span>
</v-col>
<v-col
cols="12"
sm="2"
>
<v-btn :disabled="quantity_error" :loading="saving_progress" @click="save_cart" rounded color="success" x-large>{{ $t("main.save") }}</v-btn>
</v-col>
</v-row>
</div>
</div>
<cannotview-component v-else></cannotview-component>
<new-item-component
@ItemCreated="create_item"
:selling_default="retailbyDefaule"
>
</new-item-component>
<v-snackbar
v-model="success_message"
:timeout="snack_timeout"
>
{{toast_msg}}
<v-btn
color="success"
text
@click="success_message = false"
>
{{ $t("main.cancel") }}
</v-btn>
</v-snackbar>
<v-snackbar
v-model="error_message"
color="error"
:timeout="snack_timeout"
>
{{error_msg}}
<v-btn
color="light"
text
@click="error_message = false"
>
{{ $t("main.close") }}
</v-btn>
</v-snackbar>
</div>
</template>
<script>
import moment from "moment";
import CannotviewComponent from "./cannotviewComponent";
import NewItemComponent from "../product/NewItemComponent";
const AccountDropdown = ()=>import('./accountDropdown.vue');
const loginComponent = ()=>import('./loginComponent');
const tagInput = () => import("./tagInput")
export default {
name: "newsaleComponent",
components:{
NewItemComponent,
CannotviewComponent,
tagInput,
AccountDropdown
},
data(){
return{
retailbyDefaule:false,
tags: [],
tagFill: false,
categories:[],
cat_loading:false,
category_id:null,
img_preview:'/img/item_photo.png',
has_photo:false,
change_photo:false,
not_allowed:false,
print_receipt:true,
selling_price:0,
display_unitPrice:0,
code:'',
quantity_error:false,
unit_price:0,
name:'',
formValid:false,
saving_progress:false,
is_product:true,
new_product:false,
search_value:'',
payment_accounts:[],
payment_account:null,
delete_dialog:false,
sale_id:'',
gross_amount:0,
amount_due:0,
snack_timeout:4500,
snack_infinite:0,
search:'',
business_name:'',
business_logo:'',
business_address:'',
business_phone:'',
business_email:'',
loading:false,
success_message:false,
sales_preview:false,
toast_msg:'',
items:[],
items_alt:[],
sales:[],
error_msg:'',
error_message:false,
client_error:false,
editing:false,
create_overlay:false,
discount_percentage:0,
discount_percentageSwitch:true,
discount_amountSwitch:false,
discount_amount:0,
discounted_amount:0,
note:'',
sale_number:'',
discount_dlg:false,
sales_items:[],
sales_dialog:false,
startDate_menu:false,
entrydate:'',
clear_dialog:false,
item_type:'Product',
item_types:[{text:'Product',value:'Product'},{text:'Service',value:'Service'}],
buy:false,
tax_amount:0,
retail:true,
taxed:false,
sales_accounts:[],
sales_account:null,
purchase_accounts:[],
purchase_account:null,
track_inventory:false,
description:'',
alltaxes:[],
item_tax:null,
nameRules: [
v => !!v || this.$t("main.item_name_required"),
//v => (v && v.length <= 10) || 'Name must be less than 10 characters',
],
required_rules:[
v => !!v || this.$t("main.required"),
v => v>0 || this.$t("main.invalid_value"),
],
headers: [
{
text: 'SALE #',
align: 'center',
sortable: true,
value: 'sales_number',
},
{ text: 'NET AMOUNT', value: 'amount_due' },
{ text: 'ENTRY DATE', value: 'entry_date',
align:'center',
},
{ text: 'CREATED ON', value: 'created_at' },
{ text: 'ACTIONS', value: 'id', align:'center' },
]
}
},
computed: {
baseUrl() {
return this.$store.state.baseURL
},
businessCurrency(){
return this.$store.state.user.user_infor.current_business.currency;
}
},
methods:{
setTags(tags) {
this.tags = tags
},
handleAccountChange(accountId, type) {
if (type === "payment") this.payment_account = accountId
if (type === "sales") this.sales_account = accountId
if (type === "purchase") this.purchase_account = accountId
},
get_categories(){
this.cat_loading=true;
axios.get('/api/itemcategories')
.then(res=>{
this.categories = res.data;
this.category_id = this.categories[0].id;
this.cat_loading=false;
})
},
remove_photo(){
this.photo=null;
this.img_preview='/img/item_photo.png';
this.has_photo=false;
this.change_photo=true;
},
select_image(){
let image_selector = document.getElementById('item_photo');
image_selector.click();
},
set_image(e){
this.photo = e.target.files[0];
const fr = new FileReader();
fr.readAsDataURL(this.photo);
fr.onload = ()=>{
this.img_preview = fr.result;
this.has_photo=true;
}
},
get_default_account(accounts,account_name=null){
return accounts.filter(account=>{
return account.account_name == account_name;
});
},
apply_item(i){
let item = this.sales_items[i];
if(item.track_inventory === 1 && Number(item.display_unitPrice) ===0){
this.error_msg = this.$t("item.stock_error");
this.error_message =true;
}
let product= JSON.parse(JSON.stringify(item.item));
product.invoice_quantity =1;
this.sales_items[i] = product ;
this.sales_items[i].item = product;
//item = product ;
this.compute_amount(this.sales_items[i]);
//this.compute_total_amount();
this.compute_discountPercentage();
},
get_country_and_taxes(){
axios.get('/api/getcountryinfo')
.then(res=>{
this.$store.state.user.countryInfo = res.data;
this.alltaxes = res.data.usertaxes;
this.loading = false;
})
.catch(()=>{
this.error_msg = this.$t("quick_sale.sales_account_error");
this.error_message = true;
this.sales_dialog = false;
this.loading = false;
});
},
type_change(){
this.is_product = this.item_type=='Product' ? true:false;
},
create_item(item){
this.add_item(item);
this.items.push(item);
this.retailbyDefaule = false;
},
search_item(){
let new_list = [];
new_list = new_list.length > 0 ? new_list : JSON.parse(JSON.stringify(this.items));
let search_match = [];
if(this.search_value.length >0){
for(let i =0; i<new_list.length;i++){
if(new_list[i].name.toLowerCase().indexOf(this.search_value.toLowerCase())> -1){
search_match.push(new_list[i]);
}
}
this.items = search_match;
}else{
this.items = JSON.parse(JSON.stringify(this.items_alt));
}
},
print_invoice(){
window.print();
},
view_details(item){
this.$store.state.selected_invoice = item;
this.sales_preview = true;
},
new_sale(){
this.editing = false;
this.sales_dialog = true;
this.sale_number ='';
this.get_items();
},
compute_discountPercentage(){
if(this.discount_percentageSwitch){
this.discount_amount = this.gross_amount*(this.discount_percentage/100);
this.amount_due =this.gross_amount - this.discount_amount;
this.amount_due+=Number(this.tax_amount);
}
},
compute_inputTax(rate,amount){
return (Number(rate)/100)*Number(amount);
},
compute_total_amount(){
let gross_amount = 0;
let tax_amount = 0;
this.quantity_error=false;
this.sales_items.forEach(item=>{
if(item.track_inventory==1){
if(item.invoice_quantity > item.quantity){
this.quantity_error = true;
this.error_msg = "Invalid quantity provided for "+item.name+", you have "+item.quantity+" of "+item.name+" in stock";
this.error_message =true;
}else{
this.quantity_error = false;
this.error_message =false;
}
}else{
this.quantity_error = false;
}
let total_tax = 0;
if(item.tax_id){
let selected_tax = item.taxes.find(tax=>{
return tax.id == item.tax_id;
});
if(selected_tax.type=='Flat'){
item.tax_amount = this.compute_inputTax(selected_tax.rate,item.invoice_amount);
tax_amount += item.tax_amount;
gross_amount += item.invoice_amount;
}else if(selected_tax.type=='Compound'){
//compute input taxes before applying compound taxes
let subamount = this.compute_inputTax(selected_tax.sub_rate,item.invoice_amount);
let compound_amount = Number(subamount) + Number(item.invoice_amount);
let compound_taxAmount = this.compute_inputTax(selected_tax.rate,compound_amount);
item.tax_amount = compound_taxAmount+subamount;
item.total_tax_rate = total_tax;
// gross_amount+= Number(item.tax_amount)+Number(item.invoice_amount)+Number(subamount);
// item.tax_amount+=subamount;
tax_amount += item.tax_amount;
gross_amount += item.invoice_amount;
}
}else{
item.tax_amount=0;
item.total_tax_rate = 0;
gross_amount+=Number(item.invoice_amount);
}
});
this.tax_amount = tax_amount;
this.compute_discountPercentage();
this.gross_amount = gross_amount;
this.amount_due = (Number(gross_amount) - Number(this.discount_amount)) +this.tax_amount;
},
remove_item(index){
this.sales_items.splice(index,1);
this.compute_total_amount();
this.compute_discountPercentage();
},
compute_amount(item){
if(item.invoice_quantity){
item.invoice_amount = (Number(item.rawUnit_price) * Number(item.invoice_quantity));
}
this.compute_total_amount();
},
delete_item(item){
this.sale_number = item.sales_number;
this.sale_id = item.id;
this.delete_dialog = true;
},
get_items(){
this.loading = true;
axios.get("/api/getinvoiceitems")
.then(res=>{
this.items=res.data;
this.items_alt=res.data;
this.loading=false;
if(this.$route.query.item_id){
let searchItem = this.items.find(item=>{ return item.id==this.$route.query.item_id});
if (searchItem){
this.create_item(searchItem);
}
}
this.get_country_and_taxes();
})
.catch(()=>{
this.error_msg=this.$t("main.error_msg");
this.error_message=true;
});
},
reload_items(){
this.loading = true;
axios.get("/api/getinvoiceitems")
.then(res=>{
this.items=res.data;
this.items_alt=res.data;
this.loading=false;
})
.catch(()=>{
this.error_msg=this.$t("main.error_msg");
this.error_message=true;
});
},
get_salenumber(){
axios.get('/api/getsalenumber')
.then(number=>{
this.sale_number = number.data;
this.loading=false;
})
.catch(()=>{
this.sales_dialog=false;
this.error_msg=this.$t("main.error_msg");
this.error_message=true;
});
},
prep_items(items){
let new_items=[];
items.forEach(item=>{
item.item=null;
new_items.push(item);
});
return new_items;
},
reprep_items(){
this.sales_items.forEach(item=>{
item.item=item;
});
},
save_cart(){
if (this.quantity_error){
this.error_message=true;
return false;
}
if (!this.validate_items()){
return false;
}
if(this.sales_items.length){
if(!this.payment_account){
this.error_msg=this.$t("main.payment_account_selected_error");
this.error_message=true;
return false;
}
if(this.$refs.item_form.validate()){
this.saving_progress=true;
let cart={
entry_date:this.entrydate,
sale_number:this.sale_number,
gross_amount:this.gross_amount,
discount_amount:this.discount_amount,
amount_due:this.amount_due,
items:this.prep_items(this.sales_items),
note:this.note,
tag: this.tags.toString(),
payment_accountID:this.payment_account
};
axios.post('/api/savesale',cart)
.then(sale=>{
this.saving_progress=false;
this.toast_msg=this.$t("quick_sale.sale_create_success");
this.success_message=true;
this.sales_items = [];
this.discount_amount=0;
this.discount_percentage=0;
this.tax_amount=0;
this.amount_due=0;
this.gross_amount=0;
this.tags = []
this.tagFill = true
if (this.print_receipt){
let w = window.open(this.baseUrl + '/printposreceipt/'+sale.data.enc_id,"Sales receipt"+sale.data.sales_number, 'height=800,width=900');
w.onload=function (e) {
w.print();
};
}
this.reload_items();
})
.catch(err=>{
this.saving_progress=false;
this.error_msg= err.response.status===302 ? err.response.data : this.$t("quick_sale.item_create_error");
this.error_message=true;
this.reprep_items();
});
}
}else{
this.error_msg=this.$t("quick_sale.cart_empty_error");
this.error_message=true;
}
},
// add_item(item){
// if(item.track_inventory === 1 && Number(item.display_unitPrice) ==0){
// this.error_msg = "This item is out of stocked, please purchase this item before you can sell it";
// this.error_message =true;
// return false;
// }
// let newitem =JSON.parse(JSON.stringify(item));
// newitem.invoice_quantity=1;
// this.sales_items.push(newitem);
//
// this.compute_total_amount();
// this.compute_discountPercentage();
//
// this.$refs.item_menu.isActive = false;
//
// },
validate_items(){
let res = true;
for (let item of this.sales_items){
if (!item.id){
this.error_msg=this.$t("item.item_line_error");
this.error_message=true;
res = false;
break;
}
}
return res;
},
add_line(){
let item = {
id:null,
description:'',
tax_id:'' ,
rawUnit_price:0,
invoice_amount:0,
invoice_quantity:1,
item:{}
};
this.sales_items.push(item);
},
add_item(item){
if(item.track_inventory == 1 && Number(item.display_unitPrice) ==0){
this.error_msg = this.$t("item.stock_error")
this.error_message =true;
return false;
}
let newitem =JSON.parse(JSON.stringify(item));
this.items.push(newitem);
newitem.item = JSON.parse(JSON.stringify(item));
newitem.invoice_quantity=1;
this.sales_items.push(newitem);
this.compute_total_amount();
this.compute_discountPercentage();
},
moveItem_up(index,item){
let index2 = index-1;
let item2 = this.sales_items[index2];
this.sales_items[index]=item2;
this.sales_items[index2]=item;
this.sales_items.push({});
let lastindex = (this.sales_items.length -1);
this.remove_item(lastindex);
},
moveItem_down(index,item){
let index2 = index+1;
let item2 = this.sales_items[index2];
this.sales_items[index]=item2;
this.sales_items[index2]=item;
this.sales_items.push({});
let lastindex = (this.sales_items.length -1);
this.remove_item(lastindex);
}
},
mounted() {
if (this.$store.state.user.user_infor.is_admin==1){
this.not_allowed=false;
this.get_items();
}else {
if(this.$store.state.user.user_infor.components[2].roles.create == 1){
this.not_allowed=false;
this.get_items();
}else {
this.not_allowed=true;
this.progress = false;
return false;
}
}
let sales_sub = this.$store.state.user.countryInfo.incomeAccounts.subtypes.filter(account_type=>{
return account_type.name == "Revenue";
});
this.sales_accounts = sales_sub[0].accounts;
this.sales_account = this.sales_accounts[1].id;
let expense_sub = this.$store.state.user.countryInfo.expenseAccounts.subtypes.filter(account_type=>{
return account_type.name == "Cost of Sale";
});
let expense_sub2 = this.$store.state.user.countryInfo.expenseAccounts.subtypes.filter(account_type=>{
return account_type.name == "Operating Expenses";
});
this.purchase_accounts = [...expense_sub[0].accounts, ...expense_sub2[0].accounts];
// let default_account = this.get_default_account(expense_sub[0].accounts,'Purchases');
// let default_sales = this.get_default_account(sales_sub[0].accounts,'Sales Revenue');
// // this.sales_account=default_sales[0].id;
// this.purchase_account = default_account[0].id;
this.entrydate = moment().format('Y-M-D');
let sales2_sub = this.$store.state.user.countryInfo.assetAccounts.subtypes.filter(account_type=>{
return account_type.name == "Cash & Bank";
});
let all_payment_accounts = sales2_sub[0].accounts;
this.payment_accounts = all_payment_accounts.filter(account=>{
return !account.currency || account.currency === this.businessCurrency.code;
});
}
}
</script>
<style scoped>
.v-menu__content{
max-height: 100%;
overflow-y: auto !important;
}
@media screen and (max-device-width:768px) {
td {
display: table-row;
width: 100% !important;
}
td> .v-input{
min-width: 360px !important;
align-self: center;
}
}
@media screen and (device-width:768px) {
td {
display: table-row;
width: 100% !important;
}
td> .v-input{
min-width: 660px !important;
align-self: center;
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment