mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 07:54:46 +00:00
Merge pull request #43563 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -174,6 +174,17 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
|
||||||
|
const row = frappe.get_doc(cdt, cdn);
|
||||||
|
return {
|
||||||
|
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
|
||||||
|
filters: {
|
||||||
|
reference_doctype: row.reference_doctype,
|
||||||
|
reference_name: row.reference_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -191,7 +202,15 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.add_fetch(
|
||||||
|
"payment_request",
|
||||||
|
"outstanding_amount",
|
||||||
|
"payment_request_outstanding",
|
||||||
|
"Payment Entry Reference"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
erpnext.hide_company(frm);
|
erpnext.hide_company(frm);
|
||||||
frm.events.hide_unhide_fields(frm);
|
frm.events.hide_unhide_fields(frm);
|
||||||
@@ -216,6 +235,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||||
|
frappe.flags.allocate_payment_amount = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_company: (frm) => {
|
validate_company: (frm) => {
|
||||||
@@ -797,7 +817,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (frm.doc.payment_type == "Pay")
|
if (frm.doc.payment_type == "Pay")
|
||||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||||
else frm.events.set_unallocated_amount(frm);
|
else frm.events.set_unallocated_amount(frm);
|
||||||
|
|
||||||
frm.set_paid_amount_based_on_received_amount = false;
|
frm.set_paid_amount_based_on_received_amount = false;
|
||||||
@@ -818,7 +838,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.payment_type == "Receive")
|
if (frm.doc.payment_type == "Receive")
|
||||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
|
||||||
else frm.events.set_unallocated_amount(frm);
|
else frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -989,6 +1009,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
c.outstanding_amount = d.outstanding_amount;
|
c.outstanding_amount = d.outstanding_amount;
|
||||||
c.bill_no = d.bill_no;
|
c.bill_no = d.bill_no;
|
||||||
c.payment_term = d.payment_term;
|
c.payment_term = d.payment_term;
|
||||||
|
c.payment_term_outstanding = d.payment_term_outstanding;
|
||||||
c.allocated_amount = d.allocated_amount;
|
c.allocated_amount = d.allocated_amount;
|
||||||
c.account = d.account;
|
c.account = d.account;
|
||||||
|
|
||||||
@@ -1038,7 +1059,8 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
frm.events.allocate_party_amount_against_ref_docs(
|
frm.events.allocate_party_amount_against_ref_docs(
|
||||||
frm,
|
frm,
|
||||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
|
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1052,93 +1074,13 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
return ["Sales Invoice", "Purchase Invoice"];
|
return ["Sales Invoice", "Purchase Invoice"];
|
||||||
},
|
},
|
||||||
|
|
||||||
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
|
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||||
var total_positive_outstanding_including_order = 0;
|
await frm.call("allocate_amount_to_references", {
|
||||||
var total_negative_outstanding = 0;
|
paid_amount: paid_amount,
|
||||||
var total_deductions = frappe.utils.sum(
|
paid_amount_change: paid_amount_change,
|
||||||
$.map(frm.doc.deductions || [], function (d) {
|
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||||
return flt(d.amount);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
paid_amount -= total_deductions;
|
|
||||||
|
|
||||||
$.each(frm.doc.references || [], function (i, row) {
|
|
||||||
if (flt(row.outstanding_amount) > 0)
|
|
||||||
total_positive_outstanding_including_order += flt(row.outstanding_amount);
|
|
||||||
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var allocated_negative_outstanding = 0;
|
|
||||||
if (
|
|
||||||
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
|
|
||||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
|
|
||||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
|
|
||||||
) {
|
|
||||||
if (total_positive_outstanding_including_order > paid_amount) {
|
|
||||||
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
|
|
||||||
allocated_negative_outstanding =
|
|
||||||
total_negative_outstanding < remaining_outstanding
|
|
||||||
? total_negative_outstanding
|
|
||||||
: remaining_outstanding;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
|
||||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
|
||||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
|
|
||||||
if (paid_amount > total_negative_outstanding) {
|
|
||||||
if (total_negative_outstanding == 0) {
|
|
||||||
frappe.msgprint(
|
|
||||||
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
|
|
||||||
frm.doc.payment_type,
|
|
||||||
frm.doc.party_type == "Customer" ? "to" : "from",
|
|
||||||
frm.doc.party_type,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
frappe.msgprint(
|
|
||||||
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
|
|
||||||
total_negative_outstanding,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
|
|
||||||
allocated_negative_outstanding =
|
|
||||||
paid_amount +
|
|
||||||
(total_positive_outstanding_including_order < allocated_positive_outstanding
|
|
||||||
? total_positive_outstanding_including_order
|
|
||||||
: allocated_positive_outstanding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$.each(frm.doc.references || [], function (i, row) {
|
|
||||||
if (frappe.flags.allocate_payment_amount == 0) {
|
|
||||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
|
||||||
row.allocated_amount = 0;
|
|
||||||
} else if (
|
|
||||||
frappe.flags.allocate_payment_amount != 0 &&
|
|
||||||
(!row.allocated_amount || paid_amount_change)
|
|
||||||
) {
|
|
||||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
|
||||||
row.allocated_amount =
|
|
||||||
row.outstanding_amount >= allocated_positive_outstanding
|
|
||||||
? allocated_positive_outstanding
|
|
||||||
: row.outstanding_amount;
|
|
||||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
|
||||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
|
||||||
row.allocated_amount =
|
|
||||||
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
|
|
||||||
? -1 * allocated_negative_outstanding
|
|
||||||
: row.outstanding_amount;
|
|
||||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.refresh_fields();
|
|
||||||
frm.events.set_total_allocated_amount(frm);
|
frm.events.set_total_allocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1686,6 +1628,62 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
return current_tax_amount;
|
return current_tax_amount;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cost_center: function (frm) {
|
||||||
|
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||||
|
return frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||||
|
args: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
date: frm.doc.posting_date,
|
||||||
|
paid_from: frm.doc.paid_from,
|
||||||
|
paid_to: frm.doc.paid_to,
|
||||||
|
ptype: frm.doc.party_type,
|
||||||
|
pty: frm.doc.party,
|
||||||
|
cost_center: frm.doc.cost_center,
|
||||||
|
},
|
||||||
|
callback: function (r, rt) {
|
||||||
|
if (r.message) {
|
||||||
|
frappe.run_serially([
|
||||||
|
() => {
|
||||||
|
frm.set_value(
|
||||||
|
"paid_from_account_balance",
|
||||||
|
r.message.paid_from_account_balance
|
||||||
|
);
|
||||||
|
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||||
|
frm.set_value("party_balance", r.message.party_balance);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
after_save: function (frm) {
|
||||||
|
const { matched_payment_requests } = frappe.last_response;
|
||||||
|
if (!matched_payment_requests) return;
|
||||||
|
|
||||||
|
const COLUMN_LABEL = [
|
||||||
|
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __("Unset Matched Payment Request"),
|
||||||
|
message: COLUMN_LABEL.concat(matched_payment_requests),
|
||||||
|
as_table: true,
|
||||||
|
wide: true,
|
||||||
|
primary_action: {
|
||||||
|
label: __("Allocate Payment Request"),
|
||||||
|
action() {
|
||||||
|
frappe.hide_msgprint();
|
||||||
|
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
|
||||||
|
frm.dirty();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Entry Reference", {
|
frappe.ui.form.on("Payment Entry Reference", {
|
||||||
@@ -1778,35 +1776,3 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
|||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
frappe.ui.form.on("Payment Entry", {
|
|
||||||
cost_center: function (frm) {
|
|
||||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
|
||||||
return frappe.call({
|
|
||||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
|
||||||
args: {
|
|
||||||
company: frm.doc.company,
|
|
||||||
date: frm.doc.posting_date,
|
|
||||||
paid_from: frm.doc.paid_from,
|
|
||||||
paid_to: frm.doc.paid_to,
|
|
||||||
ptype: frm.doc.party_type,
|
|
||||||
pty: frm.doc.party,
|
|
||||||
cost_center: frm.doc.cost_center,
|
|
||||||
},
|
|
||||||
callback: function (r, rt) {
|
|
||||||
if (r.message) {
|
|
||||||
frappe.run_serially([
|
|
||||||
() => {
|
|
||||||
frm.set_value(
|
|
||||||
"paid_from_account_balance",
|
|
||||||
r.message.paid_from_account_balance
|
|
||||||
);
|
|
||||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
|
||||||
frm.set_value("party_balance", r.message.party_balance);
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from functools import reduce
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import ValidationError, _, qb, scrub, throw
|
from frappe import ValidationError, _, qb, scrub, throw
|
||||||
|
from frappe.query_builder import Tuple
|
||||||
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||||
from frappe.utils.data import comma_and, fmt_money
|
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||||
from pypika import Case
|
from pypika import Case
|
||||||
from pypika.functions import Coalesce, Sum
|
from pypika.functions import Coalesce, Sum
|
||||||
|
|
||||||
@@ -98,13 +100,17 @@ class PaymentEntry(AccountsController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_total_in_words()
|
self.set_total_in_words()
|
||||||
|
|
||||||
|
def before_save(self):
|
||||||
|
self.set_matched_unset_payment_requests_to_response()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
frappe.throw(_("Difference Amount must be zero"))
|
frappe.throw(_("Difference Amount must be zero"))
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
|
||||||
self.update_payment_schedule()
|
self.update_payment_schedule()
|
||||||
|
self.update_payment_requests()
|
||||||
|
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def set_liability_account(self):
|
def set_liability_account(self):
|
||||||
@@ -188,30 +194,34 @@ class PaymentEntry(AccountsController):
|
|||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
|
||||||
self.delink_advance_entry_references()
|
self.delink_advance_entry_references()
|
||||||
self.update_payment_schedule(cancel=1)
|
self.update_payment_schedule(cancel=1)
|
||||||
self.set_payment_req_status()
|
self.update_payment_requests(cancel=True)
|
||||||
|
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def set_payment_req_status(self):
|
def update_payment_requests(self, cancel=False):
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||||
|
update_payment_requests_as_per_pe_references,
|
||||||
|
)
|
||||||
|
|
||||||
update_payment_req_status(self, None)
|
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
|
||||||
|
|
||||||
def update_outstanding_amounts(self):
|
def update_outstanding_amounts(self):
|
||||||
self.set_missing_ref_details(force=True)
|
self.set_missing_ref_details(force=True)
|
||||||
|
|
||||||
def validate_duplicate_entry(self):
|
def validate_duplicate_entry(self):
|
||||||
reference_names = []
|
reference_names = set()
|
||||||
for d in self.get("references"):
|
for d in self.get("references"):
|
||||||
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
|
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
|
||||||
|
if key in reference_names:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
||||||
d.idx, d.reference_doctype, d.reference_name
|
d.idx, d.reference_doctype, d.reference_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
|
|
||||||
|
reference_names.add(key)
|
||||||
|
|
||||||
def set_bank_account_data(self):
|
def set_bank_account_data(self):
|
||||||
if self.bank_account:
|
if self.bank_account:
|
||||||
@@ -237,6 +247,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.payment_type == "Internal Transfer":
|
if self.payment_type == "Internal Transfer":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.validate_allocated_amount_as_per_payment_request()
|
||||||
|
|
||||||
if self.party_type in ("Customer", "Supplier"):
|
if self.party_type in ("Customer", "Supplier"):
|
||||||
self.validate_allocated_amount_with_latest_data()
|
self.validate_allocated_amount_with_latest_data()
|
||||||
else:
|
else:
|
||||||
@@ -249,6 +261,27 @@ class PaymentEntry(AccountsController):
|
|||||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||||
frappe.throw(fail_message.format(d.idx))
|
frappe.throw(fail_message.format(d.idx))
|
||||||
|
|
||||||
|
def validate_allocated_amount_as_per_payment_request(self):
|
||||||
|
"""
|
||||||
|
Allocated amount should not be greater than the outstanding amount of the Payment Request.
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
|
||||||
|
|
||||||
|
if not pr_outstanding_amounts:
|
||||||
|
return
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
|
||||||
|
frappe.throw(
|
||||||
|
msg=_(
|
||||||
|
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
|
||||||
|
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
|
||||||
|
title=_("Invalid Allocated Amount"),
|
||||||
|
)
|
||||||
|
|
||||||
def term_based_allocation_enabled_for_reference(
|
def term_based_allocation_enabled_for_reference(
|
||||||
self, reference_doctype: str, reference_name: str
|
self, reference_doctype: str, reference_name: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -1606,6 +1639,380 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
return current_tax_fraction
|
return current_tax_fraction
|
||||||
|
|
||||||
|
def set_matched_unset_payment_requests_to_response(self):
|
||||||
|
"""
|
||||||
|
Find matched Payment Requests for those references which have no Payment Request set.\n
|
||||||
|
And set to `frappe.response` to show in the frontend for allocation.
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
matched_payment_requests = get_matched_payment_request_of_references(
|
||||||
|
[row for row in self.references if not row.payment_request]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matched_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.response["matched_payment_requests"] = matched_payment_requests
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
|
||||||
|
"""
|
||||||
|
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
|
||||||
|
:param paid_amount: Paid Amount / Received Amount.
|
||||||
|
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
|
||||||
|
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not allocate_payment_amount:
|
||||||
|
for ref in self.references:
|
||||||
|
ref.allocated_amount = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# calculating outstanding amounts
|
||||||
|
precision = self.precision("paid_amount")
|
||||||
|
total_positive_outstanding_including_order = 0
|
||||||
|
total_negative_outstanding = 0
|
||||||
|
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
reference_outstanding_amount = ref.outstanding_amount
|
||||||
|
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||||
|
|
||||||
|
if reference_outstanding_amount > 0:
|
||||||
|
total_positive_outstanding_including_order += abs_outstanding_amount
|
||||||
|
else:
|
||||||
|
total_negative_outstanding += abs_outstanding_amount
|
||||||
|
|
||||||
|
# calculating allocated outstanding amounts
|
||||||
|
allocated_negative_outstanding = 0
|
||||||
|
allocated_positive_outstanding = 0
|
||||||
|
|
||||||
|
# checking party type and payment type
|
||||||
|
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
|
||||||
|
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
|
||||||
|
):
|
||||||
|
if total_positive_outstanding_including_order > paid_amount:
|
||||||
|
remaining_outstanding = flt(
|
||||||
|
total_positive_outstanding_including_order - paid_amount, precision
|
||||||
|
)
|
||||||
|
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
|
||||||
|
|
||||||
|
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||||
|
|
||||||
|
elif self.party_type in ("Supplier", "Employee"):
|
||||||
|
if paid_amount > total_negative_outstanding:
|
||||||
|
if total_negative_outstanding == 0:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||||
|
self.payment_type,
|
||||||
|
self.party_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||||
|
total_negative_outstanding
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
|
||||||
|
allocated_negative_outstanding = paid_amount + min(
|
||||||
|
total_positive_outstanding_including_order, allocated_positive_outstanding
|
||||||
|
)
|
||||||
|
|
||||||
|
# inner function to set `allocated_amount` to those row which have no PR
|
||||||
|
def _allocation_to_unset_pr_row(
|
||||||
|
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
|
||||||
|
):
|
||||||
|
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||||
|
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
|
||||||
|
allocated_positive_outstanding = flt(
|
||||||
|
allocated_positive_outstanding - row.allocated_amount, precision
|
||||||
|
)
|
||||||
|
elif outstanding_amount < 0 and allocated_negative_outstanding:
|
||||||
|
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
|
||||||
|
allocated_negative_outstanding = flt(
|
||||||
|
allocated_negative_outstanding - abs(row.allocated_amount), precision
|
||||||
|
)
|
||||||
|
return allocated_positive_outstanding, allocated_negative_outstanding
|
||||||
|
|
||||||
|
# allocate amount based on `paid_amount` is changed or not
|
||||||
|
if not paid_amount_change:
|
||||||
|
for ref in self.references:
|
||||||
|
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||||
|
ref,
|
||||||
|
ref.outstanding_amount,
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
payment_request_outstanding_amounts = (
|
||||||
|
get_payment_request_outstanding_set_in_references(self.references) or {}
|
||||||
|
)
|
||||||
|
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
|
||||||
|
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
|
||||||
|
|
||||||
|
# Re allocate amount to those references which have PR set (Higher priority)
|
||||||
|
for ref in self.references:
|
||||||
|
if not ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||||
|
reference_outstanding_amount = references_outstanding_amounts[key]
|
||||||
|
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
|
||||||
|
|
||||||
|
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||||
|
# allocate amount according to outstanding amounts
|
||||||
|
outstanding_amounts = (
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
reference_outstanding_amount,
|
||||||
|
pr_outstanding_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref.allocated_amount = min(outstanding_amounts)
|
||||||
|
|
||||||
|
# update amounts to track allocation
|
||||||
|
allocated_amount = ref.allocated_amount
|
||||||
|
allocated_positive_outstanding = flt(
|
||||||
|
allocated_positive_outstanding - allocated_amount, precision
|
||||||
|
)
|
||||||
|
remaining_references_allocated_amounts[key] = flt(
|
||||||
|
remaining_references_allocated_amounts[key] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
|
||||||
|
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
|
||||||
|
# allocate amount according to outstanding amounts
|
||||||
|
outstanding_amounts = (
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
abs(reference_outstanding_amount),
|
||||||
|
pr_outstanding_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref.allocated_amount = min(outstanding_amounts) * -1
|
||||||
|
|
||||||
|
# update amounts to track allocation
|
||||||
|
allocated_amount = abs(ref.allocated_amount)
|
||||||
|
allocated_negative_outstanding = flt(
|
||||||
|
allocated_negative_outstanding - allocated_amount, precision
|
||||||
|
)
|
||||||
|
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
# Re allocate amount to those references which have no PR (Lower priority)
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||||
|
reference_outstanding_amount = remaining_references_allocated_amounts[key]
|
||||||
|
|
||||||
|
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||||
|
ref,
|
||||||
|
reference_outstanding_amount,
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_matched_payment_requests(self, matched_payment_requests):
|
||||||
|
"""
|
||||||
|
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
|
||||||
|
:param matched_payment_requests: List of tuple of matched Payment Requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||||
|
"""
|
||||||
|
if not self.references or not matched_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(matched_payment_requests, str):
|
||||||
|
matched_payment_requests = json.loads(matched_payment_requests)
|
||||||
|
|
||||||
|
# modify matched_payment_requests
|
||||||
|
# like (reference_doctype, reference_name, allocated_amount): payment_request
|
||||||
|
payment_requests = {}
|
||||||
|
|
||||||
|
for row in matched_payment_requests:
|
||||||
|
key = tuple(row[:3])
|
||||||
|
payment_requests[key] = row[3]
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
|
||||||
|
|
||||||
|
if key in payment_requests:
|
||||||
|
ref.payment_request = payment_requests[key]
|
||||||
|
del payment_requests[key] # to avoid duplicate allocation
|
||||||
|
|
||||||
|
|
||||||
|
def get_matched_payment_request_of_references(references=None):
|
||||||
|
"""
|
||||||
|
Get those `Payment Requests` which are matched with `References`.\n
|
||||||
|
- Amount must be same.
|
||||||
|
- Only single `Payment Request` available for this amount.
|
||||||
|
|
||||||
|
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
# to fetch matched rows
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name, row.allocated_amount)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
# query to group by reference_doctype, reference_name, outstanding_amount
|
||||||
|
subquery = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(
|
||||||
|
PR.reference_doctype,
|
||||||
|
PR.reference_name,
|
||||||
|
PR.outstanding_amount.as_("allocated_amount"),
|
||||||
|
PR.name.as_("payment_request"),
|
||||||
|
Count("*").as_("count"),
|
||||||
|
)
|
||||||
|
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||||
|
.where(PR.status != "Paid")
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||||
|
)
|
||||||
|
|
||||||
|
# query to fetch matched rows which are single
|
||||||
|
matched_prs = (
|
||||||
|
frappe.qb.from_(subquery)
|
||||||
|
.select(
|
||||||
|
subquery.reference_doctype,
|
||||||
|
subquery.reference_name,
|
||||||
|
subquery.allocated_amount,
|
||||||
|
subquery.payment_request,
|
||||||
|
)
|
||||||
|
.where(subquery.count == 1)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
return matched_prs if matched_prs else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_references_outstanding_amount(references=None):
|
||||||
|
"""
|
||||||
|
Fetch accurate outstanding amount of `References`.\n
|
||||||
|
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
|
||||||
|
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
|
||||||
|
|
||||||
|
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
|
||||||
|
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
|
||||||
|
|
||||||
|
return {**refs_with_payment_term, **refs_without_payment_term}
|
||||||
|
|
||||||
|
|
||||||
|
def get_outstanding_of_references_with_payment_term(references=None):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `References` which have `Payment Term` set.\n
|
||||||
|
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name, row.payment_term)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.payment_term
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PS = frappe.qb.DocType("Payment Schedule")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PS)
|
||||||
|
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
|
||||||
|
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
|
||||||
|
|
||||||
|
|
||||||
|
def get_outstanding_of_references_with_no_payment_term(references):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||||
|
- Fetch outstanding amount from `References` it self.
|
||||||
|
|
||||||
|
Note: `None` is used for allocation of `Payment Request`
|
||||||
|
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
outstanding_amounts = {}
|
||||||
|
|
||||||
|
for ref in references:
|
||||||
|
if ref.payment_term:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, None)
|
||||||
|
|
||||||
|
if key not in outstanding_amounts:
|
||||||
|
outstanding_amounts[key] = ref.outstanding_amount
|
||||||
|
|
||||||
|
return outstanding_amounts
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_request_outstanding_set_in_references(references=None):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
|
||||||
|
Example: {payment_request: outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
|
||||||
|
|
||||||
|
if not referenced_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(PR.name, PR.outstanding_amount)
|
||||||
|
.where(PR.name.isin(referenced_payment_requests))
|
||||||
|
).run()
|
||||||
|
|
||||||
|
return dict(response) if response else None
|
||||||
|
|
||||||
|
|
||||||
def validate_inclusive_tax(tax, doc):
|
def validate_inclusive_tax(tax, doc):
|
||||||
def _on_previous_row_error(row_range):
|
def _on_previous_row_error(row_range):
|
||||||
@@ -2236,6 +2643,8 @@ def get_payment_entry(
|
|||||||
party_type=None,
|
party_type=None,
|
||||||
payment_type=None,
|
payment_type=None,
|
||||||
reference_date=None,
|
reference_date=None,
|
||||||
|
ignore_permissions=False,
|
||||||
|
created_from_payment_request=False,
|
||||||
):
|
):
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||||
@@ -2385,9 +2794,179 @@ def get_payment_entry(
|
|||||||
|
|
||||||
pe.set_difference_amount()
|
pe.set_difference_amount()
|
||||||
|
|
||||||
|
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||||
|
if not created_from_payment_request:
|
||||||
|
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
|
||||||
|
|
||||||
return pe
|
return pe
|
||||||
|
|
||||||
|
|
||||||
|
def get_open_payment_requests_for_references(references=None):
|
||||||
|
"""
|
||||||
|
Fetch all unpaid Payment Requests for the references. \n
|
||||||
|
- Each reference can have multiple Payment Requests. \n
|
||||||
|
|
||||||
|
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||||
|
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||||
|
.where(PR.status != "Paid")
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
reference_payment_requests = {}
|
||||||
|
|
||||||
|
for row in response:
|
||||||
|
key = (row.reference_doctype, row.reference_name)
|
||||||
|
|
||||||
|
if key not in reference_payment_requests:
|
||||||
|
reference_payment_requests[key] = {row.name: row.outstanding_amount}
|
||||||
|
else:
|
||||||
|
reference_payment_requests[key][row.name] = row.outstanding_amount
|
||||||
|
|
||||||
|
return reference_payment_requests
|
||||||
|
|
||||||
|
|
||||||
|
def allocate_open_payment_requests_to_references(references=None, precision=None):
|
||||||
|
"""
|
||||||
|
Allocate unpaid Payment Requests to the references. \n
|
||||||
|
---
|
||||||
|
- Allocation based on below factors
|
||||||
|
- Reference Allocated Amount
|
||||||
|
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
|
||||||
|
- Reference Payment Request's outstanding amount
|
||||||
|
---
|
||||||
|
- Allocation based on below scenarios
|
||||||
|
- Reference's Allocated Amount == Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- This PR will not be allocated further
|
||||||
|
- Reference's Allocated Amount < Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- Reduce the PR's outstanding amount by the allocated amount
|
||||||
|
- This PR can be allocated further
|
||||||
|
- Reference's Allocated Amount > Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- Reduce Allocated Amount of the reference by the PR's outstanding amount
|
||||||
|
- Create a new row for the remaining amount until the Allocated Amount is 0
|
||||||
|
- Allocate PR if available
|
||||||
|
---
|
||||||
|
- Note:
|
||||||
|
- Priority is given to the first Payment Request of respective references.
|
||||||
|
- Single Reference can have multiple rows.
|
||||||
|
- With Payment Terms or without Payment Terms
|
||||||
|
- With Payment Request or without Payment Request
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get all unpaid payment requests for the references
|
||||||
|
references_open_payment_requests = get_open_payment_requests_for_references(references)
|
||||||
|
|
||||||
|
if not references_open_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not precision:
|
||||||
|
precision = references[0].precision("allocated_amount")
|
||||||
|
|
||||||
|
# to manage new rows
|
||||||
|
row_number = 1
|
||||||
|
MOVE_TO_NEXT_ROW = 1
|
||||||
|
TO_SKIP_NEW_ROW = 2
|
||||||
|
|
||||||
|
while row_number <= len(references):
|
||||||
|
row = references[row_number - 1]
|
||||||
|
reference_key = (row.reference_doctype, row.reference_name)
|
||||||
|
|
||||||
|
# update the idx to maintain the order
|
||||||
|
row.idx = row_number
|
||||||
|
|
||||||
|
# unpaid payment requests for the reference
|
||||||
|
reference_payment_requests = references_open_payment_requests.get(reference_key)
|
||||||
|
|
||||||
|
if not reference_payment_requests:
|
||||||
|
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get the first payment request and its outstanding amount
|
||||||
|
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
|
||||||
|
allocated_amount = row.allocated_amount
|
||||||
|
|
||||||
|
# allocate the payment request to the reference and PR's outstanding amount
|
||||||
|
row.payment_request = payment_request
|
||||||
|
|
||||||
|
if pr_outstanding_amount == allocated_amount:
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
elif pr_outstanding_amount > allocated_amount:
|
||||||
|
# reduce the outstanding amount of the payment request
|
||||||
|
reference_payment_requests[payment_request] -= allocated_amount
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
else:
|
||||||
|
# split the reference row to allocate the remaining amount
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row.allocated_amount = pr_outstanding_amount
|
||||||
|
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||||
|
|
||||||
|
# set the remaining amount to the next row
|
||||||
|
while allocated_amount:
|
||||||
|
# create a new row for the remaining amount
|
||||||
|
new_row = frappe.copy_doc(row)
|
||||||
|
references.insert(row_number, new_row)
|
||||||
|
|
||||||
|
# get the first payment request and its outstanding amount
|
||||||
|
payment_request, pr_outstanding_amount = next(
|
||||||
|
iter(reference_payment_requests.items()), (None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# update new row
|
||||||
|
new_row.idx = row_number + 1
|
||||||
|
new_row.payment_request = payment_request
|
||||||
|
new_row.allocated_amount = min(
|
||||||
|
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_request or not pr_outstanding_amount:
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
elif pr_outstanding_amount == allocated_amount:
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
elif pr_outstanding_amount > allocated_amount:
|
||||||
|
reference_payment_requests[payment_request] -= allocated_amount
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
|
||||||
def update_accounting_dimensions(pe, doc):
|
def update_accounting_dimensions(pe, doc):
|
||||||
"""
|
"""
|
||||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"due_date",
|
"due_date",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"payment_term",
|
"payment_term",
|
||||||
|
"payment_term_outstanding",
|
||||||
"account_type",
|
"account_type",
|
||||||
"payment_type",
|
"payment_type",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
@@ -18,7 +19,9 @@
|
|||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
"exchange_gain_loss",
|
"exchange_gain_loss",
|
||||||
"account"
|
"account",
|
||||||
|
"payment_request",
|
||||||
|
"payment_request_outstanding"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -120,12 +123,33 @@
|
|||||||
"fieldname": "payment_type",
|
"fieldname": "payment_type",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Payment Type"
|
"label": "Payment Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_request",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Payment Request",
|
||||||
|
"options": "Payment Request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.payment_term",
|
||||||
|
"fieldname": "payment_term_outstanding",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Payment Term Outstanding",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
|
||||||
|
"fieldname": "payment_request_outstanding",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"is_virtual": 1,
|
||||||
|
"label": "Payment Request Outstanding",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-05 09:44:08.310593",
|
"modified": "2024-09-16 18:11:50.019343",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
payment_request: DF.Link | None
|
||||||
|
payment_request_outstanding: DF.Float
|
||||||
payment_term: DF.Link | None
|
payment_term: DF.Link | None
|
||||||
|
payment_term_outstanding: DF.Float
|
||||||
payment_type: DF.Data | None
|
payment_type: DF.Data | None
|
||||||
reference_doctype: DF.Link
|
reference_doctype: DF.Link
|
||||||
reference_name: DF.DynamicLink
|
reference_name: DF.DynamicLink
|
||||||
total_amount: DF.Float
|
total_amount: DF.Float
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
@property
|
||||||
|
def payment_request_outstanding(self):
|
||||||
|
if not self.payment_request:
|
||||||
|
return
|
||||||
|
|
||||||
|
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
|
frm.doc.payment_request_type == "Outward" &&
|
||||||
frm.doc.status == "Initiated"
|
["Initiated", "Partially Paid"].includes(frm.doc.status)
|
||||||
) {
|
) {
|
||||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
"reference_name",
|
"reference_name",
|
||||||
"transaction_details",
|
"transaction_details",
|
||||||
"grand_total",
|
"grand_total",
|
||||||
|
"currency",
|
||||||
"is_a_subscription",
|
"is_a_subscription",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
"currency",
|
"outstanding_amount",
|
||||||
|
"party_account_currency",
|
||||||
"subscription_section",
|
"subscription_section",
|
||||||
"subscription_plans",
|
"subscription_plans",
|
||||||
"bank_account_details",
|
"bank_account_details",
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "transaction_date",
|
"fieldname": "transaction_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Transaction Date"
|
"label": "Transaction Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,7 +136,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "reference_doctype",
|
"options": "reference_doctype",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "transaction_details",
|
"fieldname": "transaction_details",
|
||||||
@@ -141,12 +145,14 @@
|
|||||||
"label": "Transaction Details"
|
"label": "Transaction Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Amount in customer's currency",
|
"description": "Amount in transaction currency",
|
||||||
"fieldname": "grand_total",
|
"fieldname": "grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"non_negative": 1,
|
"non_negative": 1,
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -392,19 +398,37 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.docstatus === 1",
|
||||||
|
"description": "Amount in party's bank account currency",
|
||||||
|
"fieldname": "outstanding_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_preview": 1,
|
||||||
|
"label": "Outstanding Amount",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "party_account_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_account_currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Party Account Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-08-07 16:39:54.288002",
|
"modified": "2024-09-16 17:50:54.440090",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
@@ -439,6 +463,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_preview_popup": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue
|
from frappe.utils.background_jobs import enqueue
|
||||||
|
|
||||||
|
from erpnext import get_company_currency
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
@@ -45,6 +47,7 @@ class PaymentRequest(Document):
|
|||||||
bank_account: DF.Link | None
|
bank_account: DF.Link | None
|
||||||
bank_account_no: DF.ReadOnly | None
|
bank_account_no: DF.ReadOnly | None
|
||||||
branch_code: DF.ReadOnly | None
|
branch_code: DF.ReadOnly | None
|
||||||
|
company: DF.Link | None
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
currency: DF.Link | None
|
currency: DF.Link | None
|
||||||
email_to: DF.Data | None
|
email_to: DF.Data | None
|
||||||
@@ -56,16 +59,18 @@ class PaymentRequest(Document):
|
|||||||
mode_of_payment: DF.Link | None
|
mode_of_payment: DF.Link | None
|
||||||
mute_email: DF.Check
|
mute_email: DF.Check
|
||||||
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
||||||
|
outstanding_amount: DF.Currency
|
||||||
party: DF.DynamicLink | None
|
party: DF.DynamicLink | None
|
||||||
|
party_account_currency: DF.Link | None
|
||||||
party_type: DF.Link | None
|
party_type: DF.Link | None
|
||||||
payment_account: DF.ReadOnly | None
|
payment_account: DF.ReadOnly | None
|
||||||
payment_channel: DF.Literal["", "Email", "Phone"]
|
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
|
||||||
payment_gateway: DF.ReadOnly | None
|
payment_gateway: DF.ReadOnly | None
|
||||||
payment_gateway_account: DF.Link | None
|
payment_gateway_account: DF.Link | None
|
||||||
payment_order: DF.Link | None
|
payment_order: DF.Link | None
|
||||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||||
payment_url: DF.Data | None
|
payment_url: DF.Data | None
|
||||||
print_format: DF.Literal
|
print_format: DF.Literal[None]
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
reference_doctype: DF.Link | None
|
reference_doctype: DF.Link | None
|
||||||
reference_name: DF.DynamicLink | None
|
reference_name: DF.DynamicLink | None
|
||||||
@@ -84,7 +89,6 @@ class PaymentRequest(Document):
|
|||||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||||
swift_number: DF.ReadOnly | None
|
swift_number: DF.ReadOnly | None
|
||||||
transaction_date: DF.Date | None
|
transaction_date: DF.Date | None
|
||||||
company: DF.Link | None
|
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -100,6 +104,12 @@ class PaymentRequest(Document):
|
|||||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||||
|
|
||||||
def validate_payment_request_amount(self):
|
def validate_payment_request_amount(self):
|
||||||
|
if self.grand_total == 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
|
||||||
|
title=_("Invalid Amount"),
|
||||||
|
)
|
||||||
|
|
||||||
existing_payment_request_amount = flt(
|
existing_payment_request_amount = flt(
|
||||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||||
)
|
)
|
||||||
@@ -147,6 +157,28 @@ class PaymentRequest(Document):
|
|||||||
).format(self.grand_total, amount)
|
).format(self.grand_total, amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
if (
|
||||||
|
self.currency != self.party_account_currency
|
||||||
|
and self.party_account_currency == get_company_currency(self.company)
|
||||||
|
):
|
||||||
|
# set outstanding amount in party account currency
|
||||||
|
invoice = frappe.get_value(
|
||||||
|
self.reference_doctype,
|
||||||
|
self.reference_name,
|
||||||
|
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
|
||||||
|
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
|
||||||
|
self.outstanding_amount = flt(
|
||||||
|
self.grand_total / grand_total * base_grand_total,
|
||||||
|
self.precision("outstanding_amount"),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.outstanding_amount = self.grand_total
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.payment_request_type == "Outward":
|
if self.payment_request_type == "Outward":
|
||||||
self.db_set("status", "Initiated")
|
self.db_set("status", "Initiated")
|
||||||
@@ -275,7 +307,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
def set_as_paid(self):
|
def set_as_paid(self):
|
||||||
if self.payment_channel == "Phone":
|
if self.payment_channel == "Phone":
|
||||||
self.db_set("status", "Paid")
|
self.db_set({"status": "Paid", "outstanding_amount": 0})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
payment_entry = self.create_payment_entry()
|
payment_entry = self.create_payment_entry()
|
||||||
@@ -296,26 +328,32 @@ class PaymentRequest(Document):
|
|||||||
else:
|
else:
|
||||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||||
|
|
||||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
party_account_currency = (
|
||||||
|
self.get("party_account_currency")
|
||||||
|
or ref_doc.get("party_account_currency")
|
||||||
|
or get_account_currency(party_account)
|
||||||
|
)
|
||||||
|
|
||||||
|
party_amount = bank_amount = self.outstanding_amount
|
||||||
|
|
||||||
bank_amount = self.grand_total
|
|
||||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
exchange_rate = ref_doc.get("conversion_rate")
|
||||||
else:
|
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
|
||||||
party_amount = self.grand_total
|
|
||||||
|
|
||||||
|
# outstanding amount is already in Part's account currency
|
||||||
payment_entry = get_payment_entry(
|
payment_entry = get_payment_entry(
|
||||||
self.reference_doctype,
|
self.reference_doctype,
|
||||||
self.reference_name,
|
self.reference_name,
|
||||||
party_amount=party_amount,
|
party_amount=party_amount,
|
||||||
bank_account=self.payment_account,
|
bank_account=self.payment_account,
|
||||||
bank_amount=bank_amount,
|
bank_amount=bank_amount,
|
||||||
|
created_from_payment_request=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_entry.update(
|
payment_entry.update(
|
||||||
{
|
{
|
||||||
"mode_of_payment": self.mode_of_payment,
|
"mode_of_payment": self.mode_of_payment,
|
||||||
"reference_no": self.name,
|
"reference_no": self.name, # to prevent validation error
|
||||||
"reference_date": nowdate(),
|
"reference_date": nowdate(),
|
||||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||||
self.reference_doctype, self.reference_name, self.name
|
self.reference_doctype, self.reference_name, self.name
|
||||||
@@ -323,6 +361,9 @@ class PaymentRequest(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
|
||||||
|
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
|
||||||
|
|
||||||
# Update dimensions
|
# Update dimensions
|
||||||
payment_entry.update(
|
payment_entry.update(
|
||||||
{
|
{
|
||||||
@@ -331,14 +372,6 @@ class PaymentRequest(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
|
||||||
amount = payment_entry.base_paid_amount
|
|
||||||
else:
|
|
||||||
amount = self.grand_total
|
|
||||||
|
|
||||||
payment_entry.received_amount = amount
|
|
||||||
payment_entry.get("references")[0].allocated_amount = amount
|
|
||||||
|
|
||||||
# Update 'Paid Amount' on Forex transactions
|
# Update 'Paid Amount' on Forex transactions
|
||||||
if self.currency != ref_doc.company_currency:
|
if self.currency != ref_doc.company_currency:
|
||||||
if (
|
if (
|
||||||
@@ -429,6 +462,62 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
return create_stripe_subscription(gateway_controller, data)
|
return create_stripe_subscription(gateway_controller, data)
|
||||||
|
|
||||||
|
def _allocate_payment_request_to_pe_references(self, references):
|
||||||
|
"""
|
||||||
|
Allocate the Payment Request to the Payment Entry references based on\n
|
||||||
|
- Allocated Amount.
|
||||||
|
- Outstanding Amount of Payment Request.\n
|
||||||
|
Payment Request is doc itself and references are the rows of Payment Entry.
|
||||||
|
"""
|
||||||
|
if len(references) == 1:
|
||||||
|
references[0].payment_request = self.name
|
||||||
|
return
|
||||||
|
|
||||||
|
precision = references[0].precision("allocated_amount")
|
||||||
|
outstanding_amount = self.outstanding_amount
|
||||||
|
|
||||||
|
# to manage rows
|
||||||
|
row_number = 1
|
||||||
|
MOVE_TO_NEXT_ROW = 1
|
||||||
|
TO_SKIP_NEW_ROW = 2
|
||||||
|
NEW_ROW_ADDED = False
|
||||||
|
|
||||||
|
while row_number <= len(references):
|
||||||
|
row = references[row_number - 1]
|
||||||
|
|
||||||
|
# update the idx to maintain the order
|
||||||
|
row.idx = row_number
|
||||||
|
|
||||||
|
if outstanding_amount == 0:
|
||||||
|
if not NEW_ROW_ADDED:
|
||||||
|
break
|
||||||
|
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
continue
|
||||||
|
|
||||||
|
# allocate the payment request to the row
|
||||||
|
row.payment_request = self.name
|
||||||
|
|
||||||
|
if row.allocated_amount <= outstanding_amount:
|
||||||
|
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
else:
|
||||||
|
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
|
||||||
|
row.allocated_amount = outstanding_amount
|
||||||
|
outstanding_amount = 0
|
||||||
|
|
||||||
|
# create a new row without PR for remaining unallocated amount
|
||||||
|
new_row = frappe.copy_doc(row)
|
||||||
|
references.insert(row_number, new_row)
|
||||||
|
|
||||||
|
# update new row
|
||||||
|
new_row.idx = row_number + 1
|
||||||
|
new_row.payment_request = None
|
||||||
|
new_row.allocated_amount = remaining_allocated_amount
|
||||||
|
|
||||||
|
NEW_ROW_ADDED = True
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def make_payment_request(**args):
|
def make_payment_request(**args):
|
||||||
@@ -459,11 +548,15 @@ def make_payment_request(**args):
|
|||||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
# fetches existing payment request `grand_total` amount
|
||||||
|
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||||
|
|
||||||
if existing_payment_request_amount:
|
if existing_payment_request_amount:
|
||||||
grand_total -= existing_payment_request_amount
|
grand_total -= existing_payment_request_amount
|
||||||
|
|
||||||
|
if not grand_total:
|
||||||
|
frappe.throw(_("Payment Request is already created"))
|
||||||
|
|
||||||
if draft_payment_request:
|
if draft_payment_request:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||||
@@ -477,6 +570,13 @@ def make_payment_request(**args):
|
|||||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
party_type = args.get("party_type") or "Customer"
|
||||||
|
party_account_currency = ref_doc.party_account_currency
|
||||||
|
|
||||||
|
if not party_account_currency:
|
||||||
|
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||||
|
party_account_currency = get_account_currency(party_account)
|
||||||
|
|
||||||
pr.update(
|
pr.update(
|
||||||
{
|
{
|
||||||
"payment_gateway_account": gateway_account.get("name"),
|
"payment_gateway_account": gateway_account.get("name"),
|
||||||
@@ -485,6 +585,7 @@ def make_payment_request(**args):
|
|||||||
"payment_channel": gateway_account.get("payment_channel"),
|
"payment_channel": gateway_account.get("payment_channel"),
|
||||||
"payment_request_type": args.get("payment_request_type"),
|
"payment_request_type": args.get("payment_request_type"),
|
||||||
"currency": ref_doc.currency,
|
"currency": ref_doc.currency,
|
||||||
|
"party_account_currency": party_account_currency,
|
||||||
"grand_total": grand_total,
|
"grand_total": grand_total,
|
||||||
"mode_of_payment": args.mode_of_payment,
|
"mode_of_payment": args.mode_of_payment,
|
||||||
"email_to": args.recipient_id or ref_doc.owner,
|
"email_to": args.recipient_id or ref_doc.owner,
|
||||||
@@ -493,7 +594,7 @@ def make_payment_request(**args):
|
|||||||
"reference_doctype": args.dt,
|
"reference_doctype": args.dt,
|
||||||
"reference_name": args.dn,
|
"reference_name": args.dn,
|
||||||
"company": ref_doc.get("company"),
|
"company": ref_doc.get("company"),
|
||||||
"party_type": args.get("party_type") or "Customer",
|
"party_type": party_type,
|
||||||
"party": args.get("party") or ref_doc.get("customer"),
|
"party": args.get("party") or ref_doc.get("customer"),
|
||||||
"bank_account": bank_account,
|
"bank_account": bank_account,
|
||||||
}
|
}
|
||||||
@@ -539,9 +640,11 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if not ref_doc.get("is_pos"):
|
if not ref_doc.get("is_pos"):
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
grand_total = flt(ref_doc.grand_total)
|
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
grand_total = flt(
|
||||||
|
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||||
|
)
|
||||||
elif dt == "Sales Invoice":
|
elif dt == "Sales Invoice":
|
||||||
for pay in ref_doc.payments:
|
for pay in ref_doc.payments:
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
@@ -563,24 +666,20 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
|
|
||||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||||
"""
|
"""
|
||||||
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
Return the total amount of Payment Requests against a reference document.
|
||||||
and get the summation of existing paid payment request for Phone payment channel.
|
|
||||||
"""
|
"""
|
||||||
existing_payment_request_amount = frappe.db.sql(
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
"""
|
|
||||||
select sum(grand_total)
|
response = (
|
||||||
from `tabPayment Request`
|
frappe.qb.from_(PR)
|
||||||
where
|
.select(Sum(PR.grand_total))
|
||||||
reference_doctype = %s
|
.where(PR.reference_doctype == ref_dt)
|
||||||
and reference_name = %s
|
.where(PR.reference_name == ref_dn)
|
||||||
and docstatus = 1
|
.where(PR.docstatus == 1)
|
||||||
and (status != 'Paid'
|
.run()
|
||||||
or (payment_channel = 'Phone'
|
|
||||||
and status = 'Paid'))
|
|
||||||
""",
|
|
||||||
(ref_dt, ref_dn),
|
|
||||||
)
|
)
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
|
||||||
|
return response[0][0] if response[0] else 0
|
||||||
|
|
||||||
|
|
||||||
def get_gateway_details(args): # nosemgrep
|
def get_gateway_details(args): # nosemgrep
|
||||||
@@ -627,41 +726,66 @@ def make_payment_entry(docname):
|
|||||||
return doc.create_payment_entry(submit=False).as_dict()
|
return doc.create_payment_entry(submit=False).as_dict()
|
||||||
|
|
||||||
|
|
||||||
def update_payment_req_status(doc, method):
|
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
|
"""
|
||||||
|
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
for ref in doc.references:
|
precision = references[0].precision("allocated_amount")
|
||||||
payment_request_name = frappe.db.get_value(
|
|
||||||
"Payment Request",
|
referenced_payment_requests = frappe.get_all(
|
||||||
{
|
"Payment Request",
|
||||||
"reference_doctype": ref.reference_doctype,
|
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||||
"reference_name": ref.reference_name,
|
fields=[
|
||||||
"docstatus": 1,
|
"name",
|
||||||
},
|
"grand_total",
|
||||||
|
"outstanding_amount",
|
||||||
|
"payment_request_type",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||||
|
|
||||||
|
for ref in references:
|
||||||
|
if not ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
payment_request = referenced_payment_requests[ref.payment_request]
|
||||||
|
pr_outstanding = payment_request["outstanding_amount"]
|
||||||
|
|
||||||
|
# update outstanding amount
|
||||||
|
new_outstanding_amount = flt(
|
||||||
|
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
|
||||||
|
precision,
|
||||||
)
|
)
|
||||||
|
|
||||||
if payment_request_name:
|
# to handle same payment request for the multiple allocations
|
||||||
ref_details = get_reference_details(
|
payment_request["outstanding_amount"] = new_outstanding_amount
|
||||||
ref.reference_doctype,
|
|
||||||
ref.reference_name,
|
if not cancel and new_outstanding_amount < 0:
|
||||||
doc.party_account_currency,
|
frappe.throw(
|
||||||
doc.party_type,
|
msg=_(
|
||||||
doc.party,
|
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
|
||||||
|
).format(ref.payment_request),
|
||||||
|
title=_("Invalid Allocated Amount"),
|
||||||
)
|
)
|
||||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
|
||||||
status = pay_req_doc.status
|
|
||||||
|
|
||||||
if status != "Paid" and not ref_details.outstanding_amount:
|
# update status
|
||||||
status = "Paid"
|
if new_outstanding_amount == payment_request["grand_total"]:
|
||||||
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
|
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||||
status = "Partially Paid"
|
elif new_outstanding_amount == 0:
|
||||||
elif ref_details.outstanding_amount == ref_details.total_amount:
|
status = "Paid"
|
||||||
if pay_req_doc.payment_request_type == "Outward":
|
elif new_outstanding_amount > 0:
|
||||||
status = "Initiated"
|
status = "Partially Paid"
|
||||||
elif pay_req_doc.payment_request_type == "Inward":
|
|
||||||
status = "Requested"
|
|
||||||
|
|
||||||
pay_req_doc.db_set("status", status)
|
# update database
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Payment Request",
|
||||||
|
ref.payment_request,
|
||||||
|
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_message(doc):
|
def get_dummy_message(doc):
|
||||||
@@ -745,3 +869,35 @@ def validate_payment(doc, method=None):
|
|||||||
doc.reference_docname
|
doc.reference_docname
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
# permission checks in `get_list()`
|
||||||
|
reference_doctype = filters.get("reference_doctype")
|
||||||
|
reference_name = filters.get("reference_doctype")
|
||||||
|
|
||||||
|
if not reference_doctype or not reference_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
open_payment_requests = frappe.get_list(
|
||||||
|
"Payment Request",
|
||||||
|
filters={
|
||||||
|
"reference_doctype": filters["reference_doctype"],
|
||||||
|
"reference_name": filters["reference_name"],
|
||||||
|
"status": ["!=", "Paid"],
|
||||||
|
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fields=["name", "grand_total", "outstanding_amount"],
|
||||||
|
order_by="transaction_date ASC,creation ASC",
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
pr.name,
|
||||||
|
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||||
|
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||||
|
)
|
||||||
|
for pr in open_payment_requests
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@@ -15,6 +17,7 @@ from erpnext.setup.utils import get_exchange_rate
|
|||||||
|
|
||||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||||
|
|
||||||
|
|
||||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||||
|
|
||||||
payment_method = [
|
payment_method = [
|
||||||
@@ -278,3 +281,246 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
self.assertEqual(pe.paid_amount, 800)
|
self.assertEqual(pe.paid_amount, 800)
|
||||||
self.assertEqual(pe.base_received_amount, 800)
|
self.assertEqual(pe.base_received_amount, 800)
|
||||||
self.assertEqual(pe.received_amount, 10)
|
self.assertEqual(pe.received_amount, 10)
|
||||||
|
|
||||||
|
def test_multiple_payment_if_partially_paid_for_same_currency(self):
|
||||||
|
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
# to make partial payment
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 200
|
||||||
|
pe.references[0].allocated_amount = 200
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 800)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# complete payment
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
|
||||||
|
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||||
|
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# creating a more payment Request must not allowed
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||||
|
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
|
||||||
|
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
|
||||||
|
pi.credit_to = "Creditors - _TC"
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
dn=pi.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 100 USD -> 5000 INR
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
self.assertEqual(pr.outstanding_amount, 5000)
|
||||||
|
self.assertEqual(pr.currency, "USD")
|
||||||
|
self.assertEqual(pr.party_account_currency, "INR")
|
||||||
|
self.assertEqual(pr.status, "Initiated")
|
||||||
|
|
||||||
|
# to make partial payment
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 2000
|
||||||
|
pe.references[0].allocated_amount = 2000
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 3000)
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
|
||||||
|
# complete payment
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 3000)
|
||||||
|
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
|
||||||
|
# creating a more payment Request must not allowed
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
dn=pi.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_payment_with_payment_term_for_same_currency(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
|
||||||
|
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||||
|
po.save()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Order",
|
||||||
|
dn=po.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 20000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||||
|
self.assertEqual(pr.status, "Initiated")
|
||||||
|
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
|
||||||
|
self.assertEqual(len(pe.references), 2)
|
||||||
|
self.assertEqual(pe.paid_amount, 20000)
|
||||||
|
|
||||||
|
# check 1st payment term
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
# check 2nd payment term
|
||||||
|
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
|
||||||
|
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||||
|
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 20000)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||||
|
def test_single_payment_with_payment_term_for_multi_currency(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
si = create_sales_invoice(
|
||||||
|
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
|
||||||
|
)
|
||||||
|
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Invoice",
|
||||||
|
dn=si.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 200 USD -> 10000 INR
|
||||||
|
self.assertEqual(pr.grand_total, 200)
|
||||||
|
self.assertEqual(pr.outstanding_amount, 10000)
|
||||||
|
self.assertEqual(pr.currency, "USD")
|
||||||
|
self.assertEqual(pr.party_account_currency, "INR")
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
self.assertEqual(len(pe.references), 2)
|
||||||
|
self.assertEqual(pe.paid_amount, 10000)
|
||||||
|
|
||||||
|
# check 1st payment term
|
||||||
|
# convert it via dollar and conversion_rate
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
# check 2nd payment term
|
||||||
|
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
|
||||||
|
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 200)
|
||||||
|
|
||||||
|
def test_payment_cancel_process(self):
|
||||||
|
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 800
|
||||||
|
pe.references[0].allocated_amount = 800
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 200)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# cancelling PE
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Requested")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 1000)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|||||||
@@ -392,8 +392,7 @@ def process_closing_entries(gl_entries, closing_entries, voucher_name, company,
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if gl_entries + closing_entries:
|
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.log_error(e)
|
frappe.log_error(e)
|
||||||
|
|||||||
@@ -862,6 +862,7 @@ def get_item_group(pos_profile):
|
|||||||
if pos_profile.get("item_groups"):
|
if pos_profile.get("item_groups"):
|
||||||
# Get items based on the item groups defined in the POS profile
|
# Get items based on the item groups defined in the POS profile
|
||||||
for row in pos_profile.get("item_groups"):
|
for row in pos_profile.get("item_groups"):
|
||||||
|
item_groups.append(row.item_group)
|
||||||
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||||
|
|
||||||
return list(set(item_groups))
|
return list(set(item_groups))
|
||||||
|
|||||||
@@ -561,11 +561,12 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
"Purchase Invoice": "Return / Debit Note",
|
"Purchase Invoice": "Return / Debit Note",
|
||||||
"Payment Entry": "Payment",
|
"Payment Entry": "Payment",
|
||||||
"Landed Cost Voucher": function () {
|
|
||||||
frm.trigger("create_landed_cost_voucher");
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (frm.doc.update_stock) {
|
||||||
|
frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher";
|
||||||
|
}
|
||||||
|
|
||||||
frm.set_query("additional_discount_account", function () {
|
frm.set_query("additional_discount_account", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -607,20 +608,6 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_landed_cost_voucher: function (frm) {
|
|
||||||
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
|
|
||||||
lcv.company = frm.doc.company;
|
|
||||||
|
|
||||||
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice");
|
|
||||||
lcv_receipt.receipt_document_type = "Purchase Invoice";
|
|
||||||
lcv_receipt.receipt_document = frm.doc.name;
|
|
||||||
lcv_receipt.supplier = frm.doc.supplier;
|
|
||||||
lcv_receipt.grand_total = frm.doc.grand_total;
|
|
||||||
lcv.purchase_receipts = [lcv_receipt];
|
|
||||||
|
|
||||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
|
||||||
},
|
|
||||||
|
|
||||||
add_custom_buttons: function (frm) {
|
add_custom_buttons: function (frm) {
|
||||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
@@ -645,6 +632,32 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
__("View")
|
__("View")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Landed Cost Voucher"),
|
||||||
|
() => {
|
||||||
|
frm.events.make_lcv(frm);
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
make_lcv(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
|
||||||
|
args: {
|
||||||
|
doctype: frm.doc.doctype,
|
||||||
|
docname: frm.doc.name,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
|||||||
@@ -2123,7 +2123,7 @@ def make_delivery_note(source_name, target_doc=None):
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {
|
"Sales Team": {
|
||||||
"doctype": "Sales Team",
|
"doctype": "Sales Team",
|
||||||
"field_map": {"incentives": "incentives"},
|
"field_map": {"incentives": "incentives"},
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
else:
|
else:
|
||||||
# if no TCS has been charged in FY,
|
# if no TCS has been charged in FY,
|
||||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||||
|
|
||||||
if cint(tax_details.round_off_tax_amount):
|
if cint(tax_details.round_off_tax_amount):
|
||||||
@@ -414,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
|
|||||||
Use Payment Ledger to fetch unallocated Advance Payments
|
Use Payment Ledger to fetch unallocated Advance Payments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if party_type == "Supplier":
|
||||||
|
return []
|
||||||
|
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
@@ -631,9 +634,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||||
|
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
||||||
|
|
||||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
||||||
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
total_invoiced_amt = (
|
||||||
|
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
||||||
|
)
|
||||||
|
|
||||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
||||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
||||||
@@ -642,6 +648,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
|||||||
return tcs_amount
|
return tcs_amount
|
||||||
|
|
||||||
|
|
||||||
|
def get_advance_adjusted_in_invoice(inv):
|
||||||
|
advances_adjusted = 0
|
||||||
|
for row in inv.get("advances", []):
|
||||||
|
advances_adjusted += row.allocated_amount
|
||||||
|
|
||||||
|
return advances_adjusted
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_total_without_tcs(inv, tax_details):
|
def get_invoice_total_without_tcs(inv, tax_details):
|
||||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||||
|
|||||||
@@ -210,6 +210,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
|||||||
d.reload()
|
d.reload()
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_tcs_on_allocated_advance_payments(self):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||||
|
)
|
||||||
|
|
||||||
|
vouchers = []
|
||||||
|
|
||||||
|
# create advance payment
|
||||||
|
pe = create_payment_entry(
|
||||||
|
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
|
||||||
|
)
|
||||||
|
pe.paid_from = "Debtors - _TC"
|
||||||
|
pe.paid_to = "Cash - _TC"
|
||||||
|
pe.submit()
|
||||||
|
vouchers.append(pe)
|
||||||
|
|
||||||
|
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
|
||||||
|
advances = si.get_advance_entries()
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
vouchers.append(si)
|
||||||
|
|
||||||
|
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
|
||||||
|
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
|
||||||
|
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
|
||||||
|
self.assertEqual(tcs_charged, 2000)
|
||||||
|
|
||||||
|
# cancel invoice and payments to avoid clashing
|
||||||
|
for d in reversed(vouchers):
|
||||||
|
d.reload()
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
def test_tds_calculation_on_net_total(self):
|
def test_tds_calculation_on_net_total(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
|
|||||||
"""select distinct expense_account
|
"""select distinct expense_account
|
||||||
from `tabPurchase Invoice Item` where docstatus = 1
|
from `tabPurchase Invoice Item` where docstatus = 1
|
||||||
and (expense_account is not null and expense_account != '')
|
and (expense_account is not null and expense_account != '')
|
||||||
|
and parenttype='Purchase Invoice'
|
||||||
and parent in (%s) order by expense_account"""
|
and parent in (%s) order by expense_account"""
|
||||||
% ", ".join(["%s"] * len(invoice_list)),
|
% ", ".join(["%s"] * len(invoice_list)),
|
||||||
tuple([inv.name for inv in invoice_list]),
|
tuple([inv.name for inv in invoice_list]),
|
||||||
@@ -451,7 +452,7 @@ def get_invoice_expense_map(invoice_list):
|
|||||||
"""
|
"""
|
||||||
select parent, expense_account, sum(base_net_amount) as amount
|
select parent, expense_account, sum(base_net_amount) as amount
|
||||||
from `tabPurchase Invoice Item`
|
from `tabPurchase Invoice Item`
|
||||||
where parent in (%s)
|
where parent in (%s) and parenttype='Purchase Invoice'
|
||||||
group by parent, expense_account
|
group by parent, expense_account
|
||||||
"""
|
"""
|
||||||
% ", ".join(["%s"] * len(invoice_list)),
|
% ", ".join(["%s"] * len(invoice_list)),
|
||||||
@@ -522,7 +523,7 @@ def get_invoice_po_pr_map(invoice_list):
|
|||||||
"""
|
"""
|
||||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||||
from `tabPurchase Invoice Item`
|
from `tabPurchase Invoice Item`
|
||||||
where parent in (%s)
|
where parent in (%s) and parenttype='Purchase Invoice'
|
||||||
"""
|
"""
|
||||||
% ", ".join(["%s"] * len(invoice_list)),
|
% ", ".join(["%s"] * len(invoice_list)),
|
||||||
tuple(inv.name for inv in invoice_list),
|
tuple(inv.name for inv in invoice_list),
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
|
|||||||
|
|
||||||
if join_required:
|
if join_required:
|
||||||
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
|
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
|
||||||
|
query = query.where(child_doc.parenttype == doctype)
|
||||||
query = query.distinct()
|
query = query.distinct()
|
||||||
|
|
||||||
if parent_doc.get_table_name() != "tabJournal Entry":
|
if parent_doc.get_table_name() != "tabJournal Entry":
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||||
and doc.delivered_by_supplier != 1,
|
and doc.delivered_by_supplier != 1,
|
||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
|
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
set_missing_values,
|
set_missing_values,
|
||||||
@@ -819,7 +819,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
|
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
doc = get_mapped_doc(
|
doc = get_mapped_doc(
|
||||||
@@ -889,6 +889,20 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F
|
|||||||
|
|
||||||
|
|
||||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||||
|
def post_process(source_doc, target_doc):
|
||||||
|
target_doc.populate_items_table()
|
||||||
|
|
||||||
|
if target_doc.set_warehouse:
|
||||||
|
for item in target_doc.items:
|
||||||
|
item.warehouse = target_doc.set_warehouse
|
||||||
|
else:
|
||||||
|
if source_doc.set_warehouse:
|
||||||
|
for item in target_doc.items:
|
||||||
|
item.warehouse = source_doc.set_warehouse
|
||||||
|
else:
|
||||||
|
for idx, item in enumerate(target_doc.items):
|
||||||
|
item.warehouse = source_doc.items[idx].warehouse
|
||||||
|
|
||||||
if target_doc and isinstance(target_doc, str):
|
if target_doc and isinstance(target_doc, str):
|
||||||
target_doc = json.loads(target_doc)
|
target_doc = json.loads(target_doc)
|
||||||
for key in ["service_items", "items", "supplied_items"]:
|
for key in ["service_items", "items", "supplied_items"]:
|
||||||
@@ -919,22 +933,9 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
post_process,
|
||||||
)
|
)
|
||||||
|
|
||||||
target_doc.populate_items_table()
|
|
||||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
|
||||||
|
|
||||||
if target_doc.set_warehouse:
|
|
||||||
for item in target_doc.items:
|
|
||||||
item.warehouse = target_doc.set_warehouse
|
|
||||||
else:
|
|
||||||
if source_doc.set_warehouse:
|
|
||||||
for item in target_doc.items:
|
|
||||||
item.warehouse = source_doc.set_warehouse
|
|
||||||
else:
|
|
||||||
for idx, item in enumerate(target_doc.items):
|
|
||||||
item.warehouse = source_doc.items[idx].warehouse
|
|
||||||
|
|
||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,10 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||||
|
|
||||||
|
frappe.query_reports["Purchase Order Trends"]["filters"].push({
|
||||||
|
fieldname: "include_closed_orders",
|
||||||
|
label: __("Include Closed Orders"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1974,7 +1974,7 @@ class AccountsController(TransactionBase):
|
|||||||
).format(formatted_advance_paid, self.name, formatted_order_total)
|
).format(formatted_advance_paid, self.name, formatted_order_total)
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
|
self.db_set("advance_paid", advance_paid)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def company_abbr(self):
|
def company_abbr(self):
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
@frappe.request_cache
|
@frappe.request_cache
|
||||||
def is_serial_batch_item(self, item_code) -> bool:
|
def is_serial_batch_item(self, item_code) -> bool:
|
||||||
|
if not frappe.db.exists("Item", item_code):
|
||||||
|
frappe.throw(_("Item {0} does not exist.").format(bold(item_code)))
|
||||||
|
|
||||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||||
|
|
||||||
if item_details.has_serial_no or item_details.has_batch_no:
|
if item_details.has_serial_no or item_details.has_batch_no:
|
||||||
|
|||||||
@@ -576,30 +576,56 @@ class SubcontractingController(StockController):
|
|||||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||||
|
|
||||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||||
args = frappe._dict(
|
|
||||||
{
|
|
||||||
"item_code": rm_obj.rm_item_code,
|
|
||||||
"warehouse": self.supplier_warehouse,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"posting_time": self.posting_time,
|
|
||||||
"qty": -1 * flt(rm_obj.consumed_qty),
|
|
||||||
"actual_qty": -1 * flt(rm_obj.consumed_qty),
|
|
||||||
"voucher_type": self.doctype,
|
|
||||||
"voucher_no": self.name,
|
|
||||||
"voucher_detail_no": item_row.name,
|
|
||||||
"company": self.company,
|
|
||||||
"allow_zero_valuation": 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||||
item_row, rm_obj, rm_obj.consumed_qty
|
item_row, rm_obj, rm_obj.consumed_qty
|
||||||
)
|
)
|
||||||
|
|
||||||
if rm_obj.serial_and_batch_bundle:
|
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||||
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
|
|
||||||
|
|
||||||
rm_obj.rate = get_incoming_rate(args)
|
def update_rate_for_supplied_items(self):
|
||||||
|
if self.doctype != "Subcontracting Receipt":
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in self.supplied_items:
|
||||||
|
item_row = None
|
||||||
|
if row.reference_name:
|
||||||
|
item_row = self.get_item_row(row.reference_name)
|
||||||
|
|
||||||
|
if not item_row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.set_rate_for_supplied_items(row, item_row)
|
||||||
|
|
||||||
|
def get_item_row(self, reference_name):
|
||||||
|
for item in self.items:
|
||||||
|
if item.name == reference_name:
|
||||||
|
return item
|
||||||
|
|
||||||
|
def set_rate_for_supplied_items(self, rm_obj, item_row):
|
||||||
|
args = frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": rm_obj.rm_item_code,
|
||||||
|
"warehouse": self.supplier_warehouse,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
"qty": -1 * flt(rm_obj.consumed_qty),
|
||||||
|
"actual_qty": -1 * flt(rm_obj.consumed_qty),
|
||||||
|
"voucher_type": self.doctype,
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"voucher_detail_no": item_row.name,
|
||||||
|
"company": self.company,
|
||||||
|
"allow_zero_valuation": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if rm_obj.serial_and_batch_bundle:
|
||||||
|
args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
|
||||||
|
|
||||||
|
if rm_obj.use_serial_batch_fields:
|
||||||
|
args["batch_no"] = rm_obj.batch_no
|
||||||
|
args["serial_no"] = rm_obj.serial_no
|
||||||
|
|
||||||
|
rm_obj.rate = get_incoming_rate(args)
|
||||||
|
|
||||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||||
|
|||||||
@@ -69,13 +69,15 @@ def get_data(filters, conditions):
|
|||||||
"Delivery Note",
|
"Delivery Note",
|
||||||
]:
|
]:
|
||||||
posting_date = "t1.posting_date"
|
posting_date = "t1.posting_date"
|
||||||
if filters.period_based_on:
|
if filters.period_based_on and conditions.get("trans") in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
posting_date = "t1." + filters.period_based_on
|
posting_date = "t1." + filters.period_based_on
|
||||||
|
|
||||||
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
|
||||||
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
|
||||||
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
|
||||||
cond += " and t1.status != 'Closed'"
|
if not filters.get("include_closed_orders"):
|
||||||
|
if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
|
||||||
|
cond += " and t1.status != 'Closed'"
|
||||||
|
|
||||||
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
|
||||||
cond += " and t1.quotation_to = 'Customer'"
|
cond += " and t1.quotation_to = 'Customer'"
|
||||||
@@ -222,7 +224,7 @@ def period_wise_columns_query(filters, trans):
|
|||||||
|
|
||||||
if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]:
|
if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]:
|
||||||
trans_date = "posting_date"
|
trans_date = "posting_date"
|
||||||
if filters.period_based_on:
|
if filters.period_based_on and trans in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
trans_date = filters.period_based_on
|
trans_date = filters.period_based_on
|
||||||
else:
|
else:
|
||||||
trans_date = "transaction_date"
|
trans_date = "transaction_date"
|
||||||
|
|||||||
@@ -28,18 +28,18 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
erpnext.toggle_naming_series();
|
erpnext.toggle_naming_series();
|
||||||
|
|
||||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
this.frm.add_custom_button(__("Customer"), this.make_customer.bind(this), __("Create"));
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity.bind(this), __("Create"));
|
||||||
__("Opportunity"),
|
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
|
||||||
function () {
|
|
||||||
me.frm.trigger("make_opportunity");
|
|
||||||
},
|
|
||||||
__("Create")
|
|
||||||
);
|
|
||||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
|
||||||
if (!doc.__onload.linked_prospects.length) {
|
if (!doc.__onload.linked_prospects.length) {
|
||||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
this.frm.add_custom_button(__("Prospect"), this.make_prospect.bind(this), __("Create"));
|
||||||
this.frm.add_custom_button(__("Add to Prospect"), this.add_lead_to_prospect, __("Action"));
|
this.frm.add_custom_button(
|
||||||
|
__("Add to Prospect"),
|
||||||
|
() => {
|
||||||
|
this.add_lead_to_prospect(this.frm);
|
||||||
|
},
|
||||||
|
__("Action")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +53,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
this.show_activities();
|
this.show_activities();
|
||||||
}
|
}
|
||||||
|
|
||||||
add_lead_to_prospect() {
|
add_lead_to_prospect(frm) {
|
||||||
let me = this;
|
|
||||||
frappe.prompt(
|
frappe.prompt(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -69,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect",
|
method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect",
|
||||||
args: {
|
args: {
|
||||||
lead: me.frm.doc.name,
|
lead: frm.doc.name,
|
||||||
prospect: data.prospect,
|
prospect: data.prospect,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
me.frm.reload_doc();
|
frm.reload_doc();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
@@ -89,32 +88,123 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
make_customer() {
|
make_customer() {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_customer",
|
method: "erpnext.crm.doctype.lead.lead.make_customer",
|
||||||
frm: cur_frm,
|
frm: this.frm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
make_quotation() {
|
make_quotation() {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||||
frm: cur_frm,
|
frm: this.frm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async make_opportunity() {
|
||||||
|
const frm = this.frm;
|
||||||
|
let existing_prospect = (
|
||||||
|
await frappe.db.get_value(
|
||||||
|
"Prospect Lead",
|
||||||
|
{
|
||||||
|
lead: frm.doc.name,
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
null,
|
||||||
|
"Prospect"
|
||||||
|
)
|
||||||
|
).message?.name;
|
||||||
|
|
||||||
|
let fields = [];
|
||||||
|
if (!existing_prospect) {
|
||||||
|
fields.push(
|
||||||
|
{
|
||||||
|
label: "Create Prospect",
|
||||||
|
fieldname: "create_prospect",
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Prospect Name",
|
||||||
|
fieldname: "prospect_name",
|
||||||
|
fieldtype: "Data",
|
||||||
|
default: frm.doc.company_name,
|
||||||
|
reqd: 1,
|
||||||
|
depends_on: "create_prospect",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await frm.reload_doc();
|
||||||
|
|
||||||
|
let existing_contact = (
|
||||||
|
await frappe.db.get_value(
|
||||||
|
"Contact",
|
||||||
|
{
|
||||||
|
first_name: frm.doc.first_name || frm.doc.lead_name,
|
||||||
|
last_name: frm.doc.last_name,
|
||||||
|
},
|
||||||
|
"name"
|
||||||
|
)
|
||||||
|
).message?.name;
|
||||||
|
|
||||||
|
if (!existing_contact) {
|
||||||
|
fields.push({
|
||||||
|
label: "Create Contact",
|
||||||
|
fieldname: "create_contact",
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: "1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length) {
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Create Opportunity"),
|
||||||
|
fields: fields,
|
||||||
|
primary_action: function (data) {
|
||||||
|
frappe.call({
|
||||||
|
method: "create_prospect_and_contact",
|
||||||
|
doc: frm.doc,
|
||||||
|
args: {
|
||||||
|
data: data,
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function (r) {
|
||||||
|
if (!r.exc) {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||||
|
frm: frm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __("Create"),
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
} else {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||||
|
frm: frm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
make_prospect() {
|
make_prospect() {
|
||||||
|
const me = this;
|
||||||
frappe.model.with_doctype("Prospect", function () {
|
frappe.model.with_doctype("Prospect", function () {
|
||||||
let prospect = frappe.model.get_new_doc("Prospect");
|
let prospect = frappe.model.get_new_doc("Prospect");
|
||||||
prospect.company_name = cur_frm.doc.company_name;
|
prospect.company_name = me.frm.doc.company_name;
|
||||||
prospect.no_of_employees = cur_frm.doc.no_of_employees;
|
prospect.no_of_employees = me.frm.doc.no_of_employees;
|
||||||
prospect.industry = cur_frm.doc.industry;
|
prospect.industry = me.frm.doc.industry;
|
||||||
prospect.market_segment = cur_frm.doc.market_segment;
|
prospect.market_segment = me.frm.doc.market_segment;
|
||||||
prospect.territory = cur_frm.doc.territory;
|
prospect.territory = me.frm.doc.territory;
|
||||||
prospect.fax = cur_frm.doc.fax;
|
prospect.fax = me.frm.doc.fax;
|
||||||
prospect.website = cur_frm.doc.website;
|
prospect.website = me.frm.doc.website;
|
||||||
prospect.prospect_owner = cur_frm.doc.lead_owner;
|
prospect.prospect_owner = me.frm.doc.lead_owner;
|
||||||
prospect.notes = cur_frm.doc.notes;
|
prospect.notes = me.frm.doc.notes;
|
||||||
|
|
||||||
let leads_row = frappe.model.add_child(prospect, "leads");
|
let leads_row = frappe.model.add_child(prospect, "leads");
|
||||||
leads_row.lead = cur_frm.doc.name;
|
leads_row.lead = me.frm.doc.name;
|
||||||
|
|
||||||
frappe.set_route("Form", "Prospect", prospect.name);
|
frappe.set_route("Form", "Prospect", prospect.name);
|
||||||
});
|
});
|
||||||
@@ -150,90 +240,3 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
|||||||
};
|
};
|
||||||
|
|
||||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||||
|
|
||||||
frappe.ui.form.on("Lead", {
|
|
||||||
make_opportunity: async function (frm) {
|
|
||||||
let existing_prospect = (
|
|
||||||
await frappe.db.get_value(
|
|
||||||
"Prospect Lead",
|
|
||||||
{
|
|
||||||
lead: frm.doc.name,
|
|
||||||
},
|
|
||||||
"name",
|
|
||||||
null,
|
|
||||||
"Prospect"
|
|
||||||
)
|
|
||||||
).message.name;
|
|
||||||
|
|
||||||
if (!existing_prospect) {
|
|
||||||
var fields = [
|
|
||||||
{
|
|
||||||
label: "Create Prospect",
|
|
||||||
fieldname: "create_prospect",
|
|
||||||
fieldtype: "Check",
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Prospect Name",
|
|
||||||
fieldname: "prospect_name",
|
|
||||||
fieldtype: "Data",
|
|
||||||
default: frm.doc.company_name,
|
|
||||||
depends_on: "create_prospect",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
let existing_contact = (
|
|
||||||
await frappe.db.get_value(
|
|
||||||
"Contact",
|
|
||||||
{
|
|
||||||
first_name: frm.doc.first_name || frm.doc.lead_name,
|
|
||||||
last_name: frm.doc.last_name,
|
|
||||||
},
|
|
||||||
"name"
|
|
||||||
)
|
|
||||||
).message.name;
|
|
||||||
|
|
||||||
if (!existing_contact) {
|
|
||||||
fields.push({
|
|
||||||
label: "Create Contact",
|
|
||||||
fieldname: "create_contact",
|
|
||||||
fieldtype: "Check",
|
|
||||||
default: "1",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fields) {
|
|
||||||
var d = new frappe.ui.Dialog({
|
|
||||||
title: __("Create Opportunity"),
|
|
||||||
fields: fields,
|
|
||||||
primary_action: function () {
|
|
||||||
var data = d.get_values();
|
|
||||||
frappe.call({
|
|
||||||
method: "create_prospect_and_contact",
|
|
||||||
doc: frm.doc,
|
|
||||||
args: {
|
|
||||||
data: data,
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function (r) {
|
|
||||||
if (!r.exc) {
|
|
||||||
frappe.model.open_mapped_doc({
|
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
|
||||||
frm: frm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
d.hide();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
primary_action_label: __("Create"),
|
|
||||||
});
|
|
||||||
d.show();
|
|
||||||
} else {
|
|
||||||
frappe.model.open_mapped_doc({
|
|
||||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
|
||||||
frm: frm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -365,7 +365,6 @@ doc_events = {
|
|||||||
"Payment Entry": {
|
"Payment Entry": {
|
||||||
"on_submit": [
|
"on_submit": [
|
||||||
"erpnext.regional.create_transaction_log",
|
"erpnext.regional.create_transaction_log",
|
||||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
|
||||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||||
],
|
],
|
||||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ class JobCard(Document):
|
|||||||
self.set_transferred_qty()
|
self.set_transferred_qty()
|
||||||
|
|
||||||
def validate_transfer_qty(self):
|
def validate_transfer_qty(self):
|
||||||
if self.items and self.transferred_qty < self.for_quantity:
|
if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
frm.clear_table("prod_plan_references");
|
frm.clear_table("prod_plan_references");
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_items",
|
method: "combine_so_items",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: function () {
|
callback: function () {
|
||||||
|
|||||||
@@ -269,6 +269,31 @@ class ProductionPlan(Document):
|
|||||||
{"material_request": data.name, "material_request_date": data.transaction_date},
|
{"material_request": data.name, "material_request_date": data.transaction_date},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def combine_so_items(self):
|
||||||
|
if self.combine_items and self.po_items and len(self.po_items) > 0:
|
||||||
|
items = []
|
||||||
|
for row in self.po_items:
|
||||||
|
items.append(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"parent": row.sales_order,
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": row.warehouse,
|
||||||
|
"qty": row.pending_qty,
|
||||||
|
"pending_qty": row.pending_qty,
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
"description": row.description,
|
||||||
|
"bom_no": row.bom_no,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set("po_items", [])
|
||||||
|
self.add_items(items)
|
||||||
|
else:
|
||||||
|
self.get_items()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items(self):
|
def get_items(self):
|
||||||
self.set("po_items", [])
|
self.set("po_items", [])
|
||||||
@@ -435,24 +460,28 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
item_details = get_item_details(data.item_code, throw=False)
|
item_details = get_item_details(data.item_code, throw=False)
|
||||||
if self.combine_items:
|
if self.combine_items:
|
||||||
if item_details.bom_no in refs:
|
bom_no = item_details.bom_no
|
||||||
refs[item_details.bom_no]["so_details"].append(
|
if data.get("bom_no"):
|
||||||
|
bom_no = data.get("bom_no")
|
||||||
|
|
||||||
|
if bom_no in refs:
|
||||||
|
refs[bom_no]["so_details"].append(
|
||||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
||||||
)
|
)
|
||||||
refs[item_details.bom_no]["qty"] += data.pending_qty
|
refs[bom_no]["qty"] += data.pending_qty
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
refs[item_details.bom_no] = {
|
refs[bom_no] = {
|
||||||
"qty": data.pending_qty,
|
"qty": data.pending_qty,
|
||||||
"po_item_ref": data.name,
|
"po_item_ref": data.name,
|
||||||
"so_details": [],
|
"so_details": [],
|
||||||
}
|
}
|
||||||
refs[item_details.bom_no]["so_details"].append(
|
refs[bom_no]["so_details"].append(
|
||||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
||||||
)
|
)
|
||||||
|
|
||||||
bom_no = data.bom_no or item_details and item_details.bom_no or ""
|
bom_no = data.bom_no or item_details and item_details.get("bom_no") or ""
|
||||||
if not bom_no:
|
if not bom_no:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,6 @@ class WorkOrder(Document):
|
|||||||
def delete_auto_created_batch_and_serial_no(self):
|
def delete_auto_created_batch_and_serial_no(self):
|
||||||
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
|
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
|
||||||
frappe.delete_doc("Serial No", row.name)
|
frappe.delete_doc("Serial No", row.name)
|
||||||
self.db_set("serial_no", "")
|
|
||||||
|
|
||||||
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
|
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
|
||||||
frappe.delete_doc("Batch", row.name)
|
frappe.delete_doc("Batch", row.name)
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ erpnext.buying = {
|
|||||||
item[field] = r.message[field];
|
item[field] = r.message[field];
|
||||||
});
|
});
|
||||||
|
|
||||||
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
|
item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
|
||||||
item.is_rejected = true;
|
item.is_rejected = true;
|
||||||
|
|
||||||
new erpnext.SerialBatchPackageSelector(
|
new erpnext.SerialBatchPackageSelector(
|
||||||
@@ -404,7 +404,7 @@ erpnext.buying = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let update_values = {
|
let update_values = {
|
||||||
"serial_and_batch_bundle": r.name,
|
"rejected_serial_and_batch_bundle": r.name,
|
||||||
"use_serial_batch_fields": 0,
|
"use_serial_batch_fields": 0,
|
||||||
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -920,6 +920,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
|||||||
target: opts.target,
|
target: opts.target,
|
||||||
date_field: opts.date_field || undefined,
|
date_field: opts.date_field || undefined,
|
||||||
setters: opts.setters,
|
setters: opts.setters,
|
||||||
|
read_only_setters: opts.read_only_setters,
|
||||||
data_fields: data_fields,
|
data_fields: data_fields,
|
||||||
get_query: opts.get_query,
|
get_query: opts.get_query,
|
||||||
add_filters_group: 1,
|
add_filters_group: 1,
|
||||||
|
|||||||
@@ -96,7 +96,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
options: "Warehouse",
|
options: "Warehouse",
|
||||||
default: this.get_warehouse(),
|
default: this.get_warehouse(),
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
this.item.warehouse = this.dialog.get_value("warehouse");
|
if (this.item?.is_rejected) {
|
||||||
|
this.item.rejected_warehouse = this.dialog.get_value("warehouse");
|
||||||
|
} else {
|
||||||
|
this.item.warehouse = this.dialog.get_value("warehouse");
|
||||||
|
}
|
||||||
|
|
||||||
this.get_auto_data();
|
this.get_auto_data();
|
||||||
},
|
},
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
@@ -282,10 +287,6 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_serial_nos_from_series() {}
|
|
||||||
|
|
||||||
set_batch_nos_from_series() {}
|
|
||||||
|
|
||||||
set_serial_nos_from_range() {
|
set_serial_nos_from_range() {
|
||||||
const serial_no_range = this.dialog.get_value("serial_no_range");
|
const serial_no_range = this.dialog.get_value("serial_no_range");
|
||||||
|
|
||||||
@@ -508,12 +509,17 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
based_on = "FIFO";
|
based_on = "FIFO";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let warehouse = this.item.warehouse || this.item.s_warehouse;
|
||||||
|
if (this.item?.is_rejected) {
|
||||||
|
warehouse = this.item.rejected_warehouse;
|
||||||
|
}
|
||||||
|
|
||||||
if (qty) {
|
if (qty) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data",
|
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data",
|
||||||
args: {
|
args: {
|
||||||
item_code: this.item.item_code,
|
item_code: this.item.item_code,
|
||||||
warehouse: this.item.warehouse || this.item.s_warehouse,
|
warehouse: warehouse,
|
||||||
has_serial_no: this.item.has_serial_no,
|
has_serial_no: this.item.has_serial_no,
|
||||||
has_batch_no: this.item.has_batch_no,
|
has_batch_no: this.item.has_batch_no,
|
||||||
qty: qty,
|
qty: qty,
|
||||||
@@ -627,6 +633,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
frappe.throw(__("Please select a Warehouse"));
|
frappe.throw(__("Please select a Warehouse"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.item?.is_rejected && this.item.rejected_warehouse === this.item.warehouse) {
|
||||||
|
frappe.throw(__("Rejected Warehouse and Accepted Warehouse cannot be same."));
|
||||||
|
}
|
||||||
|
|
||||||
frappe
|
frappe
|
||||||
.call({
|
.call({
|
||||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
|
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
|
||||||
@@ -701,5 +711,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.dialog.fields_dict.entries.grid.refresh();
|
this.dialog.fields_dict.entries.grid.refresh();
|
||||||
|
if (this.dialog.fields_dict.entries.df.data?.length) {
|
||||||
|
this.dialog.set_value("enter_manually", 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": can_map_row,
|
"condition": can_map_row,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
"Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
|
"Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
|
||||||
},
|
},
|
||||||
@@ -495,7 +495,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda row: not row.is_alternative,
|
"condition": lambda row: not row.is_alternative,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
|||||||
@@ -933,7 +933,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
|||||||
|
|
||||||
mapper = {
|
mapper = {
|
||||||
"Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
|
"Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1125,7 +1125,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
"condition": lambda doc: doc.qty
|
"condition": lambda doc: doc.qty
|
||||||
and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"reset_value": True,
|
||||||
|
},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
|||||||
@@ -2,3 +2,10 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.query_reports["Sales Order Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
frappe.query_reports["Sales Order Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
||||||
|
|
||||||
|
frappe.query_reports["Sales Order Trends"]["filters"].push({
|
||||||
|
fieldname: "include_closed_orders",
|
||||||
|
label: __("Include Closed Orders"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1030,7 +1030,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
|||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {
|
"Sales Taxes and Charges": {
|
||||||
"doctype": "Sales Taxes and Charges",
|
"doctype": "Sales Taxes and Charges",
|
||||||
"add_if_empty": True,
|
"reset_value": not (args and args.get("merge_taxes")),
|
||||||
"ignore": args.get("merge_taxes") if args else 0,
|
"ignore": args.get("merge_taxes") if args else 0,
|
||||||
},
|
},
|
||||||
"Sales Team": {
|
"Sales Team": {
|
||||||
|
|||||||
@@ -11,25 +11,10 @@ erpnext.buying.setup_buying_controller();
|
|||||||
|
|
||||||
frappe.ui.form.on("Purchase Receipt", {
|
frappe.ui.form.on("Purchase Receipt", {
|
||||||
setup: (frm) => {
|
setup: (frm) => {
|
||||||
frm.make_methods = {
|
|
||||||
"Landed Cost Voucher": () => {
|
|
||||||
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
|
|
||||||
lcv.company = frm.doc.company;
|
|
||||||
|
|
||||||
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Receipt");
|
|
||||||
lcv_receipt.receipt_document_type = "Purchase Receipt";
|
|
||||||
lcv_receipt.receipt_document = frm.doc.name;
|
|
||||||
lcv_receipt.supplier = frm.doc.supplier;
|
|
||||||
lcv_receipt.grand_total = frm.doc.grand_total;
|
|
||||||
lcv.purchase_receipts = [lcv_receipt];
|
|
||||||
|
|
||||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
"Stock Entry": "Return",
|
"Stock Entry": "Return",
|
||||||
"Purchase Invoice": "Purchase Invoice",
|
"Purchase Invoice": "Purchase Invoice",
|
||||||
|
"Landed Cost Voucher": "Landed Cost Voucher",
|
||||||
};
|
};
|
||||||
|
|
||||||
frm.set_query("expense_account", "items", function () {
|
frm.set_query("expense_account", "items", function () {
|
||||||
@@ -114,9 +99,35 @@ frappe.ui.form.on("Purchase Receipt", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 1) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Landed Cost Voucher"),
|
||||||
|
() => {
|
||||||
|
frm.events.make_lcv(frm);
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
frm.events.add_custom_buttons(frm);
|
frm.events.add_custom_buttons(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
make_lcv(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
|
||||||
|
args: {
|
||||||
|
doctype: frm.doc.doctype,
|
||||||
|
docname: frm.doc.name,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
add_custom_buttons: function (frm) {
|
add_custom_buttons: function (frm) {
|
||||||
if (frm.doc.docstatus == 0) {
|
if (frm.doc.docstatus == 0) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
|
|||||||
@@ -1214,7 +1214,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
|||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {
|
"Purchase Taxes and Charges": {
|
||||||
"doctype": "Purchase Taxes and Charges",
|
"doctype": "Purchase Taxes and Charges",
|
||||||
"add_if_empty": True,
|
"reset_value": not (args and args.get("merge_taxes")),
|
||||||
"ignore": args.get("merge_taxes") if args else 0,
|
"ignore": args.get("merge_taxes") if args else 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1370,3 +1370,26 @@ def get_item_account_wise_additional_cost(purchase_document):
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def update_regional_gl_entries(gl_list, doc):
|
def update_regional_gl_entries(gl_list, doc):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_lcv(doctype, docname):
|
||||||
|
landed_cost_voucher = frappe.new_doc("Landed Cost Voucher")
|
||||||
|
|
||||||
|
details = frappe.db.get_value(doctype, docname, ["supplier", "company", "base_grand_total"], as_dict=1)
|
||||||
|
|
||||||
|
landed_cost_voucher.company = details.company
|
||||||
|
|
||||||
|
landed_cost_voucher.append(
|
||||||
|
"purchase_receipts",
|
||||||
|
{
|
||||||
|
"receipt_document_type": doctype,
|
||||||
|
"receipt_document": docname,
|
||||||
|
"grand_total": details.base_grand_total,
|
||||||
|
"supplier": details.supplier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
landed_cost_voucher.get_items_from_purchase_receipts()
|
||||||
|
|
||||||
|
return landed_cost_voucher.as_dict()
|
||||||
|
|||||||
@@ -1308,8 +1308,12 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=Fal
|
|||||||
if parent_doc and isinstance(parent_doc, str):
|
if parent_doc and isinstance(parent_doc, str):
|
||||||
parent_doc = parse_json(parent_doc)
|
parent_doc = parse_json(parent_doc)
|
||||||
|
|
||||||
if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
|
bundle = child_row.serial_and_batch_bundle
|
||||||
sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
|
if child_row.get("is_rejected"):
|
||||||
|
bundle = child_row.rejected_serial_and_batch_bundle
|
||||||
|
|
||||||
|
if frappe.db.exists("Serial and Batch Bundle", bundle):
|
||||||
|
sb_doc = update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse)
|
||||||
else:
|
else:
|
||||||
sb_doc = create_serial_batch_no_ledgers(
|
sb_doc = create_serial_batch_no_ledgers(
|
||||||
entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
|
entries, child_row, parent_doc, warehouse, do_not_save=do_not_save
|
||||||
@@ -1412,8 +1416,8 @@ def get_type_of_transaction(parent_doc, child_row):
|
|||||||
return type_of_transaction
|
return type_of_transaction
|
||||||
|
|
||||||
|
|
||||||
def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
|
def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object:
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", bundle)
|
||||||
doc.voucher_detail_no = child_row.name
|
doc.voucher_detail_no = child_row.name
|
||||||
doc.posting_date = parent_doc.posting_date
|
doc.posting_date = parent_doc.posting_date
|
||||||
doc.posting_time = parent_doc.posting_time
|
doc.posting_time = parent_doc.posting_time
|
||||||
|
|||||||
@@ -447,9 +447,11 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
source_doctype: "Stock Entry",
|
source_doctype: "Stock Entry",
|
||||||
target: frm,
|
target: frm,
|
||||||
date_field: "posting_date",
|
date_field: "posting_date",
|
||||||
|
read_only_setters: ["stock_entry_type", "purpose", "add_to_transit"],
|
||||||
setters: {
|
setters: {
|
||||||
stock_entry_type: "Material Transfer",
|
stock_entry_type: "Material Transfer",
|
||||||
purpose: "Material Transfer",
|
purpose: "Material Transfer",
|
||||||
|
add_to_transit: 1,
|
||||||
},
|
},
|
||||||
get_query_filters: {
|
get_query_filters: {
|
||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
|
|||||||
@@ -249,11 +249,15 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("batch_no", "supplied_items", (doc, cdt, cdn) => {
|
frm.set_query("batch_no", "supplied_items", (doc, cdt, cdn) => {
|
||||||
var row = locals[cdt][cdn];
|
let row = locals[cdt][cdn];
|
||||||
|
let filters = {
|
||||||
|
item_code: row.rm_item_code,
|
||||||
|
warehouse: doc.supplier_warehouse,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters: {
|
query: "erpnext.controllers.queries.get_batch_no",
|
||||||
item: row.rm_item_code,
|
filters: filters,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -237,9 +237,14 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||||
== "BOM"
|
== "BOM"
|
||||||
and self.supplied_items
|
and self.supplied_items
|
||||||
and not any(item.serial_and_batch_bundle for item in self.supplied_items)
|
|
||||||
):
|
):
|
||||||
self.supplied_items = []
|
if not any(
|
||||||
|
item.serial_and_batch_bundle or item.batch_no or item.serial_no
|
||||||
|
for item in self.supplied_items
|
||||||
|
):
|
||||||
|
self.supplied_items = []
|
||||||
|
else:
|
||||||
|
self.update_rate_for_supplied_items()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_scrap_items(self, recalculate_rate=False):
|
def get_scrap_items(self, recalculate_rate=False):
|
||||||
|
|||||||
@@ -1361,6 +1361,66 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
|
def test_change_batch_for_raw_materials(self):
|
||||||
|
set_backflush_based_on("BOM")
|
||||||
|
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BNGS-.####",
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
|
||||||
|
second_batch_no = None
|
||||||
|
for row in bom.items:
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=row.item_code,
|
||||||
|
qty=1,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
rate=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
se1 = make_stock_entry(
|
||||||
|
item_code=row.item_code,
|
||||||
|
qty=1,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
rate=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
se1.reload()
|
||||||
|
|
||||||
|
second_batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": fg_item,
|
||||||
|
"fg_item_qty": 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.save()
|
||||||
|
scr.reload()
|
||||||
|
|
||||||
|
scr.supplied_items[0].batch_no = second_batch_no
|
||||||
|
scr.supplied_items[0].use_serial_batch_fields = 1
|
||||||
|
scr.submit()
|
||||||
|
scr.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle)
|
||||||
|
self.assertEqual(batch_no, second_batch_no)
|
||||||
|
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
|
||||||
|
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
|
||||||
|
|
||||||
|
|
||||||
def make_return_subcontracting_receipt(**args):
|
def make_return_subcontracting_receipt(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ requires = ["flit_core >=3.4,<4"]
|
|||||||
build-backend = "flit_core.buildapi"
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
[tool.bench.frappe-dependencies]
|
[tool.bench.frappe-dependencies]
|
||||||
frappe = ">=15.10.0,<16.0.0"
|
frappe = ">=15.40.4,<16.0.0"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 110
|
line-length = 110
|
||||||
|
|||||||
Reference in New Issue
Block a user