mirror of
https://github.com/frappe/books.git
synced 2024-12-22 19:09:01 +00:00
feat: Add provisional mode where submission can be undone
This commit is contained in:
parent
a2b80a3414
commit
8ec5e624ed
@ -223,6 +223,30 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return true;
|
||||
}
|
||||
|
||||
get canUndoSubmit() {
|
||||
if (!this.canCancel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.fyo.singles.SystemSettings?.provisionalModeSince == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const submittedAt: Date = (this.submittedAt || this.created) as Date;
|
||||
if (!submittedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
submittedAt <
|
||||
(this.fyo.singles.SystemSettings.provisionalModeSince as Date)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get canCancel() {
|
||||
if (!this.schema.isSubmittable) {
|
||||
return false;
|
||||
@ -344,14 +368,17 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
value?: DocValue | Doc[] | DocValueMap[]
|
||||
): boolean {
|
||||
if (fieldname === 'numberSeries' && !this.notInserted) {
|
||||
// console.log("cannot set %s, numberSeries inserted", fieldname)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
// console.log("cannot set %s, undefined value", fieldname)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.fieldMap[fieldname] === undefined) {
|
||||
// console.log("cannot set %s, no fieldMap", fieldname, this.fieldMap)
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -940,12 +967,27 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
await this.trigger('beforeSubmit');
|
||||
await this.setAndSync('submitted', true);
|
||||
await this.setAndSync('submittedAt', new Date());
|
||||
await this.trigger('afterSubmit');
|
||||
|
||||
this.fyo.telemetry.log(Verb.Submitted, this.schemaName);
|
||||
this.fyo.doc.observer.trigger(`submit:${this.schemaName}`, this.name);
|
||||
}
|
||||
|
||||
async submitUndo() {
|
||||
if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.trigger('beforeSubmitUndo');
|
||||
await this.setAndSync('submitted', false);
|
||||
await this.setAndSync('submittedAt', null);
|
||||
await this.trigger('afterSubmitUndo');
|
||||
|
||||
this.fyo.telemetry.log(Verb.SubmitUndone, this.schemaName);
|
||||
this.fyo.doc.observer.trigger(`submitUndo:${this.schemaName}`, this.name);
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
|
||||
return;
|
||||
@ -1058,6 +1100,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
async afterSync() {}
|
||||
async beforeSubmit() {}
|
||||
async afterSubmit() {}
|
||||
async beforeSubmitUndo() {}
|
||||
async afterSubmitUndo() {}
|
||||
async beforeRename() {}
|
||||
async afterRename() {}
|
||||
async beforeCancel() {}
|
||||
|
@ -8,6 +8,7 @@ export enum Verb {
|
||||
Created = 'created',
|
||||
Deleted = 'deleted',
|
||||
Submitted = 'submitted',
|
||||
SubmitUndone = 'submitUndone',
|
||||
Cancelled = 'cancelled',
|
||||
Imported = 'imported',
|
||||
Exported = 'exported',
|
||||
|
@ -56,6 +56,15 @@ export abstract class Transactional extends Doc {
|
||||
await posting.post();
|
||||
}
|
||||
|
||||
async afterSubmitUndo(): Promise<void> {
|
||||
await super.afterSubmitUndo();
|
||||
if (!this.isTransactional) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._deletePostings();
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
if (!this.isTransactional) {
|
||||
@ -76,6 +85,10 @@ export abstract class Transactional extends Doc {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._deletePostings();
|
||||
}
|
||||
|
||||
async _deletePostings(): Promise<void> {
|
||||
const ledgerEntryIds = (await this.fyo.db.getAll(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
|
@ -245,9 +245,29 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmitUndo() {
|
||||
await super.afterSubmitUndo();
|
||||
await this._cancelPayments({ undo: true });
|
||||
await this._updatePartyOutStanding();
|
||||
await this._updateIsItemsReturned();
|
||||
await this._removeLoyaltyPointEntry();
|
||||
this.reduceUsedCountOfCoupons();
|
||||
}
|
||||
|
||||
async _undoPayments() {
|
||||
const paymentIds = await this.getPaymentIds();
|
||||
for (const paymentId of paymentIds) {
|
||||
const paymentDoc = (await this.fyo.doc.getDoc(
|
||||
'Payment',
|
||||
paymentId
|
||||
)) as Payment;
|
||||
await paymentDoc.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
await super.afterCancel();
|
||||
await this._cancelPayments();
|
||||
await this._cancelPayments({ undo: false });
|
||||
await this._updatePartyOutStanding();
|
||||
await this._updateIsItemsReturned();
|
||||
await this._removeLoyaltyPointEntry();
|
||||
@ -258,16 +278,20 @@ export abstract class Invoice extends Transactional {
|
||||
await removeLoyaltyPoint(this);
|
||||
}
|
||||
|
||||
async _cancelPayments() {
|
||||
async _cancelPayments({ undo }: { undo: boolean }) {
|
||||
const paymentIds = await this.getPaymentIds();
|
||||
for (const paymentId of paymentIds) {
|
||||
const paymentDoc = (await this.fyo.doc.getDoc(
|
||||
'Payment',
|
||||
paymentId
|
||||
)) as Payment;
|
||||
if (undo) {
|
||||
await paymentDoc.submitUndo();
|
||||
} else {
|
||||
await paymentDoc.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _updatePartyOutStanding() {
|
||||
const partyDoc = (await this.fyo.doc.getDoc(
|
||||
|
@ -442,6 +442,11 @@ export class Payment extends Transactional {
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmitUndo() {
|
||||
await super.afterSubmitUndo();
|
||||
await this.revertOutstandingAmount();
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
await super.afterCancel();
|
||||
await this.revertOutstandingAmount();
|
||||
|
@ -46,6 +46,10 @@ export class StockManager {
|
||||
await this.#sync();
|
||||
}
|
||||
|
||||
async undoTransfers() {
|
||||
await this.cancelTransfers();
|
||||
}
|
||||
|
||||
async cancelTransfers() {
|
||||
const { referenceName, referenceType } = this.details;
|
||||
await this.fyo.db.deleteAll(ModelNameEnum.StockLedgerEntry, {
|
||||
|
@ -69,6 +69,11 @@ export class StockMovement extends Transfer {
|
||||
await updateSerialNumbers(this, false);
|
||||
}
|
||||
|
||||
async afterSubmitUndo(): Promise<void> {
|
||||
await super.afterSubmitUndo();
|
||||
await updateSerialNumbers(this, true);
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
await updateSerialNumbers(this, true);
|
||||
|
@ -217,6 +217,13 @@ export abstract class StockTransfer extends Transfer {
|
||||
await this._updateItemsReturned();
|
||||
}
|
||||
|
||||
async afterSubmitUndo() {
|
||||
await super.afterSubmitUndo();
|
||||
await updateSerialNumbers(this, false, this.isReturn);
|
||||
await this._updateBackReference();
|
||||
await this._updateItemsReturned();
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
await updateSerialNumbers(this, true, this.isReturn);
|
||||
|
@ -21,6 +21,19 @@ export abstract class Transfer extends Transactional {
|
||||
await this._getStockManager().createTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async beforeSubmitUndo(): Promise<void> {
|
||||
await super.beforeSubmitUndo();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
const stockManager = this._getStockManager();
|
||||
stockManager.isCancelled = true;
|
||||
await stockManager.validateCancel(transferDetails);
|
||||
}
|
||||
|
||||
async afterSubmitUndo(): Promise<void> {
|
||||
await super.afterSubmitUndo();
|
||||
await this._getStockManager().undoTransfers();
|
||||
}
|
||||
|
||||
async beforeCancel(): Promise<void> {
|
||||
await super.beforeCancel();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
|
@ -125,6 +125,13 @@
|
||||
"default": false,
|
||||
"description": "Sets the theme of the app.",
|
||||
"section": "Theme"
|
||||
},
|
||||
{
|
||||
"fieldname": "provisionalModeSince",
|
||||
"label": "Provisional Mode Since",
|
||||
"fieldtype": "Datetime",
|
||||
"description": "Date since the provisional mode is set, or NULL for definitive mode",
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
|
@ -9,6 +9,14 @@
|
||||
"meta": true,
|
||||
"section": "System"
|
||||
},
|
||||
{
|
||||
"fieldname": "submittedAt",
|
||||
"label": "Submition Date",
|
||||
"fieldtype": "Datetime",
|
||||
"required": false,
|
||||
"meta": true,
|
||||
"section": "System"
|
||||
},
|
||||
{
|
||||
"fieldname": "cancelled",
|
||||
"label": "Cancelled",
|
||||
|
@ -109,6 +109,32 @@
|
||||
|
||||
<!-- Report Issue and DB Switcher -->
|
||||
<div class="window-no-drag flex flex-col gap-2 py-2 px-4">
|
||||
<button
|
||||
class="
|
||||
flex
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-400
|
||||
gap-1
|
||||
items-center
|
||||
"
|
||||
@click="toggleProvisionalMode"
|
||||
:title="
|
||||
isProvisionalMode()
|
||||
? t`Provisional mode since` + ' ' + provisionalModeDate()
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<feather-icon
|
||||
:name="isProvisionalMode() ? 'pause-circle' : 'play-circle'"
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<p>
|
||||
{{ isProvisionalMode() ? t`Provisional mode` : t`Definitive mode` }}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="
|
||||
flex
|
||||
@ -184,7 +210,12 @@
|
||||
@click="showDevMode = false"
|
||||
title="Open dev tools with Ctrl+Shift+I"
|
||||
>
|
||||
dev mode
|
||||
<feather-icon
|
||||
name="hash"
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
style="display: inline"
|
||||
/>
|
||||
hide dev mode
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -214,6 +245,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { t } from 'fyo';
|
||||
import { reportIssue } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { languageDirectionKey, shortcutsKey } from 'src/utils/injectionKeys';
|
||||
@ -226,6 +258,7 @@ import router from '../router';
|
||||
import Icon from './Icon.vue';
|
||||
import Modal from './Modal.vue';
|
||||
import ShortcutsHelper from './ShortcutsHelper.vue';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
|
||||
const COMPONENT_NAME = 'Sidebar';
|
||||
|
||||
@ -291,6 +324,64 @@ export default defineComponent({
|
||||
routeTo,
|
||||
reportIssue,
|
||||
toggleSidebar,
|
||||
async toggleProvisionalMode() {
|
||||
let title, detail, provisionalModeSince, showUnlimited;
|
||||
if (fyo.singles.SystemSettings?.provisionalModeSince != null) {
|
||||
title = t`Leave provisional mode?`;
|
||||
detail = t`All submissions while provisional mode was effective will be definitive.`;
|
||||
provisionalModeSince = null;
|
||||
} else {
|
||||
title = t`Enter provisional mode?`;
|
||||
detail = t`Documents submission while in provisional mode can be undone.`;
|
||||
provisionalModeSince = new Date();
|
||||
showUnlimited = this.showDevMode;
|
||||
}
|
||||
|
||||
let response = (await showDialog({
|
||||
title,
|
||||
detail,
|
||||
type: 'warning',
|
||||
buttons: [
|
||||
{
|
||||
label: t`Yes`,
|
||||
action() {
|
||||
return true;
|
||||
},
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: t`No`,
|
||||
action() {
|
||||
return false;
|
||||
},
|
||||
isEscape: true,
|
||||
},
|
||||
showUnlimited
|
||||
? {
|
||||
label: t`Unlimited`,
|
||||
action() {
|
||||
provisionalModeSince = new Date(0);
|
||||
return true;
|
||||
},
|
||||
isPrimary: false,
|
||||
}
|
||||
: null,
|
||||
].filter((x) => x),
|
||||
})) as boolean;
|
||||
|
||||
if (response) {
|
||||
await fyo.singles.SystemSettings?.setAndSync(
|
||||
'provisionalModeSince',
|
||||
provisionalModeSince
|
||||
);
|
||||
}
|
||||
},
|
||||
isProvisionalMode() {
|
||||
return fyo.singles.SystemSettings?.provisionalModeSince != null;
|
||||
},
|
||||
provisionalModeDate() {
|
||||
return fyo.singles.SystemSettings?.provisionalModeSince;
|
||||
},
|
||||
openDocumentation() {
|
||||
ipc.openLink('https://docs.frappe.io/' + docsPathRef.value);
|
||||
},
|
||||
|
@ -193,6 +193,7 @@ export function getActionsForDoc(doc?: Doc): Action[] {
|
||||
...getActions(doc),
|
||||
getDuplicateAction(doc),
|
||||
getDeleteAction(doc),
|
||||
getSubmitUndoAction(doc),
|
||||
getCancelAction(doc),
|
||||
];
|
||||
|
||||
@ -234,6 +235,19 @@ export function getGroupedActionsForDoc(doc?: Doc): ActionGroup[] {
|
||||
return [grouped, actionsMap['']].flat().filter(Boolean);
|
||||
}
|
||||
|
||||
function getSubmitUndoAction(doc: Doc): Action {
|
||||
return {
|
||||
label: t`Undo Submit`,
|
||||
component: {
|
||||
template: '<span class="text-red-700">{{ t`Undo Submit` }}</span>',
|
||||
},
|
||||
condition: (doc: Doc) => doc.canUndoSubmit,
|
||||
async action() {
|
||||
await commonDocUndoSubmit(doc);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getCancelAction(doc: Doc): Action {
|
||||
return {
|
||||
label: t`Cancel`,
|
||||
@ -514,6 +528,21 @@ export async function commongDocDelete(
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function commonDocUndoSubmit(doc: Doc): Promise<boolean> {
|
||||
let res = false;
|
||||
try {
|
||||
res = doc.submitUndo();
|
||||
} catch (err) {
|
||||
await handleErrorWithDialog(err as Error, doc);
|
||||
}
|
||||
if (!res) {
|
||||
return false;
|
||||
}
|
||||
|
||||
showActionToast(doc, 'submitUndo');
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function commonDocCancel(doc: Doc): Promise<boolean> {
|
||||
const res = await cancelDocWithPrompt(doc);
|
||||
if (!res) {
|
||||
@ -726,10 +755,14 @@ function getDocSubmitMessage(doc: Doc): string {
|
||||
return details.join(' ');
|
||||
}
|
||||
|
||||
function showActionToast(doc: Doc, type: 'sync' | 'cancel' | 'delete') {
|
||||
function showActionToast(
|
||||
doc: Doc,
|
||||
type: 'submitUndo' | 'sync' | 'cancel' | 'delete'
|
||||
) {
|
||||
const label = getDocReferenceLabel(doc);
|
||||
const message = {
|
||||
sync: t`${label} saved`,
|
||||
submitUndo: t`${label} submission undone`,
|
||||
cancel: t`${label} cancelled`,
|
||||
delete: t`${label} deleted`,
|
||||
}[type];
|
||||
|
Loading…
Reference in New Issue
Block a user