Merge pull request #43563 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
rohitwaghchaure
2024-10-09 17:31:15 +05:30
committed by GitHub
43 changed files with 1713 additions and 438 deletions

View File

@@ -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);
},
]);
}
},
});
}
},
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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