2
0
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:
Faris Ansari 2019-10-11 15:25:50 +05:30
parent 59e79ab919
commit 227133c1ab
23 changed files with 462 additions and 151 deletions

View File

@ -107,7 +107,7 @@ module.exports = class LedgerPosting {
])
}
});
throw new Error();
throw new Error(`Debit ${debit} must be equal to Credit ${credit}`);
}
}

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -5,6 +5,13 @@ module.exports = {
isChild: 1,
keywordFields: [],
layout: 'ratio',
fieldsInForm: [
'item',
'tax',
'quantity',
'rate',
'amount'
],
fields: [
{
fieldname: 'item',

View File

@ -28,6 +28,7 @@ function createWindow() {
width: 1200,
height: 907,
frame: false,
resizable: false,
useContentSize: true,
webPreferences: {
webSecurity: false,

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -0,0 +1,13 @@
<script>
import Base from './Base';
export default {
name: 'Date',
extends: Base,
computed: {
inputType() {
return 'date';
}
}
};
</script>

View File

@ -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, {

View File

@ -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() {

View File

@ -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" />

View File

@ -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: {

View File

@ -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
View 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>

View File

@ -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();

View File

@ -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>

View File

@ -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(' ')
}
}
}
}

View File

@ -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;
}
}
};

View File

@ -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();

View File

@ -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',

View File

@ -7,6 +7,9 @@ module.exports = {
maxHeight: {
'64': '16rem'
},
minWidth: {
'56': '14rem'
},
spacing: {
'72': '18rem',
'80': '20rem'