mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
feat: InvoiceForm
- SalesInvoice List with badges in Status column - QuickEdit view in InvoiceForm and ListView - Native Date control for now - Wrap Quick Edit and Invoice Form in keep-alive
This commit is contained in:
parent
59e79ab919
commit
227133c1ab
@ -107,7 +107,7 @@ module.exports = class LedgerPosting {
|
||||
])
|
||||
}
|
||||
});
|
||||
throw new Error();
|
||||
throw new Error(`Debit ${debit} must be equal to Credit ${credit}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,19 +26,8 @@ module.exports = {
|
||||
fieldname: 'customer',
|
||||
label: 'Customer',
|
||||
fieldtype: 'Link',
|
||||
target: 'Party',
|
||||
required: 1,
|
||||
getFilters: query => {
|
||||
if (query)
|
||||
return {
|
||||
keywords: ['like', query],
|
||||
customer: 1
|
||||
};
|
||||
|
||||
return {
|
||||
customer: 1
|
||||
};
|
||||
}
|
||||
target: 'Customer',
|
||||
required: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'account',
|
||||
|
@ -33,7 +33,7 @@ module.exports = class SalesInvoice extends BaseDocument {
|
||||
|
||||
async formatIntoCustomerCurrency(value) {
|
||||
const companyCurrency = frappe.AccountingSettings.currency;
|
||||
if (this.currency.length && this.currency !== companyCurrency) {
|
||||
if (this.currency && this.currency.length && this.currency !== companyCurrency) {
|
||||
const { numberFormat, symbol } = await this.getCustomerCurrencyInfo();
|
||||
return frappe.format(value, {
|
||||
fieldtype: 'Currency',
|
||||
|
@ -1,29 +1,37 @@
|
||||
import { _ } from 'frappejs/utils';
|
||||
import indicators from 'frappejs/ui/constants/indicators';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
export default {
|
||||
doctype: 'SalesInvoice',
|
||||
title: _('Sales Invoices'),
|
||||
formRoute: name => `/edit/SalesInvoice/${name}`,
|
||||
columns: [
|
||||
'customer',
|
||||
'name',
|
||||
{
|
||||
label: 'Invoice No',
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
getValue(doc) {
|
||||
return doc.name;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
fieldname: 'status',
|
||||
fieldtype: 'Select',
|
||||
size: 'small',
|
||||
options: ['Status..', 'Paid', 'Pending'],
|
||||
getValue(doc) {
|
||||
render(doc) {
|
||||
let status = 'Pending';
|
||||
let color = 'orange';
|
||||
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
|
||||
return 'Paid';
|
||||
status = 'Paid';
|
||||
color = 'green';
|
||||
}
|
||||
return 'Pending';
|
||||
},
|
||||
getIndicator(doc) {
|
||||
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
|
||||
return indicators.GREEN;
|
||||
}
|
||||
return indicators.ORANGE;
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
|
||||
components: { Badge }
|
||||
};
|
||||
}
|
||||
},
|
||||
'date',
|
||||
|
@ -5,6 +5,13 @@ module.exports = {
|
||||
isChild: 1,
|
||||
keywordFields: [],
|
||||
layout: 'ratio',
|
||||
fieldsInForm: [
|
||||
'item',
|
||||
'tax',
|
||||
'quantity',
|
||||
'rate',
|
||||
'amount'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'item',
|
||||
|
@ -28,6 +28,7 @@ function createWindow() {
|
||||
width: 1200,
|
||||
height: 907,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
|
@ -1,11 +1,27 @@
|
||||
<template>
|
||||
<div class="bg-blue-100 rounded text-blue-500 px-2 py-1 truncate">
|
||||
<div class="inline-block rounded px-2 py-1 truncate" :class="getColorClass">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Badge'
|
||||
name: 'Badge',
|
||||
props: {
|
||||
color: {
|
||||
default: 'gray'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getColorClass() {
|
||||
return {
|
||||
gray: 'bg-gray-100 text-gray-600',
|
||||
red: 'bg-red-100 text-red-600',
|
||||
yellow: 'bg-yellow-100 text-yellow-600',
|
||||
orange: 'bg-orange-100 text-orange-600',
|
||||
blue: 'bg-blue-100 text-blue-600'
|
||||
}[this.color];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="text-sm px-4 py-2 focus:outline-none rounded-lg" :style="style" v-bind="$attrs" v-on="$listeners">
|
||||
<button class="px-4 py-2 focus:outline-none rounded-lg" :style="style" v-bind="$attrs" v-on="$listeners">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
@ -6,6 +6,8 @@
|
||||
:class="inputClass"
|
||||
:type="inputType"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
:readonly="df.readOnly"
|
||||
@blur="e => triggerChange(e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
@ -14,7 +16,7 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Base',
|
||||
props: ['df', 'value', 'inputClass'],
|
||||
props: ['df', 'value', 'inputClass', 'placeholder'],
|
||||
inject: ['doctype', 'name'],
|
||||
computed: {
|
||||
inputType() {
|
||||
|
13
src/components/Controls/Date.vue
Normal file
13
src/components/Controls/Date.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import Base from './Base';
|
||||
|
||||
export default {
|
||||
name: 'Date',
|
||||
extends: Base,
|
||||
computed: {
|
||||
inputType() {
|
||||
return 'date';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,6 +1,7 @@
|
||||
import Data from './Data';
|
||||
import Select from './Select';
|
||||
import Link from './Link';
|
||||
import Date from './Date';
|
||||
|
||||
export default {
|
||||
name: 'FormControl',
|
||||
@ -8,7 +9,8 @@ export default {
|
||||
let controls = {
|
||||
Data,
|
||||
Select,
|
||||
Link
|
||||
Link,
|
||||
Date
|
||||
};
|
||||
let { df } = this.$attrs;
|
||||
return h(controls[df.fieldtype] || Data, {
|
||||
|
@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="relative" v-on-outside-click="() => showDropdown = false">
|
||||
<input
|
||||
ref="input"
|
||||
class="focus:outline-none w-full"
|
||||
:class="inputClass"
|
||||
type="text"
|
||||
:value="linkValue"
|
||||
:placeholder="placeholder"
|
||||
:readonly="df.readOnly"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
@keydown.up="highlightItemUp"
|
||||
@keydown.down="highlightItemDown"
|
||||
@keydown.esc="$refs.input.blur"
|
||||
@keydown.enter="selectHighlightedItem"
|
||||
@keydown.tab="showDropdown = false"
|
||||
@keydown.esc="showDropdown = false"
|
||||
/>
|
||||
<div class="mt-1 absolute left-0 z-10 bg-white rounded border w-full" v-if="isFocused">
|
||||
<div
|
||||
class="mt-1 absolute left-0 z-10 bg-white rounded border w-full min-w-56"
|
||||
v-if="showDropdown"
|
||||
>
|
||||
<div class="p-1 max-h-64 overflow-auto" v-if="suggestions.length">
|
||||
<a
|
||||
ref="suggestionItems"
|
||||
@ -31,9 +36,12 @@
|
||||
<a
|
||||
class="block px-2 rounded mt-1 first:mt-0 cursor-pointer flex items-center"
|
||||
:class="{'bg-gray-200': highlightedIndex === suggestions.length, 'py-1': linkValue, 'py-2': !linkValue}"
|
||||
@mouseenter="highlightedIndex = suggestions.length"
|
||||
@mouseleave="highlightedIndex = -1"
|
||||
@click="openNewDoc"
|
||||
>
|
||||
<div>Create</div>
|
||||
<Badge class="ml-2" v-if="isNewValue">{{ linkValue }}</Badge>
|
||||
<div>{{ _('Create') }}</div>
|
||||
<Badge color="blue" class="ml-2" v-if="isNewValue">{{ linkValue }}</Badge>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,20 +62,23 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
linkValue: '',
|
||||
isFocused: false,
|
||||
showDropdown: false,
|
||||
suggestions: [],
|
||||
highlightedIndex: -1
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.linkValue = this.value;
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
this.linkValue = this.value;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isNewValue() {
|
||||
let values = this.suggestions.map(d => d.value);
|
||||
return !values.includes(this.linkValue);
|
||||
return this.linkValue && !values.includes(this.linkValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -97,9 +108,7 @@ export default {
|
||||
},
|
||||
async getFilters(keyword) {
|
||||
let doc = await frappe.getDoc(this.doctype, this.name);
|
||||
return this.df.getFilters
|
||||
? await this.df.getFilters(keyword, doc)
|
||||
: {};
|
||||
return this.df.getFilters ? await this.df.getFilters(keyword, doc) : {};
|
||||
},
|
||||
selectHighlightedItem() {
|
||||
if (![-1, this.suggestions.length].includes(this.highlightedIndex)) {
|
||||
@ -116,42 +125,36 @@ export default {
|
||||
},
|
||||
setSuggestion(suggestion) {
|
||||
this.triggerChange(suggestion.value);
|
||||
this.isFocused = false;
|
||||
this.showDropdown = false;
|
||||
},
|
||||
async openNewDoc() {
|
||||
let doctype = this.df.target;
|
||||
let doc = await frappe.getNewDoc(doctype);
|
||||
let currentPath = this.$route.path;
|
||||
let currentQuery = this.$route.query;
|
||||
let filters = await this.getFilters();
|
||||
this.$router.push({
|
||||
path: `/list/${doctype}/${doc.name}`,
|
||||
path: currentPath,
|
||||
query: {
|
||||
edit: 1,
|
||||
doctype,
|
||||
name: doc.name,
|
||||
values: Object.assign({}, filters, {
|
||||
name: this.linkValue
|
||||
})
|
||||
}
|
||||
});
|
||||
doc.once('afterInsert', () => {
|
||||
this.$router.push({
|
||||
path: currentPath,
|
||||
query: {
|
||||
values: {
|
||||
[this.df.fieldname]: doc.name
|
||||
}
|
||||
}
|
||||
});
|
||||
this.$emit('new-doc', doc);
|
||||
this.$router.go(-1);
|
||||
});
|
||||
},
|
||||
onFocus() {
|
||||
this.isFocused = true;
|
||||
this.showDropdown = true;
|
||||
this.updateSuggestions();
|
||||
},
|
||||
onBlur() {
|
||||
setTimeout(() => {
|
||||
this.isFocused = false;
|
||||
}, 100);
|
||||
},
|
||||
onInput(e) {
|
||||
this.showDropdown = true;
|
||||
this.updateSuggestions(e);
|
||||
},
|
||||
highlightItemUp() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-4 px-8 flex justify-between">
|
||||
<div class="mt-4 px-8 flex justify-between items-center">
|
||||
<slot name="title" />
|
||||
<div class="flex items-center">
|
||||
<slot name="actions" />
|
||||
|
@ -36,7 +36,6 @@
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import ListRow from '../pages/ListView/ListRow';
|
||||
import ListCell from '../pages/ListView/ListCell';
|
||||
import SearchIcon from '@/components/Icons/Search';
|
||||
|
||||
export default {
|
||||
@ -49,7 +48,6 @@ export default {
|
||||
},
|
||||
components: {
|
||||
ListRow,
|
||||
ListCell,
|
||||
SearchIcon
|
||||
},
|
||||
methods: {
|
||||
|
@ -2,7 +2,14 @@
|
||||
<div class="flex">
|
||||
<Sidebar class="w-56" />
|
||||
<div class="flex flex-1 overflow-y-hidden">
|
||||
<router-view class="flex-1" :key="$route.fullPath" />
|
||||
<keep-alive exclude="ListView">
|
||||
<router-view class="flex-1" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
<div class="flex" v-if="$route.query.edit && $route.query.doctype && $route.query.name">
|
||||
<keep-alive>
|
||||
<router-view name="edit" class="w-80 flex-1" :key="$route.query.doctype + $route.query.name"/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
190
src/pages/InvoiceForm.vue
Normal file
190
src/pages/InvoiceForm.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<PageHeader>
|
||||
<a class="cursor-pointer font-semibold" slot="title" @click="$router.go(-1)">{{ _('Back') }}</a>
|
||||
<template slot="actions">
|
||||
<Button class="text-gray-900 text-xs">Customise</Button>
|
||||
<Button v-if="doc._notInserted || doc._dirty" type="primary" class="text-white text-xs ml-2" @click="onSaveClick">{{ _('Save') }}</Button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<div class="flex-1 ml-8 mb-8 mt-6" v-if="meta">
|
||||
<div class="border rounded shadow h-full flex flex-col justify-between" style="width: 600px">
|
||||
<div>
|
||||
<div class="px-6 pt-6">
|
||||
<div class="flex text-xs text-gray-600 border-b pb-4">
|
||||
<div class="w-1/3">
|
||||
<svg class="w-32" viewBox="0 0 120 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#1F7AE0" fill-rule="nonzero">
|
||||
<path
|
||||
d="M6.032 18.953l-4.356-2.088a.401.401 0 01-.162-.582l2.181-3.2a.413.413 0 01.72.073l2.18 5.284c.071.157.032.34-.095.457a.407.407 0 01-.468.056zm14.967-.95V7.185a.404.404 0 00-.198-.344.412.412 0 00-.4-.012l-9.678 4.93a.4.4 0 00-.22.356v10.818a.404.404 0 00.198.344.412.412 0 00.4.012l9.681-4.93a.4.4 0 00.217-.356zm-2.832-4.176a4.924 4.924 0 01-2.423 4.023c-1.335.68-2.418-.017-2.417-1.559a4.924 4.924 0 012.423-4.024c1.335-.68 2.417.02 2.417 1.56zm-8.06-3.505L19.7 5.46a.4.4 0 00-.008-.716L10.18.166a.416.416 0 00-.368 0L.22 5.03a.4.4 0 00.008.716l9.514 4.576c.115.056.25.056.365 0v.001zm-.03-7.407l4.054 1.95a.4.4 0 01.007.716l-3.932 1.993a.419.419 0 01-.37 0l-4.052-1.95a.4.4 0 01-.007-.715l3.931-1.994a.418.418 0 01.37 0zM32.663 18.123a6.095 6.095 0 01-3.44-1.057c-.159-.113-.294-.315-.181-.54l.475-.968c.113-.225.407-.293.679-.135.611.337 1.358.765 2.603.765.882 0 1.38-.428 1.38-1.013 0-.697-.792-1.147-2.24-1.8-1.607-.72-2.83-1.575-2.83-3.285 0-1.306.929-2.836 3.463-2.836 1.449 0 2.535.428 3.056.765.249.18.362.495.226.765l-.362.72c-.159.315-.475.293-.68.203-.723-.315-1.425-.518-2.24-.518-.905 0-1.335.45-1.335.923 0 .675.747 1.08 1.788 1.53 1.924.81 3.372 1.508 3.372 3.443 0 1.62-1.403 3.038-3.734 3.038zm6.133-.742V7.974c0-.247.226-.495.498-.495h.52c.25 0 .385.113.453.315l.294.9a4.461 4.461 0 013.327-1.44c1.517 0 2.422.585 3.192 1.643.294-.293 1.538-1.643 3.53-1.643 3.191 0 3.983 2.205 3.983 4.906v5.22c0 .27-.203.496-.498.496h-1.267c-.317 0-.498-.225-.498-.495V12.07c0-1.665-.634-2.678-2.06-2.678-1.584 0-2.33 1.058-2.512 1.215.046.225.091.855.091 1.395v5.379c0 .27-.226.495-.475.495h-1.29a.487.487 0 01-.498-.495V12.07c0-1.688-.611-2.678-2.06-2.678-1.561 0-2.353 1.148-2.467 1.508v6.48c0 .27-.249.496-.498.496h-1.267a.5.5 0 01-.498-.495zm18.06-2.723c0-1.913 1.494-3.353 4.187-3.353 1.087 0 2.105.315 2.105.315.046-1.643-.362-2.386-1.675-2.386-1.199 0-2.376.338-2.919.495-.317.068-.52-.135-.588-.427l-.204-.855c-.068-.36.09-.518.34-.608.18-.067 1.674-.585 3.598-.585 3.35 0 3.666 2.003 3.666 4.59v5.537c0 .27-.249.495-.498.495h-.747c-.203 0-.316-.09-.43-.36l-.249-.698c-.565.54-1.606 1.305-3.213 1.305-1.97 0-3.372-1.305-3.372-3.465zm2.218-.023c0 .923.544 1.643 1.63 1.643 1.041 0 2.105-.743 2.422-1.26v-1.733c-.136-.09-.95-.36-1.901-.36-1.223 0-2.15.63-2.15 1.71zm9.438 2.746V4.618c0-.247.226-.495.498-.495h1.267c.25 0 .498.248.498.495v12.763c0 .27-.249.495-.498.495H69.01a.5.5 0 01-.498-.495zm5.568 0V4.618c0-.247.226-.495.497-.495h1.268c.249 0 .498.248.498.495v12.763c0 .27-.25.495-.498.495h-1.268a.5.5 0 01-.497-.495zm4.843-4.681c0-3.06 2.467-5.446 5.364-5.446 1.63 0 2.829.608 3.802 1.8.203.248.136.54-.09.743l-.815.743c-.295.27-.498.067-.68-.113-.475-.517-1.312-1.035-2.15-1.035-1.787 0-3.168 1.44-3.168 3.285 0 1.868 1.358 3.308 3.1 3.308 1.359 0 1.902-.765 2.468-1.282.226-.225.475-.225.701-.045l.702.562c.249.225.362.473.18.743-.86 1.283-2.262 2.16-4.073 2.16-2.92 0-5.341-2.295-5.341-5.423zm10.999 1.958c0-1.913 1.494-3.353 4.187-3.353 1.086 0 2.105.315 2.105.315.045-1.643-.362-2.386-1.675-2.386-1.2 0-2.376.338-2.92.495-.316.068-.52-.135-.588-.427l-.204-.855c-.068-.36.09-.518.34-.608.18-.067 1.675-.585 3.598-.585 3.35 0 3.667 2.003 3.667 4.59v5.537c0 .27-.25.495-.498.495h-.747c-.204 0-.317-.09-.43-.36l-.25-.698c-.565.54-1.606 1.305-3.213 1.305-1.969 0-3.372-1.305-3.372-3.465zm2.218-.023c0 .923.543 1.643 1.63 1.643 1.04 0 2.104-.743 2.421-1.26v-1.733c-.136-.09-.95-.36-1.901-.36-1.222 0-2.15.63-2.15 1.71zm12.38 3.488a6.095 6.095 0 01-3.44-1.057c-.159-.113-.295-.315-.181-.54l.475-.968c.113-.225.407-.293.679-.135.61.337 1.358.765 2.602.765.883 0 1.381-.428 1.381-1.013 0-.697-.792-1.147-2.24-1.8-1.607-.72-2.83-1.575-2.83-3.285 0-1.306.928-2.836 3.463-2.836 1.449 0 2.535.428 3.055.765.25.18.363.495.227.765l-.362.72c-.159.315-.476.293-.68.203-.724-.315-1.425-.518-2.24-.518-.905 0-1.335.45-1.335.923 0 .675.747 1.08 1.788 1.53 1.924.81 3.372 1.508 3.372 3.443 0 1.62-1.403 3.038-3.734 3.038zm5.5-5.446c0-2.925 2.059-5.423 5.205-5.423 2.715 0 4.775 2.003 4.775 4.77 0 .18-.023.54-.045.72a.487.487 0 01-.476.451h-7.22c.023 1.395 1.29 2.835 3.124 2.835 1.2 0 1.924-.427 2.557-.877.227-.158.43-.225.612.045l.61.945c.182.225.272.428-.045.698-.747.652-2.127 1.282-3.87 1.282-3.168 0-5.228-2.475-5.228-5.446zm2.353-1.147h5.386c-.045-1.26-1.04-2.363-2.58-2.363-1.652 0-2.648 1.058-2.806 2.363z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<div>adickens@gmail.com</div>
|
||||
<div>02002 5798368</div>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<div>9086 Jerde Street, Port Coralie, AR 89317-0033</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 px-6">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/3">
|
||||
<h1 class="text-2xl font-semibold">Invoice</h1>
|
||||
<FormControl
|
||||
class="mt-2"
|
||||
:df="meta.getField('date')"
|
||||
:value="doc.date"
|
||||
:placeholder="'Date'"
|
||||
@change="value => doc.set('date', value)"
|
||||
input-class="bg-gray-100 rounded-lg px-3 py-2 text-sm"
|
||||
/>
|
||||
<FormControl
|
||||
class="mt-2"
|
||||
:df="meta.getField('account')"
|
||||
:value="doc.account"
|
||||
:placeholder="'Account'"
|
||||
@change="value => doc.set('account', value)"
|
||||
input-class="bg-gray-100 rounded-lg px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<FormControl
|
||||
:df="meta.getField('customer')"
|
||||
:value="doc.customer"
|
||||
:placeholder="'Customer'"
|
||||
@change="value => doc.set('customer', value)"
|
||||
@new-doc="doc => doc.set('customer', doc.name)"
|
||||
input-class="bg-gray-100 rounded-lg p-2 text-right"
|
||||
/>
|
||||
<div
|
||||
class="mt-1 text-xs text-gray-600 text-right"
|
||||
>9115 Francesco Valley, Port Christophe, NH 96860-1674</div>
|
||||
<div class="mt-1 text-xs text-gray-600 text-right">GSTIN: 27MHCQ04111A2Z5</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<ListRow class="text-sm text-gray-600" :ratio="itemTableColumnRatio">
|
||||
<div class="py-4 ml-2 text-center">
|
||||
No
|
||||
</div>
|
||||
<div
|
||||
class="py-4 ml-5"
|
||||
:class="['Float', 'Currency'].includes(df.fieldtype) ? 'text-right' : ''"
|
||||
v-for="df in itemTableFields"
|
||||
:key="df.fieldname"
|
||||
>{{ df.label }}</div>
|
||||
</ListRow>
|
||||
<ListRow
|
||||
class="text-sm"
|
||||
:ratio="itemTableColumnRatio"
|
||||
v-for="row in doc.items"
|
||||
:key="row.idx"
|
||||
>
|
||||
<div class="py-4 ml-2 text-gray-700 text-center">
|
||||
{{ row.idx + 1 }}
|
||||
</div>
|
||||
<div class="py-4 ml-5" v-for="df in itemTableFields" :key="df.fieldname">
|
||||
<FormControl
|
||||
:df="df"
|
||||
:value="row[df.fieldname]"
|
||||
:input-class="'text-gray-900 ' + (['Float', 'Currency'].includes(df.fieldtype) ? 'text-right' : '')"
|
||||
@change="value => row.set(df.fieldname, value)"
|
||||
@new-doc="doc => row.set(df.fieldname, doc.name)"
|
||||
/>
|
||||
</div>
|
||||
</ListRow>
|
||||
<ListRow :ratio="itemTableColumnRatio" class="py-4 text-sm text-gray-500 cursor-pointer border-transparent">
|
||||
<div class="ml-2 flex items-center">
|
||||
<AddIcon class="w-4 h-4 text-gray-500 stroke-current" />
|
||||
</div>
|
||||
<div class="ml-5" @click="addNewItem">Add Row</div>
|
||||
</ListRow>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 mb-6 flex justify-end text-sm">
|
||||
<div class="w-64">
|
||||
<div class="flex pl-2 justify-between py-3 border-b">
|
||||
<div>Subtotal</div>
|
||||
<div>{{ doc.netTotal }}</div>
|
||||
</div>
|
||||
<div class="flex pl-2 justify-between py-3" v-for="tax in doc.taxes" :key="tax.name">
|
||||
<div>{{ tax.account }} ({{ tax.rate }}%)</div>
|
||||
<div>{{ tax.amount }}</div>
|
||||
</div>
|
||||
<div class="flex pl-2 justify-between py-3 border-t text-green-600 font-semibold text-base">
|
||||
<div>Grand Total</div>
|
||||
<div>{{ doc.grandTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import Button from '@/components/Button';
|
||||
import FormControl from '@/components/Controls/FormControl';
|
||||
import ListRow from '@/pages/ListView/ListRow';
|
||||
import AddIcon from '@/components/Icons/Add';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceForm',
|
||||
props: ['name'],
|
||||
components: {
|
||||
PageHeader,
|
||||
Button,
|
||||
FormControl,
|
||||
ListRow,
|
||||
AddIcon
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
doctype: 'SalesInvoice',
|
||||
name: this.name
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: null,
|
||||
itemsMeta: null,
|
||||
doc: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
itemTableFields() {
|
||||
return this.itemsMeta.fieldsInForm.map(fieldname =>
|
||||
this.itemsMeta.getField(fieldname)
|
||||
);
|
||||
},
|
||||
itemTableColumnRatio() {
|
||||
return [0.3].concat(this.itemTableFields.map(_ => 1));
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.meta = frappe.getMeta('SalesInvoice');
|
||||
this.itemsMeta = frappe.getMeta('SalesInvoiceItem');
|
||||
this.doc = await frappe.getDoc('SalesInvoice', this.name);
|
||||
window.si = this.doc;
|
||||
},
|
||||
methods: {
|
||||
async addNewItem() {
|
||||
this.doc.append('items');
|
||||
},
|
||||
async onSaveClick() {
|
||||
await this.doc.set('items', this.doc.items.filter(row => row.item));
|
||||
|
||||
if (this.doc.isNew()) {
|
||||
this.doc.insert();
|
||||
} else {
|
||||
this.doc.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="px-8 pb-8 mt-2">
|
||||
<div class="px-8 pb-8 mt-2 text-sm">
|
||||
<ListRow class="text-gray-700" :columnCount="columns.length">
|
||||
<ListCell
|
||||
<div
|
||||
v-for="column in columns"
|
||||
:key="column.label"
|
||||
class="py-4"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right pr-10' : ''"
|
||||
>{{ column.label }}</ListCell>
|
||||
>{{ column.label }}</div>
|
||||
</ListRow>
|
||||
<ListRow
|
||||
class="cursor-pointer text-gray-900 hover:text-gray-600"
|
||||
@ -18,13 +19,9 @@
|
||||
v-for="column in columns"
|
||||
:key="column.label"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right pr-10' : ''"
|
||||
>
|
||||
<indicator v-if="column.getIndicator" :color="column.getIndicator(doc)" class="mr-2" />
|
||||
<span
|
||||
style="width: 100%"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'text-right':''"
|
||||
>{{ getColumnValue(column, doc) }}</span>
|
||||
</ListCell>
|
||||
:doc="doc"
|
||||
:column="column"
|
||||
></ListCell>
|
||||
</ListRow>
|
||||
</div>
|
||||
</template>
|
||||
@ -68,22 +65,23 @@ export default {
|
||||
frappe.listView.on('filterList', this.updateData.bind(this));
|
||||
},
|
||||
methods: {
|
||||
getColumnValue(column, doc) {
|
||||
// Since currency is formatted in customer currency
|
||||
// frappe.format parses it back into company currency
|
||||
if (['Float', 'Currency'].includes(column.fieldtype)) {
|
||||
return column.getValue(doc);
|
||||
} else {
|
||||
return frappe.format(column.getValue(doc), column.fieldtype);
|
||||
}
|
||||
},
|
||||
async setupColumnsAndData() {
|
||||
this.doctype = this.listConfig.doctype;
|
||||
await this.updateData();
|
||||
},
|
||||
openForm(name) {
|
||||
this.$router.push(`/list/${this.doctype}/${name}`);
|
||||
// this.$router.push(`/edit/${this.doctype}/${name}`);
|
||||
if (this.listConfig.formRoute) {
|
||||
this.$router.push(this.listConfig.formRoute(name));
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: `/list/${this.doctype}`,
|
||||
query: {
|
||||
edit: 1,
|
||||
doctype: this.doctype,
|
||||
name
|
||||
}
|
||||
});
|
||||
},
|
||||
async updateData(filters) {
|
||||
if (!filters) filters = this.getFilters();
|
||||
|
@ -1,5 +1,33 @@
|
||||
<template>
|
||||
<div class="text-sm py-4">
|
||||
<slot></slot>
|
||||
<div
|
||||
class="py-4 flex items-center"
|
||||
:class="['Float', 'Currency'].includes(column.fieldtype) ? 'justify-end':''"
|
||||
>
|
||||
<span v-if="!customRenderer">{{ columnValue }}</span>
|
||||
<component v-else :is="customRenderer" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
|
||||
export default {
|
||||
name: 'ListCell',
|
||||
props: ['doc', 'column'],
|
||||
computed: {
|
||||
columnValue() {
|
||||
let { column, doc } = this;
|
||||
// Since currency is formatted in customer currency
|
||||
// frappe.format parses it back into company currency
|
||||
if (['Float', 'Currency'].includes(column.fieldtype)) {
|
||||
return column.getValue(doc);
|
||||
} else {
|
||||
return frappe.format(column.getValue(doc), column.fieldtype);
|
||||
}
|
||||
},
|
||||
customRenderer() {
|
||||
if (!this.column.render) return;
|
||||
return this.column.render(this.doc);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -9,13 +9,24 @@ export default {
|
||||
props: {
|
||||
columnCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
default: 0
|
||||
},
|
||||
ratio: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
'grid-template-columns': `repeat(${this.columnCount}, 1fr)`
|
||||
if (this.columnCount) {
|
||||
return {
|
||||
'grid-template-columns': `repeat(${this.columnCount}, 1fr)`
|
||||
}
|
||||
}
|
||||
if (this.ratio.length) {
|
||||
return {
|
||||
'grid-template-columns': this.ratio.map(r => `${r}fr`).join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<PageHeader>
|
||||
<h1 slot="title" class="text-xl font-bold" v-if="title">{{ title }}</h1>
|
||||
<template slot="actions">
|
||||
<Button type="primary" @click="openNewForm">
|
||||
<Button type="primary" @click="makeNewDoc">
|
||||
<Add class="w-3 h-3 stroke-current text-white" />
|
||||
</Button>
|
||||
<SearchBar class="ml-2" />
|
||||
@ -14,9 +14,6 @@
|
||||
<List :listConfig="listConfig" :filters="filters" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<router-view class="w-80 flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -31,7 +28,7 @@ import listConfigs from './listConfig';
|
||||
|
||||
export default {
|
||||
name: 'ListView',
|
||||
props: ['listName', 'filters'],
|
||||
props: ['doctype', 'filters'],
|
||||
components: {
|
||||
PageHeader,
|
||||
List,
|
||||
@ -43,7 +40,7 @@ export default {
|
||||
frappe.listView = new Observable();
|
||||
},
|
||||
methods: {
|
||||
async openNewForm() {
|
||||
async makeNewDoc() {
|
||||
const doctype = this.listConfig.doctype;
|
||||
const doc = await frappe.getNewDoc(doctype);
|
||||
if (this.listConfig.filters) {
|
||||
@ -52,22 +49,38 @@ export default {
|
||||
if (this.filters) {
|
||||
doc.set(this.filters);
|
||||
}
|
||||
this.$router.push(`/list/${this.listName}/${doc.name}`);
|
||||
let path = this.getFormPath(doc.name);
|
||||
this.$router.push(path);
|
||||
doc.on('afterInsert', () => {
|
||||
this.$router.push(`/list/${this.listName}/${doc.name}`);
|
||||
let path = this.getFormPath(doc.name);
|
||||
this.$router.replace(path);
|
||||
});
|
||||
},
|
||||
getFormPath(name) {
|
||||
if (this.listConfig.formRoute) {
|
||||
let path = this.listConfig.formRoute(name);
|
||||
return path;
|
||||
}
|
||||
return {
|
||||
path: `/list/${this.doctype}`,
|
||||
query: {
|
||||
edit: 1,
|
||||
doctype: this.doctype,
|
||||
name
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listConfig() {
|
||||
if (listConfigs[this.listName]) {
|
||||
return listConfigs[this.listName];
|
||||
if (listConfigs[this.doctype]) {
|
||||
return listConfigs[this.doctype];
|
||||
} else {
|
||||
frappe.call({
|
||||
method: 'show-dialog',
|
||||
args: {
|
||||
title: 'Not Found',
|
||||
message: `${this.listName} List not Registered`
|
||||
message: `${this.doctype} List not Registered`
|
||||
}
|
||||
});
|
||||
this.$router.go(-1);
|
||||
@ -79,7 +92,7 @@ export default {
|
||||
? this.listConfig.title(this.filters)
|
||||
: this.listConfig.title;
|
||||
}
|
||||
return this.listName;
|
||||
return this.doctype;
|
||||
}
|
||||
}
|
||||
};
|
@ -17,7 +17,7 @@
|
||||
:value="doc[titleDocField.fieldname]"
|
||||
@change="value => valueChange(titleDocField, value)"
|
||||
/>
|
||||
<span v-if="showSaved" class="text-xs text-gray-600">{{ _('Saved') }}</span>
|
||||
<span v-if="statusText" class="text-xs text-gray-600">{{ statusText }}</span>
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<div
|
||||
@ -33,6 +33,7 @@
|
||||
:df="df"
|
||||
:value="doc[df.fieldname]"
|
||||
@change="value => valueChange(df, value)"
|
||||
@new-doc="doc => valueChange(df, doc.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -42,6 +43,7 @@
|
||||
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import { _ } from 'frappejs';
|
||||
import Button from '@/components/Button';
|
||||
import XIcon from '@/components/Icons/X';
|
||||
import FormControl from '@/components/Controls/FormControl';
|
||||
@ -58,7 +60,7 @@ export default {
|
||||
return {
|
||||
doctype: this.doctype,
|
||||
name: this.name
|
||||
}
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -66,36 +68,50 @@ export default {
|
||||
doc: {},
|
||||
fields: [],
|
||||
titleDocField: null,
|
||||
showSaved: false
|
||||
statusText: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
this.fields = this.meta
|
||||
.getQuickEditFields()
|
||||
.map(fieldname => this.meta.getField(fieldname));
|
||||
this.titleDocField = this.meta.getField(this.meta.titleField);
|
||||
await this.fetchDoc();
|
||||
|
||||
// setup the title field
|
||||
if (this.doc._notInserted) {
|
||||
this.doc.set(this.titleDocField.fieldname, '');
|
||||
}
|
||||
if (this.values) {
|
||||
this.doc.set(this.values);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$refs.titleControl.focus()
|
||||
}, 300);
|
||||
await this.fetchMetaAndDoc();
|
||||
},
|
||||
methods: {
|
||||
async fetchMetaAndDoc() {
|
||||
this.meta = frappe.getMeta(this.doctype);
|
||||
this.fields = this.meta
|
||||
.getQuickEditFields()
|
||||
.map(fieldname => this.meta.getField(fieldname));
|
||||
this.titleDocField = this.meta.getField(this.meta.titleField);
|
||||
await this.fetchDoc();
|
||||
|
||||
// setup the title field
|
||||
if (this.doc._notInserted) {
|
||||
this.doc.set(this.titleDocField.fieldname, '');
|
||||
}
|
||||
if (this.values) {
|
||||
this.doc.set(this.values);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.$refs.titleControl.focus();
|
||||
}, 300);
|
||||
},
|
||||
valueChange(df, value) {
|
||||
if (!value) return;
|
||||
let oldValue = this.doc.get(df.fieldname);
|
||||
if (df.fieldname === 'name' && oldValue !== value && !this.doc._notInserted) {
|
||||
if (
|
||||
df.fieldname === 'name' &&
|
||||
oldValue !== value &&
|
||||
!this.doc._notInserted
|
||||
) {
|
||||
this.doc.rename(value);
|
||||
this.doc.once('afterRename', () => {
|
||||
this.$router.push(`/list/${this.doctype}/${this.doc.name}`);
|
||||
this.$router.push({
|
||||
path: `/list/${this.doctype}`,
|
||||
query: {
|
||||
edit: 1,
|
||||
doctype: this.doctype,
|
||||
name: this.name
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -108,6 +124,7 @@ export default {
|
||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||
},
|
||||
async updateDoc() {
|
||||
this.statusText = _('Saving...');
|
||||
try {
|
||||
await this.doc.update();
|
||||
this.triggerSaved();
|
||||
@ -116,8 +133,8 @@ export default {
|
||||
}
|
||||
},
|
||||
triggerSaved() {
|
||||
this.showSaved = true;
|
||||
setTimeout(() => (this.showSaved = false), 1000);
|
||||
this.statusText = _('Saved');
|
||||
setTimeout(() => (this.statusText = null), 1000);
|
||||
},
|
||||
insertDoc() {
|
||||
this.doc.insert();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
import ListView from '../pages/ListView';
|
||||
import ListView from '../pages/ListView/ListView';
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import FormView from '../pages/FormView/FormView';
|
||||
import PrintView from '../pages/PrintView';
|
||||
@ -17,6 +17,8 @@ import Settings from '../pages/Settings/Settings';
|
||||
import ReportList from '../pages/ReportList';
|
||||
import ChartOfAccounts from '../pages/ChartOfAccounts';
|
||||
|
||||
import InvoiceForm from '@/pages/InvoiceForm';
|
||||
|
||||
import Tree from 'frappejs/ui/components/Tree';
|
||||
|
||||
Vue.use(Router);
|
||||
@ -27,31 +29,34 @@ const routes = [
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '/list/:listName',
|
||||
name: 'ListView',
|
||||
component: ListView,
|
||||
props: route => {
|
||||
const { listName } = route.params;
|
||||
return {
|
||||
listName,
|
||||
filters: route.query.filters
|
||||
};
|
||||
path: '/edit/SalesInvoice/:name',
|
||||
name: 'InvoiceForm',
|
||||
components: {
|
||||
default: InvoiceForm,
|
||||
edit: QuickEditForm
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: ':name',
|
||||
component: QuickEditForm,
|
||||
props: route => {
|
||||
const { listName, name } = route.params;
|
||||
let values = route.query.values || null;
|
||||
return {
|
||||
doctype: listName,
|
||||
name,
|
||||
values
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
props: {
|
||||
default: true,
|
||||
edit: route => route.query
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/list/:doctype',
|
||||
name: 'ListView',
|
||||
components: {
|
||||
default: ListView,
|
||||
edit: QuickEditForm
|
||||
},
|
||||
props: {
|
||||
default: route => {
|
||||
const { doctype } = route.params;
|
||||
return {
|
||||
doctype,
|
||||
filters: route.query.filters
|
||||
};
|
||||
},
|
||||
edit: route => route.query
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/edit/:doctype/:name',
|
||||
|
@ -7,6 +7,9 @@ module.exports = {
|
||||
maxHeight: {
|
||||
'64': '16rem'
|
||||
},
|
||||
minWidth: {
|
||||
'56': '14rem'
|
||||
},
|
||||
spacing: {
|
||||
'72': '18rem',
|
||||
'80': '20rem'
|
||||
|
Loading…
Reference in New Issue
Block a user