mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-30 07:44:36 +00:00
Compare commits
268 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f261f3448 | ||
|
|
11a6ebaeef | ||
|
|
d2b2002664 | ||
|
|
56dad7d365 | ||
|
|
6869e5dde9 | ||
|
|
5d5ec2ab7c | ||
|
|
f5a4ec129b | ||
|
|
e185a06a15 | ||
|
|
193d7981ea | ||
|
|
957eabf53e | ||
|
|
3bb186736d | ||
|
|
1121c6663f | ||
|
|
944479313c | ||
|
|
daa75eea00 | ||
|
|
a2b7fc18ab | ||
|
|
7d6984c873 | ||
|
|
64cbf446bd | ||
|
|
b99cdb5be7 | ||
|
|
d6de50634f | ||
|
|
4d7c0c004a | ||
|
|
f0e3fb466a | ||
|
|
8337439589 | ||
|
|
88e5ed7998 | ||
|
|
fee2255661 | ||
|
|
23d91145d0 | ||
|
|
6c8e0fd1fb | ||
|
|
512a171ad5 | ||
|
|
30f034555b | ||
|
|
a35ce12d60 | ||
|
|
8cf057849e | ||
|
|
f3c60ea0a7 | ||
|
|
c724573a18 | ||
|
|
53e1b57354 | ||
|
|
42e7725442 | ||
|
|
18b7af977c | ||
|
|
db746a4def | ||
|
|
3d5fb5fc90 | ||
|
|
a833010d2b | ||
|
|
0d6148f218 | ||
|
|
6d51d14dfd | ||
|
|
cb2cb4447a | ||
|
|
b3a8fe9391 | ||
|
|
2a820a85ed | ||
|
|
8d8dd0cd2b | ||
|
|
3a5f19853a | ||
|
|
c0dd794e15 | ||
|
|
f56ee58e81 | ||
|
|
2c99075899 | ||
|
|
01cd509113 | ||
|
|
5563bea789 | ||
|
|
a0a932a235 | ||
|
|
6f75a3c617 | ||
|
|
a4d3934f75 | ||
|
|
b4171e4bd9 | ||
|
|
76d32ab07a | ||
|
|
3d469db47b | ||
|
|
80244bafa4 | ||
|
|
c54e97b89a | ||
|
|
14202fae06 | ||
|
|
9fc0ac1a92 | ||
|
|
5bdd2989c6 | ||
|
|
42dccadff1 | ||
|
|
0650c22b53 | ||
|
|
ec26c92263 | ||
|
|
26248924b6 | ||
|
|
8d29dc6a81 | ||
|
|
6eb50fa300 | ||
|
|
8c350d43b2 | ||
|
|
caa6ca1d0b | ||
|
|
47f1714be4 | ||
|
|
ecb0506dba | ||
|
|
42382b3945 | ||
|
|
993114942e | ||
|
|
5a28a1728e | ||
|
|
80a5df0e96 | ||
|
|
25193c5e92 | ||
|
|
1ee14ac135 | ||
|
|
a3e5ffe915 | ||
|
|
ff868a9290 | ||
|
|
247ae7a965 | ||
|
|
3a149b3c9b | ||
|
|
08bed618f6 | ||
|
|
d9ca680a29 | ||
|
|
7401dc4015 | ||
|
|
eb7e063d5c | ||
|
|
0dbe79645c | ||
|
|
995773088a | ||
|
|
be736cf641 | ||
|
|
85089d3d64 | ||
|
|
eed6d2b81c | ||
|
|
edf53f1ab7 | ||
|
|
758282739e | ||
|
|
49d995c3ac | ||
|
|
72ca2ec9a5 | ||
|
|
4297895bd9 | ||
|
|
7428df8778 | ||
|
|
767c79663c | ||
|
|
fa85482662 | ||
|
|
831e2aaf18 | ||
|
|
9ac665b4bd | ||
|
|
8d8d84bae4 | ||
|
|
9f4cb98de6 | ||
|
|
72c16097d6 | ||
|
|
2203ea9301 | ||
|
|
28c9f2adab | ||
|
|
d04f7ffe87 | ||
|
|
21e5c01f11 | ||
|
|
ae6c1a30ac | ||
|
|
1040198ce1 | ||
|
|
69878b7847 | ||
|
|
ec0201cb85 | ||
|
|
5d7fb1d945 | ||
|
|
33542cb909 | ||
|
|
549dc286d0 | ||
|
|
f547befeb9 | ||
|
|
a144059c7c | ||
|
|
b96361e837 | ||
|
|
cd5994017c | ||
|
|
431fb62803 | ||
|
|
9e9de4c99e | ||
|
|
d3369368db | ||
|
|
b24de3e35b | ||
|
|
0344442d42 | ||
|
|
0decd0955b | ||
|
|
6f7fdbefac | ||
|
|
dbd466b6b2 | ||
|
|
4914481105 | ||
|
|
8514c01a91 | ||
|
|
751c20984f | ||
|
|
81b1cbd367 | ||
|
|
a72383da0e | ||
|
|
eb8213c4e7 | ||
|
|
4d9f522f22 | ||
|
|
528f42c713 | ||
|
|
7cc7179b05 | ||
|
|
cfaad685a4 | ||
|
|
e420fa9779 | ||
|
|
fbf1160357 | ||
|
|
a3e3585e50 | ||
|
|
0f9849e672 | ||
|
|
3be037221a | ||
|
|
b2e108afcc | ||
|
|
114a5f8cca | ||
|
|
aba54ba18f | ||
|
|
6a7768367e | ||
|
|
3a1ad6e844 | ||
|
|
40abd82e2d | ||
|
|
2933a86458 | ||
|
|
ecf0d0b388 | ||
|
|
b84ca04975 | ||
|
|
abb88662c1 | ||
|
|
73af5be1c3 | ||
|
|
81e3d269dc | ||
|
|
3dafa98eef | ||
|
|
3699b96adf | ||
|
|
f782af8ab3 | ||
|
|
2af48e40a1 | ||
|
|
389227bce8 | ||
|
|
8cd1952da3 | ||
|
|
f128fcd86c | ||
|
|
d890d02b5c | ||
|
|
defd5541b0 | ||
|
|
8624a0abce | ||
|
|
13b79c6781 | ||
|
|
2926915a06 | ||
|
|
536dc47eb0 | ||
|
|
225843265e | ||
|
|
a1f98603a6 | ||
|
|
ab59b33606 | ||
|
|
1f1e934675 | ||
|
|
582fffca93 | ||
|
|
e9b3a24221 | ||
|
|
0d5ac078f6 | ||
|
|
f109164f88 | ||
|
|
765c1104c4 | ||
|
|
a8de8aecf5 | ||
|
|
20c1bcd654 | ||
|
|
6eaad90535 | ||
|
|
91a5c562d8 | ||
|
|
70d5593ace | ||
|
|
f3401243be | ||
|
|
19dfeca96d | ||
|
|
774db56762 | ||
|
|
d15c535f86 | ||
|
|
a429f2f626 | ||
|
|
dfbc7e8b33 | ||
|
|
b41f10c1b9 | ||
|
|
44745a757d | ||
|
|
47bf2d408b | ||
|
|
c71f06be9e | ||
|
|
50b1fa5deb | ||
|
|
a99057754d | ||
|
|
6f16ae3e00 | ||
|
|
38d80aab99 | ||
|
|
d8939e0bb0 | ||
|
|
f376abf0fe | ||
|
|
64e75a8e08 | ||
|
|
67a210f722 | ||
|
|
a2df276880 | ||
|
|
151d293aaf | ||
|
|
13f6f3259d | ||
|
|
da6eea7743 | ||
|
|
b28fa8b994 | ||
|
|
7ed7c22469 | ||
|
|
6a4f06cf4a | ||
|
|
dfe0079573 | ||
|
|
a983d65404 | ||
|
|
2aa90fdc14 | ||
|
|
a81bc28afb | ||
|
|
70e40183bb | ||
|
|
bdfc1ddfc3 | ||
|
|
fb770f5633 | ||
|
|
9789648175 | ||
|
|
94f4c92a03 | ||
|
|
3cac4a598f | ||
|
|
654fea8a3b | ||
|
|
838d341d91 | ||
|
|
fd4143e686 | ||
|
|
2e3b2db03b | ||
|
|
51fa336350 | ||
|
|
0a3c0ecea2 | ||
|
|
2d644ac066 | ||
|
|
d8768c5377 | ||
|
|
65c1387a04 | ||
|
|
3ad1f2d0d0 | ||
|
|
af31f3b291 | ||
|
|
2f81c99143 | ||
|
|
e679b336ef | ||
|
|
82c17632f7 | ||
|
|
59b9b7dc91 | ||
|
|
d242d5330c | ||
|
|
05c606b65d | ||
|
|
52578b03b4 | ||
|
|
8978a54479 | ||
|
|
b44598cb7a | ||
|
|
48e05b6110 | ||
|
|
3640bc7f88 | ||
|
|
531410820f | ||
|
|
d08eba7308 | ||
|
|
c1bfa0a31a | ||
|
|
155ca14b19 | ||
|
|
3ee5991896 | ||
|
|
7ec7e11634 | ||
|
|
17a6224a3f | ||
|
|
e9bcb1bbb2 | ||
|
|
d8080910c6 | ||
|
|
02a31caa05 | ||
|
|
830b95bdcb | ||
|
|
2de86eb0f4 | ||
|
|
9616c59ed8 | ||
|
|
f27e9f3089 | ||
|
|
9321408154 | ||
|
|
97cc3082e1 | ||
|
|
b7b4de7c8d | ||
|
|
dc9cf74be8 | ||
|
|
4e826fb3e9 | ||
|
|
553513c4c6 | ||
|
|
5cfa2d0692 | ||
|
|
3da7071327 | ||
|
|
c3293d110c | ||
|
|
78eb443614 | ||
|
|
97cadfe5d3 | ||
|
|
87d8603d1d | ||
|
|
7dc68cabcc | ||
|
|
7c8d13c51a | ||
|
|
14f9aef55c | ||
|
|
6e833cce6a | ||
|
|
976abf7b3c |
@@ -2,8 +2,9 @@ import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.31.4"
|
||||
__version__ = "15.34.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
|
||||
def check_app_permission():
|
||||
if frappe.session.user == "Administrator":
|
||||
return True
|
||||
|
||||
if is_website_user():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -202,7 +202,7 @@ class Account(NestedSet):
|
||||
msg = _(
|
||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||
).format(
|
||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
||||
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -96,8 +96,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -47,9 +47,11 @@ def validate_columns(data):
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 8:
|
||||
if no_of_columns != 8:
|
||||
frappe.throw(
|
||||
_("More columns found than expected. Please compare the uploaded file with standard template"),
|
||||
_(
|
||||
"Columns are not according to template. Please compare the uploaded file with standard template"
|
||||
),
|
||||
title=(_("Wrong Template")),
|
||||
)
|
||||
|
||||
|
||||
@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
},
|
||||
|
||||
make_jv: function (frm) {
|
||||
let revaluation_journal = null;
|
||||
let zero_balance_journal = null;
|
||||
frappe.call({
|
||||
method: "make_jv_entries",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: "Making Journal Entries...",
|
||||
freeze_message: __("Creating Journal Entries..."),
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let response = r.message;
|
||||
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
|
||||
frappe.msgprint(__("Journals have been created"));
|
||||
frappe.msgprint(__("Journal entries have been created"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
@@ -165,8 +173,25 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
@@ -213,7 +238,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
hide_unhide_fields: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.toggle_display(
|
||||
@@ -280,7 +305,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
var company_currency = frm.doc.company
|
||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
||||
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||
: "";
|
||||
|
||||
frm.set_currency_labels(
|
||||
@@ -395,6 +420,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +658,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -37,7 +37,6 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -1119,90 +1118,82 @@ class PaymentEntry(AccountsController):
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
@@ -1749,7 +1740,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"transaction_date",
|
||||
"column_break_2",
|
||||
"naming_series",
|
||||
"company",
|
||||
"mode_of_payment",
|
||||
"party_details",
|
||||
"party_type",
|
||||
@@ -390,13 +391,20 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-20 13:54:55.245774",
|
||||
"modified": "2024-08-07 16:39:54.288002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -338,6 +339,17 @@ class PaymentRequest(Document):
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
self.payment_request_type == "Outward"
|
||||
and payment_entry.paid_from_account_currency == ref_doc.company_currency
|
||||
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
|
||||
):
|
||||
payment_entry.paid_amount = payment_entry.base_paid_amount = (
|
||||
payment_entry.target_exchange_rate * payment_entry.received_amount
|
||||
)
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
@@ -480,6 +492,7 @@ def make_payment_request(**args):
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"company": ref_doc.get("company"),
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
@@ -541,7 +554,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
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.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
@@ -32,7 +34,7 @@ payment_method = [
|
||||
]
|
||||
|
||||
|
||||
class TestPaymentRequest(unittest.TestCase):
|
||||
class TestPaymentRequest(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
|
||||
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
|
||||
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
# Try to make Payment Request more than SO amount, should give validation
|
||||
pr2.grand_total = 900
|
||||
self.assertRaises(frappe.ValidationError, pr2.save)
|
||||
|
||||
def test_conversion_on_foreign_currency_accounts(self):
|
||||
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
|
||||
po_doc.conversion_rate = 80
|
||||
po_doc.items[0].qty = 1
|
||||
po_doc.items[0].rate = 10
|
||||
po_doc.save().submit()
|
||||
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
||||
)
|
||||
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
|
||||
@@ -188,7 +188,7 @@ class POSInvoice(SalesInvoice):
|
||||
def validate(self):
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
)
|
||||
|
||||
# run on validate method of selling controller
|
||||
|
||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
||||
)
|
||||
msg = _(
|
||||
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||
msg += " "
|
||||
msg += _(
|
||||
"Original invoice should be consolidated before or along with the return invoice."
|
||||
"The original invoice should be consolidated before or along with the return invoice."
|
||||
)
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
||||
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||
bold_return_against
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -186,7 +186,8 @@ class PricingRule(Document):
|
||||
if not self.priority:
|
||||
throw(
|
||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
||||
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||
frappe.bold(_("Priority")),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -194,7 +195,7 @@ class PricingRule(Document):
|
||||
throw(
|
||||
_(
|
||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
||||
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||
)
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
|
||||
@@ -6,7 +6,9 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
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.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -1311,6 +1313,69 @@ class TestPricingRule(unittest.TestCase):
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"ignore_cr_dr_notes",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -383,10 +384,16 @@
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
include_break: DF.Check
|
||||
@@ -133,6 +134,9 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
if doc.ignore_exchange_rate_revaluation_journals:
|
||||
filters.update({"ignore_err": True})
|
||||
|
||||
if doc.ignore_cr_dr_notes:
|
||||
filters.update({"ignore_cr_dr_notes": True})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
col, res = get_soa(filters)
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Promotional Scheme", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("for_price_list", "price_discount_slabs", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
selling: doc.selling,
|
||||
buying: doc.buying,
|
||||
currency: doc.currency,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.trigger("toggle_reqd_apply_on");
|
||||
|
||||
@@ -51,6 +51,7 @@ price_discount_fields = [
|
||||
"discount_percentage",
|
||||
"validate_applied_rule",
|
||||
"apply_multiple_pricing_rules",
|
||||
"for_price_list",
|
||||
]
|
||||
|
||||
product_discount_fields = [
|
||||
@@ -63,6 +64,7 @@ product_discount_fields = [
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
"round_free_qty",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"rate",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_11",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -120,6 +121,13 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.rate_or_discount!=\"Rate\"",
|
||||
"fieldname": "for_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -169,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-16 00:25:33.843996",
|
||||
"modified": "2024-07-23 12:33:46.574950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
@@ -177,4 +185,4 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class PromotionalSchemePriceDiscount(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
for_price_list: DF.Link | None
|
||||
max_amount: DF.Currency
|
||||
max_qty: DF.Float
|
||||
min_amount: DF.Currency
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"column_break_9",
|
||||
"free_item_uom",
|
||||
"free_item_rate",
|
||||
"round_free_qty",
|
||||
"section_break_12",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -181,12 +182,18 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-12 12:53:58.199108",
|
||||
"modified": "2024-07-22 17:25:07.880984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
@@ -195,4 +202,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
round_free_qty: DF.Check
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -377,16 +377,16 @@ class PurchaseInvoice(BuyingController):
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold("Credit To")),
|
||||
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||
).format(frappe.bold(_("Credit To"))),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
||||
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||
title=_("Invalid Account"),
|
||||
)
|
||||
|
||||
@@ -634,7 +634,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Order Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
@@ -655,7 +655,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||
).format(
|
||||
frappe.bold(_("Purchase Receipt Required")),
|
||||
frappe.bold("No"),
|
||||
frappe.bold(_("No")),
|
||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||
)
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
@@ -1262,7 +1262,11 @@ class PurchaseInvoice(BuyingController):
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
assets = frappe.db.get_all(
|
||||
"Asset",
|
||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
||||
filters={
|
||||
"purchase_invoice": self.name,
|
||||
"item_code": item.item_code,
|
||||
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||
},
|
||||
fields=["name", "asset_quantity"],
|
||||
)
|
||||
for asset in assets:
|
||||
|
||||
@@ -2236,6 +2236,62 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||
|
||||
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||
)
|
||||
pr.conversion_rate = 6300
|
||||
pr.plc_conversion_rate = 1
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
self.assertEqual(pr.conversion_rate, 6300)
|
||||
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=3000,
|
||||
distribute_charges_based_on="Qty",
|
||||
)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 1.1
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -958,12 +959,22 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -514,7 +514,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
frappe.bold(_("Consolidated Sales Invoice")),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
@@ -858,7 +858,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = (
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||
+ " "
|
||||
)
|
||||
msg += _(
|
||||
@@ -869,7 +869,7 @@ class SalesInvoice(SellingController):
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = (
|
||||
_("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
||||
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||
)
|
||||
+ " "
|
||||
)
|
||||
@@ -1316,6 +1316,10 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
@@ -1374,37 +1378,33 @@ class SalesInvoice(SellingController):
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
@@ -1479,6 +1479,10 @@ class SalesInvoice(SellingController):
|
||||
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
|
||||
payment_mode.base_amount -= flt(self.change_amount)
|
||||
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
if payment_mode.base_amount:
|
||||
# POS, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1492,7 +1496,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
@@ -3083,6 +3084,84 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
cust_doc.save()
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||
)
|
||||
si.department = "All Departments"
|
||||
si.save().submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"department": "All Departments",
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
dim.disabled = True
|
||||
dim.save()
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3758,6 +3837,40 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pos_returns_without_update_outstanding_for_self(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
||||
)
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
||||
pos.save().submit()
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.update_outstanding_for_self = False
|
||||
pos_return.save().submit()
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(gle)
|
||||
.select(gle.against_voucher)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
|
||||
)
|
||||
.run(as_list=1)
|
||||
)
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0][0], pos_return.return_against)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -38,6 +38,12 @@ frappe.ui.form.on("Subscription", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Force-Fetch Subscription Updates"),
|
||||
() => frm.trigger("force_fetch_subscription_updates"),
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Cancel Subscription"),
|
||||
() => frm.trigger("cancel_this_subscription"),
|
||||
@@ -82,4 +88,11 @@ frappe.ui.form.on("Subscription", {
|
||||
}
|
||||
});
|
||||
},
|
||||
force_fetch_subscription_updates: function (frm) {
|
||||
frm.call("force_fetch_subscription_updates").then((r) => {
|
||||
if (!r.exec) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -717,6 +717,28 @@ class Subscription(Document):
|
||||
self.update_subscription_period(posting_date or nowdate())
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def force_fetch_subscription_updates(self):
|
||||
"""
|
||||
Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
|
||||
It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
|
||||
"""
|
||||
|
||||
# Don't process future subscriptions
|
||||
if nowdate() < self.current_invoice_start:
|
||||
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
|
||||
return
|
||||
|
||||
processing_date = None
|
||||
if self.generate_invoice_at == "Beginning of the current subscription period":
|
||||
processing_date = self.current_invoice_start
|
||||
elif self.generate_invoice_at == "End of the current subscription period":
|
||||
processing_date = self.current_invoice_end
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||
|
||||
self.process(posting_date=processing_date)
|
||||
|
||||
|
||||
def is_prorate() -> int:
|
||||
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
|
||||
|
||||
@@ -521,6 +521,18 @@ class TestSubscription(FrappeTestCase):
|
||||
subscription.process(posting_date="2023-01-22")
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
def test_future_subscription(self):
|
||||
"""Force-Fetch should not process future subscriptions"""
|
||||
subscription = create_subscription(
|
||||
start_date=add_months(nowdate(), 1),
|
||||
submit_invoice=0,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
party="_Test Subscription Customer John Doe",
|
||||
)
|
||||
subscription.force_fetch_subscription_updates()
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -375,12 +375,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
AND ja.party in %s
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
AND j.company = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -497,6 +499,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
|
||||
@@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
|
||||
options: "Asset",
|
||||
depends_on: "eval: doc.group_by == 'Asset'",
|
||||
},
|
||||
{
|
||||
fieldname: "finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -69,6 +69,9 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
@@ -119,6 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset_category": filters.get("asset_category"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -128,6 +132,10 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition += " and name = %(asset)s"
|
||||
if filters.get("finance_book"):
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = `tabAsset`.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT name,
|
||||
@@ -176,6 +184,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"asset": filters.get("asset"),
|
||||
"finance_book": filters.get("finance_book"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
open_utility_report: function () {
|
||||
frappe.route_options = {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account: frappe.query_report.get_filter_value("account"),
|
||||
report_date: frappe.query_report.get_filter_value("report_date"),
|
||||
};
|
||||
frappe.open_in_new_tab = true;
|
||||
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -154,8 +154,8 @@ def get_payment_entries(filters):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Bank Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
default: frappe.defaults.get_user_default("Company")
|
||||
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
|
||||
: "",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_account_list",
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "disabled", "=", 0],
|
||||
["Account", "company", "=", company],
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-07-30 17:20:07.570971",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-07-30 17:20:07.570971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cheques and Deposits Incorrectly cleared",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Entry",
|
||||
"report_name": "Cheques and Deposits Incorrectly cleared",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = build_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def build_payment_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
}
|
||||
)
|
||||
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": row.get("amount"),
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": row.get("amount"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_journal_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
"debit": row.get("debit_in_account_currency"),
|
||||
"credit": row.get("credit_in_account_currency"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_data(filters):
|
||||
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
|
||||
data = []
|
||||
for x in vouchers:
|
||||
if x.doctype == "Payment Entry":
|
||||
data.append(build_payment_entry_dict(x))
|
||||
elif x.doctype == "Journal Entry":
|
||||
data.append(build_journal_entry_dict(x))
|
||||
return data
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
doctype_name = ConstantColumn("Journal Entry")
|
||||
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
je.name,
|
||||
jea.debit_in_account_currency,
|
||||
jea.credit_in_account_currency,
|
||||
je.posting_date,
|
||||
je.clearance_date,
|
||||
)
|
||||
.where(
|
||||
je.docstatus.eq(1)
|
||||
& jea.account.eq(filters.account)
|
||||
& je.posting_date.gt(filters.report_date)
|
||||
& je.clearance_date.lte(filters.report_date)
|
||||
& (je.is_opening.isnull() | je.is_opening.eq("No"))
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
pe.clearance_date,
|
||||
)
|
||||
.where(
|
||||
pe.docstatus.eq(1)
|
||||
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
|
||||
& pe.posting_date.gt(filters.report_date)
|
||||
& pe.clearance_date.lte(filters.report_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
return journals + payments
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "payment_document",
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry",
|
||||
"label": _("Payment Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "payment_document",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
|
||||
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
|
||||
]
|
||||
@@ -104,7 +104,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
report_summary = get_bs_summary(
|
||||
report_summary, primitive_summary = get_bs_summary(
|
||||
companies,
|
||||
asset,
|
||||
liability,
|
||||
@@ -175,7 +175,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
report_summary = get_pl_summary(
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
)
|
||||
|
||||
|
||||
@@ -209,6 +209,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_cr_dr_notes",
|
||||
label: __("Ignore System Generated Credit / Debit Notes"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -236,6 +236,23 @@ def get_conditions(filters):
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if filters.get("ignore_cr_dr_notes"):
|
||||
system_generated_cr_dr_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
|
||||
"is_system_generated": 1,
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
if system_generated_cr_dr_journals:
|
||||
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
|
||||
x[0] for x in system_generated_cr_dr_journals
|
||||
]
|
||||
filters.update({"voucher_no_not_in": vouchers_to_ignore})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
|
||||
@@ -2,13 +2,32 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def test_foreign_account_balance_after_exchange_rate_revaluation(self):
|
||||
"""
|
||||
Checks the correctness of balance after exchange rate revaluation
|
||||
@@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
)
|
||||
)
|
||||
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
def test_ignore_cr_dr_notes_filter(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
cr_note = make_return_doc(si.doctype, si.name)
|
||||
cr_note.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = si.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = si.customer
|
||||
pr.receivable_payable_account = si.debit_to
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name]
|
||||
payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
system_generated_journal = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"voucher_type": "Credit Note",
|
||||
"is_system_generated": True,
|
||||
},
|
||||
fields=["name"],
|
||||
)
|
||||
self.assertEqual(len(system_generated_journal), 1)
|
||||
expected = set([si.name, cr_note.name, system_generated_journal[0].name])
|
||||
# Without ignore_cr_dr_notes
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Without ignore_cr_dr_notes
|
||||
expected = set([si.name, cr_note.name])
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -336,7 +336,7 @@ def get_tds_docs(filters):
|
||||
def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
if not tds_accounts:
|
||||
frappe.throw(
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")),
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
|
||||
title=_("Accounts Missing Error"),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -188,11 +188,21 @@ frappe.ui.form.on("Asset", {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -660,6 +670,11 @@ frappe.ui.form.on("Asset", {
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -33,14 +33,16 @@
|
||||
"dimension_col_break",
|
||||
"purchase_details_section",
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
"purchase_invoice",
|
||||
"purchase_invoice_item",
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"total_asset_cost",
|
||||
"additional_asset_cost",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
"purchase_date",
|
||||
"additional_asset_cost",
|
||||
"total_asset_cost",
|
||||
"section_break_23",
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
@@ -221,7 +223,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
@@ -537,6 +538,20 @@
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -580,7 +595,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-07 22:27:14.733839",
|
||||
"modified": "2024-08-26 23:28:29.095139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -94,7 +94,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -669,7 +671,7 @@ class Asset(AccountsController):
|
||||
if not fixed_asset_account:
|
||||
frappe.throw(
|
||||
_("Set {0} in asset category {1} for company {2}").format(
|
||||
frappe.bold("Fixed Asset Account"),
|
||||
frappe.bold(_("Fixed Asset Account")),
|
||||
frappe.bold(self.asset_category),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
@@ -691,12 +693,17 @@ class Asset(AccountsController):
|
||||
return cwip_account
|
||||
|
||||
def make_gl_entries(self):
|
||||
if self.check_asset_capitalization_gl_entries():
|
||||
return
|
||||
|
||||
gl_entries = []
|
||||
|
||||
purchase_document = self.get_purchase_document()
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
|
||||
self.available_for_use_date
|
||||
) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -733,6 +740,24 @@ class Asset(AccountsController):
|
||||
make_gl_entries(gl_entries)
|
||||
self.db_set("booked_fixed_asset", 1)
|
||||
|
||||
def check_asset_capitalization_gl_entries(self):
|
||||
if self.is_composite_asset:
|
||||
result = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1},
|
||||
["name", "target_fixed_asset_account"],
|
||||
)
|
||||
|
||||
if result:
|
||||
asset_capitalization, target_fixed_asset_account = result
|
||||
# Check GL entries for the retrieved Asset Capitalization and target fixed asset account
|
||||
return has_gl_entries(
|
||||
"Asset Capitalization", asset_capitalization, target_fixed_asset_account
|
||||
)
|
||||
# return if there are no submitted capitalization for given asset
|
||||
return True
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_depreciation_rate(self, args, on_validate=False):
|
||||
if isinstance(args, str):
|
||||
@@ -779,6 +804,22 @@ class Asset(AccountsController):
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
|
||||
def has_gl_entries(doctype, docname, target_account):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(gl_entry.account)
|
||||
.where(
|
||||
(gl_entry.voucher_type == doctype)
|
||||
& (gl_entry.voucher_no == docname)
|
||||
& (gl_entry.debit != 0)
|
||||
& (gl_entry.account == target_account)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return len(gl_entries) > 0
|
||||
|
||||
|
||||
def update_maintenance_status():
|
||||
assets = frappe.get_all(
|
||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")}
|
||||
@@ -904,7 +945,7 @@ def transfer_asset(args):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
books.append(
|
||||
@@ -1035,6 +1076,14 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
return asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_active_capitalization(asset):
|
||||
active_capitalizations = frappe.db.count(
|
||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||
)
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -234,7 +234,7 @@ class TestAsset(AssetSetup):
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
add_days(get_last_day(add_months(purchase_date, 1)), 1),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
@@ -320,7 +320,7 @@ class TestAsset(AssetSetup):
|
||||
pro_rata_amount, _, _ = _get_pro_rata_amt(
|
||||
asset.finance_books[0],
|
||||
9000,
|
||||
get_last_day(add_months(purchase_date, 1)),
|
||||
add_days(get_last_day(add_months(purchase_date, 1)), 1),
|
||||
date,
|
||||
original_schedule_date=get_last_day(nowdate()),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -317,7 +317,16 @@ class AssetCapitalization(StockController):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
if self.capitalization_method == "Create a new composite asset" and not (
|
||||
self.get("stock_items") or self.get("asset_items")
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
|
||||
)
|
||||
)
|
||||
|
||||
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
@@ -460,13 +469,24 @@ class AssetCapitalization(StockController):
|
||||
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
return self.target_fixed_asset_account
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
|
||||
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
|
||||
if is_cwip_accounting_enabled(asset_category):
|
||||
target_account = get_asset_category_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=asset_category,
|
||||
company=self.company,
|
||||
)
|
||||
return target_account if target_account else self.target_fixed_asset_account
|
||||
else:
|
||||
return self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
|
||||
@@ -554,13 +574,13 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.target_fixed_asset_account,
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
|
||||
@@ -31,6 +31,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_perpetual_inventory(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
# Variables
|
||||
consumed_asset_value = 100000
|
||||
@@ -187,9 +193,10 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test General Ledger Entries
|
||||
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 3000,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000,
|
||||
default_expense_account: -2000,
|
||||
"_Test Fixed Asset - _TC": -100000.0,
|
||||
default_expense_account: -2000.0,
|
||||
"CWIP Account - _TC": 103000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
@@ -214,6 +221,12 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
def test_capitalization_with_wip_composite_asset(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
@@ -424,7 +437,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 1000.0,
|
||||
"CWIP Account - _TC": 1000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
|
||||
|
||||
@@ -291,7 +291,9 @@ class AssetDepreciationSchedule(Document):
|
||||
if skip_row:
|
||||
continue
|
||||
|
||||
schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
|
||||
schedule_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
|
||||
)
|
||||
if not current_fiscal_year_end_date:
|
||||
current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
|
||||
elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
|
||||
@@ -330,8 +332,10 @@ class AssetDepreciationSchedule(Document):
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
|
||||
)
|
||||
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(from_date)
|
||||
if self.depreciation_schedule:
|
||||
from_date = self.depreciation_schedule[-1].schedule_date
|
||||
from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
|
||||
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
@@ -353,9 +357,8 @@ class AssetDepreciationSchedule(Document):
|
||||
and not self.opening_accumulated_depreciation
|
||||
and not self.flags.wdv_it_act_applied
|
||||
):
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
from_date = asset_doc.available_for_use_date
|
||||
# needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
@@ -406,6 +409,8 @@ class AssetDepreciationSchedule(Document):
|
||||
(n + self.opening_number_of_booked_depreciations)
|
||||
* cint(row.frequency_of_depreciation),
|
||||
)
|
||||
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
asset_doc.to_date = get_last_day(asset_doc.to_date)
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
@@ -421,7 +426,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount_without_pro_rata, depreciation_amount
|
||||
)
|
||||
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
schedule_date = add_days(schedule_date, days - 1)
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
@@ -504,7 +509,10 @@ class AssetDepreciationSchedule(Document):
|
||||
continue
|
||||
|
||||
if not accumulated_depreciation:
|
||||
if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if i > 0 and (
|
||||
asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
|
||||
or asset_doc.flags.increase_in_asset_value_due_to_repair
|
||||
):
|
||||
accumulated_depreciation = self.get("depreciation_schedule")[
|
||||
i - 1
|
||||
].accumulated_depreciation_amount
|
||||
@@ -553,9 +561,11 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
prev_depreciation_start_date = add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
|
||||
prev_depreciation_start_date = get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
|
||||
)
|
||||
)
|
||||
from_date = asset_doc.available_for_use_date
|
||||
days = date_diff(prev_depreciation_start_date, from_date) + 1
|
||||
@@ -610,7 +620,7 @@ def _get_pro_rata_amt(
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
original_schedule_date=None,
|
||||
):
|
||||
days = date_diff(to_date, from_date)
|
||||
days = date_diff(to_date, from_date) + 1
|
||||
months = month_diff(to_date, from_date)
|
||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||
@@ -670,7 +680,7 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
number_of_pending_depreciations
|
||||
)
|
||||
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
@@ -729,10 +739,15 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * (asset.opening_number_of_booked_depreciations + 1))
|
||||
* -1,
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(
|
||||
row.frequency_of_depreciation
|
||||
* (asset.opening_number_of_booked_depreciations + 1)
|
||||
)
|
||||
* -1,
|
||||
),
|
||||
),
|
||||
1,
|
||||
),
|
||||
@@ -752,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
depr_period_start_date = add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1
|
||||
)
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12)
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
|
||||
@@ -1029,6 +1048,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
date_of_return=None,
|
||||
value_after_depreciation=None,
|
||||
ignore_booked_entry=False,
|
||||
difference_amount=None,
|
||||
):
|
||||
for row in asset_doc.get("finance_books"):
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
@@ -1043,6 +1063,8 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
)
|
||||
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
||||
value_after_depreciation = row.value_after_depreciation + difference_amount
|
||||
|
||||
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
||||
"Written Down Value",
|
||||
|
||||
@@ -29,6 +29,15 @@ frappe.ui.form.on("Asset Repair", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_invoice", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("warehouse", "stock_items", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -117,7 +117,9 @@ class AssetRepair(AccountsController):
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
self.asset_doc, notes, ignore_booked_entry=True
|
||||
)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
@@ -154,7 +156,9 @@ class AssetRepair(AccountsController):
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
self.asset_doc, notes, ignore_booked_entry=True
|
||||
)
|
||||
self.asset_doc.save()
|
||||
|
||||
add_asset_activity(
|
||||
|
||||
@@ -56,7 +56,8 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_asset(self.current_asset_value)
|
||||
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
|
||||
self.update_asset()
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
|
||||
@@ -144,7 +145,7 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
self.db_set("journal_entry", je.name)
|
||||
|
||||
def update_asset(self, asset_value):
|
||||
def update_asset(self, asset_value=None):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
if not asset.calculate_depreciation:
|
||||
@@ -170,7 +171,11 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
|
||||
asset,
|
||||
notes,
|
||||
value_after_depreciation=asset_value,
|
||||
ignore_booked_entry=True,
|
||||
difference_amount=self.difference_amount,
|
||||
)
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.save()
|
||||
|
||||
@@ -12,6 +12,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_repair.test_asset_repair import create_asset_repair
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
||||
@@ -128,6 +129,136 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_depreciation_after_cancelling_asset_repair(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 200,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
|
||||
post_depreciation_entries(getdate("2023-08-21"))
|
||||
|
||||
# create asset repair
|
||||
asset_repair = create_asset_repair(asset=asset_doc, capitalize_repair_cost=1, submit=1)
|
||||
|
||||
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Active")
|
||||
|
||||
# create asset value adjustment
|
||||
current_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_value,
|
||||
new_asset_value=50000.0,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
|
||||
first_asset_depr_schedule.load_from_db()
|
||||
|
||||
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||
self.assertEqual(second_asset_depr_schedule.status, "Active")
|
||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
# Test gl entry creted from asset value adjustemnet
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 0.0, 5625.29),
|
||||
("_Test Depreciations - _TC", 5625.29, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_no = %s
|
||||
order by account""",
|
||||
adj_doc.journal_entry,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
# test depreciation schedule after asset repair and asset value adjustemnet
|
||||
expected_schedules = [
|
||||
["2023-01-31", 5474.73, 5474.73],
|
||||
["2023-02-28", 9983.33, 15458.06],
|
||||
["2023-03-31", 9983.33, 25441.39],
|
||||
["2023-04-30", 9983.33, 35424.72],
|
||||
["2023-05-31", 9983.33, 45408.05],
|
||||
["2023-06-30", 9983.33, 55391.38],
|
||||
["2023-07-31", 9983.33, 65374.71],
|
||||
["2023-08-31", 2766.67, 68141.38],
|
||||
["2023-09-30", 2766.67, 70908.05],
|
||||
["2023-10-31", 2766.67, 73674.72],
|
||||
["2023-11-30", 2766.67, 76441.39],
|
||||
["2023-12-31", 2766.67, 79208.06],
|
||||
["2024-01-31", 2766.67, 81974.73],
|
||||
["2024-02-29", 2766.67, 84741.4],
|
||||
["2024-03-31", 2766.67, 87508.07],
|
||||
["2024-04-30", 2766.67, 90274.74],
|
||||
["2024-05-31", 2766.67, 93041.41],
|
||||
["2024-06-30", 2766.67, 95808.08],
|
||||
["2024-07-31", 2766.67, 98574.75],
|
||||
["2024-08-31", 2766.67, 101341.42],
|
||||
["2024-09-30", 2766.67, 104108.09],
|
||||
["2024-10-31", 2766.67, 106874.76],
|
||||
["2024-11-30", 2766.67, 109641.43],
|
||||
["2024-12-31", 2766.67, 112408.1],
|
||||
["2025-01-15", 2766.61, 115174.71],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in second_asset_depr_schedule.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Cancel asset repair
|
||||
asset_repair.cancel()
|
||||
asset_repair.load_from_db()
|
||||
second_asset_depr_schedule.load_from_db()
|
||||
|
||||
third_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
|
||||
self.assertEqual(third_asset_depr_schedule.status, "Active")
|
||||
self.assertEqual(second_asset_depr_schedule.status, "Cancelled")
|
||||
|
||||
# After cancelling asset repair asset life will be decreased and new depreciation schedule should be calculated
|
||||
expected_schedules = [
|
||||
["2023-01-31", 5474.73, 5474.73],
|
||||
["2023-02-28", 9983.33, 15458.06],
|
||||
["2023-03-31", 9983.33, 25441.39],
|
||||
["2023-04-30", 9983.33, 35424.72],
|
||||
["2023-05-31", 9983.33, 45408.05],
|
||||
["2023-06-30", 9983.33, 55391.38],
|
||||
["2023-07-31", 9983.33, 65374.71],
|
||||
["2023-08-31", 8133.33, 73508.04],
|
||||
["2023-09-30", 8133.33, 81641.37],
|
||||
["2023-10-31", 8133.33, 89774.7],
|
||||
["2023-11-30", 8133.33, 97908.03],
|
||||
["2023-12-31", 8133.33, 106041.36],
|
||||
["2024-01-15", 8133.35, 114174.71],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in third_asset_depr_schedule.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -97,7 +97,7 @@ class Supplier(TransactionBase):
|
||||
elif supp_master_name == "Naming Series":
|
||||
set_name_by_naming_series(self)
|
||||
else:
|
||||
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
|
||||
|
||||
def on_update(self):
|
||||
self.create_primary_contact()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Item-wise Purchase History"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
reqd: 1,
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group",
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
let format_fields = ["received_qty", "billed_amt"];
|
||||
|
||||
if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green;'>" + value + "</span>";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-05-03 14:55:53",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:57.446613",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Item-wise Purchase History",
|
||||
"owner": "Administrator",
|
||||
"query": "select\n po_item.item_code as \"Item Code:Link/Item:120\",\n\tpo_item.item_name as \"Item Name::120\",\n po_item.item_group as \"Item Group:Link/Item Group:120\",\n\tpo_item.description as \"Description::150\",\n\tpo_item.qty as \"Qty:Float:100\",\n\tpo_item.uom as \"UOM:Link/UOM:80\",\n\tpo_item.base_rate as \"Rate:Currency:120\",\n\tpo_item.base_amount as \"Amount:Currency:120\",\n\tpo.name as \"Purchase Order:Link/Purchase Order:120\",\n\tpo.transaction_date as \"Transaction Date:Date:140\",\n\tpo.supplier as \"Supplier:Link/Supplier:130\",\n sup.supplier_name as \"Supplier Name::150\",\n\tpo_item.project as \"Project:Link/Project:130\",\n\tifnull(po_item.received_qty, 0) as \"Received Qty:Float:120\",\n\tpo.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order` po, `tabPurchase Order Item` po_item, `tabSupplier` sup\nwhere\n\tpo.name = po_item.parent and po.supplier = sup.name and po.docstatus = 1\norder by po.name desc",
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Item-wise Purchase History",
|
||||
"report_type": "Query Report",
|
||||
"add_total_row": 1,
|
||||
"creation": "2013-05-03 14:55:53",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 5,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2024-06-19 12:12:15.418799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Item-wise Purchase History",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Item-wise Purchase History",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date cannot be greater than To Date"))
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
chart_data = get_chart_data(data)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"options": "Item",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Item Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "item_name",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_group",
|
||||
"options": "Item Group",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Description"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "description",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "quantity",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("UOM"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "uom",
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
"label": _("Transaction Date"),
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "transaction_date",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "supplier",
|
||||
"options": "Supplier",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "supplier_name",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "supplier_group",
|
||||
"options": "Supplier Group",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Project"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "project",
|
||||
"options": "Project",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Received Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "received_qty",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Billed Amount"),
|
||||
"fieldtype": "Currency",
|
||||
"fieldname": "billed_amt",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "company",
|
||||
"options": "Company",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "currency",
|
||||
"options": "Currency",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
|
||||
company_list = get_descendants_of("Company", filters.get("company"))
|
||||
company_list.append(filters.get("company"))
|
||||
|
||||
supplier_details = get_supplier_details()
|
||||
item_details = get_item_details()
|
||||
purchase_order_records = get_purchase_order_details(company_list, filters)
|
||||
|
||||
for record in purchase_order_records:
|
||||
supplier_record = supplier_details.get(record.supplier)
|
||||
item_record = item_details.get(record.item_code)
|
||||
row = {
|
||||
"item_code": record.get("item_code"),
|
||||
"item_name": item_record.get("item_name"),
|
||||
"item_group": item_record.get("item_group"),
|
||||
"description": record.get("description"),
|
||||
"quantity": record.get("qty"),
|
||||
"uom": record.get("uom"),
|
||||
"rate": record.get("base_rate"),
|
||||
"amount": record.get("base_amount"),
|
||||
"purchase_order": record.get("name"),
|
||||
"transaction_date": record.get("transaction_date"),
|
||||
"supplier": record.get("supplier"),
|
||||
"supplier_name": supplier_record.get("supplier_name"),
|
||||
"supplier_group": supplier_record.get("supplier_group"),
|
||||
"project": record.get("project"),
|
||||
"received_qty": flt(record.get("received_qty")),
|
||||
"billed_amt": flt(record.get("billed_amt")),
|
||||
"company": record.get("company"),
|
||||
}
|
||||
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_supplier_details():
|
||||
details = frappe.get_all("Supplier", fields=["name", "supplier_name", "supplier_group"])
|
||||
supplier_details = {}
|
||||
for d in details:
|
||||
supplier_details.setdefault(
|
||||
d.name,
|
||||
frappe._dict({"supplier_name": d.supplier_name, "supplier_group": d.supplier_group}),
|
||||
)
|
||||
return supplier_details
|
||||
|
||||
|
||||
def get_item_details():
|
||||
details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"])
|
||||
item_details = {}
|
||||
for d in details:
|
||||
item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}))
|
||||
return item_details
|
||||
|
||||
|
||||
def get_purchase_order_details(company_list, filters):
|
||||
db_po = frappe.qb.DocType("Purchase Order")
|
||||
db_po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(db_po)
|
||||
.inner_join(db_po_item)
|
||||
.on(db_po_item.parent == db_po.name)
|
||||
.select(
|
||||
db_po.name,
|
||||
db_po.supplier,
|
||||
db_po.transaction_date,
|
||||
db_po.project,
|
||||
db_po.company,
|
||||
db_po_item.item_code,
|
||||
db_po_item.description,
|
||||
db_po_item.qty,
|
||||
db_po_item.uom,
|
||||
db_po_item.base_rate,
|
||||
db_po_item.base_amount,
|
||||
db_po_item.received_qty,
|
||||
(db_po_item.billed_amt * db_po.conversion_rate).as_("billed_amt"),
|
||||
)
|
||||
.where(db_po.docstatus == 1)
|
||||
.where(db_po.company.isin(tuple(company_list)))
|
||||
)
|
||||
|
||||
for field in ("item_code", "item_group"):
|
||||
if filters.get(field):
|
||||
query = query.where(db_po_item[field] == filters[field])
|
||||
|
||||
if filters.get("from_date"):
|
||||
query = query.where(db_po.transaction_date >= filters.from_date)
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(db_po.transaction_date <= filters.to_date)
|
||||
|
||||
if filters.get("supplier"):
|
||||
query = query.where(db_po.supplier == filters.supplier)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_chart_data(data):
|
||||
item_wise_purchase_map = {}
|
||||
labels, datapoints = [], []
|
||||
|
||||
for row in data:
|
||||
item_key = row.get("item_code")
|
||||
|
||||
if item_key not in item_wise_purchase_map:
|
||||
item_wise_purchase_map[item_key] = 0
|
||||
|
||||
item_wise_purchase_map[item_key] = flt(item_wise_purchase_map[item_key]) + flt(row.get("amount"))
|
||||
|
||||
item_wise_purchase_map = {
|
||||
item: value
|
||||
for item, value in (sorted(item_wise_purchase_map.items(), key=lambda i: i[1], reverse=True))
|
||||
}
|
||||
|
||||
for key in item_wise_purchase_map:
|
||||
labels.append(key)
|
||||
datapoints.append(item_wise_purchase_map[key])
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"labels": labels[:30], # show max of 30 items in chart
|
||||
"datasets": [{"name": _("Total Purchase Amount"), "values": datapoints[:30]}],
|
||||
},
|
||||
"type": "bar",
|
||||
"fieldtype": "Currency",
|
||||
}
|
||||
@@ -40,6 +40,7 @@ def get_data(filters):
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
@@ -47,6 +48,8 @@ def get_data(filters):
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.left_join(pr_item)
|
||||
.on((pr_item.purchase_order_item == po_item.name) & (pr_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
@@ -60,7 +63,7 @@ def get_data(filters):
|
||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||
po_item.base_amount.as_("amount"),
|
||||
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
||||
(pr_item.base_amount).as_("received_qty_amount"),
|
||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
|
||||
"pending_amount"
|
||||
|
||||
@@ -14,6 +14,9 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_
|
||||
def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
"""updates last_purchase_rate in item table for each item"""
|
||||
|
||||
if doc.get("is_internal_supplier"):
|
||||
return
|
||||
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
|
||||
@@ -85,7 +85,6 @@ force_item_fields = (
|
||||
"brand",
|
||||
"stock_uom",
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"pricing_rules",
|
||||
"weight_per_unit",
|
||||
"weight_uom",
|
||||
@@ -234,7 +233,7 @@ class AccountsController(TransactionBase):
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold("Update Outstanding for Self"),
|
||||
frappe.bold(_("Update Outstanding for Self")),
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
)
|
||||
)
|
||||
@@ -743,7 +742,6 @@ class AccountsController(TransactionBase):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
if item.get(fieldname) is None or fieldname in force_item_fields:
|
||||
@@ -753,7 +751,10 @@ class AccountsController(TransactionBase):
|
||||
fieldname
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "item_tax_rate" and not (
|
||||
self.get("is_return") and self.get("return_against")
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
elif fieldname == "serial_no":
|
||||
# Ensure that serial numbers are matched against Stock UOM
|
||||
item_conversion_factor = item.get("conversion_factor") or 1.0
|
||||
@@ -1333,6 +1334,12 @@ class AccountsController(TransactionBase):
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if self.docstatus == 1:
|
||||
if dimensions_dict is None:
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
if self.get("doctype") == "Journal Entry":
|
||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
||||
@@ -1961,7 +1968,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def raise_missing_debit_credit_account_error(self, party_type, party):
|
||||
"""Raise an error if debit to/credit to account does not exist."""
|
||||
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
|
||||
db_or_cr = (
|
||||
frappe.bold(_("Debit To")) if self.doctype == "Sales Invoice" else frappe.bold(_("Credit To"))
|
||||
)
|
||||
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
|
||||
|
||||
link_to_party = frappe.utils.get_link_to_form(party_type, party)
|
||||
@@ -2432,6 +2441,15 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
@@ -3075,9 +3093,9 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||
if not child_item.warehouse:
|
||||
frappe.throw(
|
||||
_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.").format(
|
||||
frappe.bold("default warehouse"), frappe.bold(item.item_code)
|
||||
)
|
||||
_(
|
||||
"Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings."
|
||||
).format(frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||
|
||||
@@ -314,18 +314,22 @@ class BuyingController(SubcontractingController):
|
||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ item.rm_supp_cost
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("rate_difference_with_purchase_invoice"))
|
||||
@@ -336,72 +340,88 @@ class BuyingController(SubcontractingController):
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
"""
|
||||
Override item rate with incoming rate for internal stock transfer
|
||||
"""
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if not (self.doctype == "Purchase Receipt" or self.get("update_stock")):
|
||||
return
|
||||
|
||||
if cint(self.get("is_return")):
|
||||
# Get outgoing rate based on original item cost based on valuation method
|
||||
return
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
if allow_at_arms_length_price:
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
for d in self.get("items"):
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
|
||||
if d.rate == d.sales_incoming_rate:
|
||||
continue
|
||||
|
||||
d.rate = d.sales_incoming_rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def set_sales_incoming_rate_for_internal_transfer(self):
|
||||
"""
|
||||
Set incoming rate from the sales transaction against which the
|
||||
purchase is made (internal transfer)
|
||||
"""
|
||||
ref_doctype_map = {
|
||||
"Purchase Order": "Sales Order Item",
|
||||
"Purchase Receipt": "Delivery Note Item",
|
||||
"Purchase Invoice": "Sales Invoice Item",
|
||||
}
|
||||
|
||||
ref_doctype = ref_doctype_map.get(self.doctype)
|
||||
items = self.get("items")
|
||||
for d in items:
|
||||
if not cint(self.get("is_return")):
|
||||
# Get outgoing rate based on original item cost based on valuation method
|
||||
for d in self.get("items"):
|
||||
if not d.get(frappe.scrub(ref_doctype)):
|
||||
posting_time = self.get("posting_time")
|
||||
if not posting_time:
|
||||
posting_time = nowtime()
|
||||
|
||||
if not d.get(frappe.scrub(ref_doctype)):
|
||||
posting_time = self.get("posting_time")
|
||||
if not posting_time and self.doctype == "Purchase Order":
|
||||
posting_time = nowtime()
|
||||
outgoing_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
"posting_date": self.get("posting_date") or self.get("transaction_date"),
|
||||
"posting_time": posting_time,
|
||||
"qty": -1 * flt(d.get("stock_qty")),
|
||||
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"voucher_detail_no": d.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
outgoing_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
"posting_date": self.get("posting_date") or self.get("transaction_date"),
|
||||
"posting_time": posting_time,
|
||||
"qty": -1 * flt(d.get("stock_qty")),
|
||||
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"voucher_detail_no": d.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = (
|
||||
"incoming_rate"
|
||||
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
|
||||
else "rate"
|
||||
)
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Purchase Receipt" or self.get("update_stock"):
|
||||
if rate != d.rate:
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
d.sales_incoming_rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
def validate_for_subcontracting(self):
|
||||
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
|
||||
@@ -566,11 +586,9 @@ class BuyingController(SubcontractingController):
|
||||
if d.from_warehouse:
|
||||
sle.dependant_sle_voucher_detail_no = d.name
|
||||
else:
|
||||
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
|
||||
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
|
||||
sle.update(
|
||||
{
|
||||
"incoming_rate": incoming_rate,
|
||||
"incoming_rate": d.valuation_rate,
|
||||
"recalculate_rate": 1
|
||||
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
|
||||
else 0,
|
||||
@@ -671,9 +689,11 @@ class BuyingController(SubcontractingController):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
self.process_fixed_asset()
|
||||
|
||||
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
|
||||
"Buying Settings", "disable_last_purchase_rate"
|
||||
):
|
||||
if self.doctype in [
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
update_last_purchase_rate(self, is_submit=1)
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -802,6 +822,8 @@ class BuyingController(SubcontractingController):
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
|
||||
"purchase_receipt_item": row.name if self.doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ def validate_item_attribute_value(attributes_list, attribute, attribute_value, i
|
||||
)
|
||||
msg += "<br>" + _(
|
||||
"To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings."
|
||||
).format(frappe.bold("Allow Rename Attribute Value"))
|
||||
).format(frappe.bold(_("Allow Rename Attribute Value")))
|
||||
|
||||
frappe.throw(msg, InvalidItemAttributeValueError, title=_("Edit Not Allowed"))
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
|
||||
def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None):
|
||||
query_filter = {"item": filters.get("item_code")}
|
||||
query_filter = {"item": filters.get("item_code"), "disabled": 0}
|
||||
if txt:
|
||||
query_filter["name"] = ("like", f"%{txt}%")
|
||||
|
||||
|
||||
@@ -319,6 +319,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
def set_missing_values(source, target):
|
||||
doc = frappe.get_doc(target)
|
||||
doc.is_return = 1
|
||||
doc.ignore_pricing_rule = 1
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
@@ -478,6 +480,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
target_doc.pricing_rules = None
|
||||
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
|
||||
@@ -435,6 +435,9 @@ class SellingController(StockController):
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||
@@ -481,6 +484,9 @@ class SellingController(StockController):
|
||||
if d.incoming_rate != incoming_rate:
|
||||
d.incoming_rate = incoming_rate
|
||||
else:
|
||||
if allow_at_arms_length_price:
|
||||
continue
|
||||
|
||||
rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("rate"),
|
||||
@@ -691,7 +697,7 @@ class SellingController(StockController):
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
|
||||
@@ -92,6 +92,9 @@ class calculate_taxes_and_totals:
|
||||
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
||||
|
||||
def validate_item_tax_template(self):
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
@@ -241,7 +244,6 @@ class calculate_taxes_and_totals:
|
||||
"tax_fraction_for_current_item",
|
||||
"grand_total_fraction_for_current_item",
|
||||
]
|
||||
|
||||
if tax.charge_type != "Actual" and not (
|
||||
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
|
||||
):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
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.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
@@ -804,6 +805,41 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
|
||||
def test_16_internal_transfer_at_arms_length_price(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
target_warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company)
|
||||
warehouse = create_warehouse("_Test Internal Warehouse New 2", company=company)
|
||||
arms_length_price = 40
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
debit_to="Debtors - TCP1",
|
||||
target_warehouse=target_warehouse,
|
||||
warehouse=warehouse,
|
||||
income_account="Sales - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
update_stock=True,
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
si.items[0].rate = arms_length_price
|
||||
si.save()
|
||||
# rate should not reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, arms_length_price)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 0)
|
||||
si.items[0].rate = arms_length_price
|
||||
si.save()
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
{
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
"label": "Expected Closing Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
@@ -357,6 +358,7 @@
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Opportunity Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1,
|
||||
@@ -388,6 +390,7 @@
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -622,7 +625,7 @@
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-10-13 12:42:21.545636",
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
||||
@@ -93,7 +93,26 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
|
||||
def onload(self):
|
||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||
|
||||
load_address_and_contact(ref_doc)
|
||||
load_address_and_contact(self)
|
||||
|
||||
ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
|
||||
opportunity_doc_contact_list = [
|
||||
contact
|
||||
for contact in self.get("__onload").get("contact_list")
|
||||
if contact not in ref_doc_contact_list
|
||||
]
|
||||
ref_doc_contact_list.extend(opportunity_doc_contact_list)
|
||||
ref_doc.set_onload("contact_list", ref_doc_contact_list)
|
||||
|
||||
ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
|
||||
opportunity_doc_addr_list = [
|
||||
addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
|
||||
]
|
||||
ref_doc_addr_list.extend(opportunity_doc_addr_list)
|
||||
ref_doc.set_onload("addr_list", ref_doc_addr_list)
|
||||
|
||||
self.set("__onload", ref_doc.get("__onload"))
|
||||
|
||||
def after_insert(self):
|
||||
|
||||
@@ -27,6 +27,29 @@ class TestProspect(unittest.TestCase):
|
||||
address_doc.reload()
|
||||
self.assertEqual(address_doc.has_link("Prospect", prospect_doc.name), True)
|
||||
|
||||
def test_make_customer_from_prospect(self):
|
||||
from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect
|
||||
|
||||
frappe.delete_doc_if_exists("Customer", "_Test Prospect")
|
||||
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prospect",
|
||||
"company_name": "_Test Prospect",
|
||||
"customer_group": "_Test Customer Group",
|
||||
}
|
||||
)
|
||||
prospect.insert()
|
||||
|
||||
customer = make_customer_from_prospect("_Test Prospect")
|
||||
|
||||
self.assertEqual(customer.doctype, "Customer")
|
||||
self.assertEqual(customer.company_name, "_Test Prospect")
|
||||
self.assertEqual(customer.customer_group, "_Test Customer Group")
|
||||
|
||||
customer.company = "_Test Company"
|
||||
customer.insert()
|
||||
|
||||
|
||||
def make_prospect(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -108,7 +108,9 @@ class OpportunitySummaryBySalesStage:
|
||||
self.grouped_data = []
|
||||
|
||||
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
|
||||
for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
|
||||
for (sales_stage, _based_on), rows in groupby(
|
||||
sorted(self.query_result, key=grouping_key), key=grouping_key
|
||||
):
|
||||
self.grouped_data.append(
|
||||
{
|
||||
"sales_stage": sales_stage,
|
||||
|
||||
@@ -8,7 +8,7 @@ from itertools import groupby
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
@@ -21,7 +21,15 @@ class SalesPipelineAnalytics:
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
|
||||
def validate_filters(self):
|
||||
if not self.filters.from_date:
|
||||
frappe.throw(_("From Date is mandatory"))
|
||||
|
||||
if not self.filters.to_date:
|
||||
frappe.throw(_("To Date is mandatory"))
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
self.get_chart_data()
|
||||
@@ -114,7 +122,9 @@ class SalesPipelineAnalytics:
|
||||
self.grouped_data = []
|
||||
|
||||
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
|
||||
for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
|
||||
for (pipeline_by, period_by), rows in groupby(
|
||||
sorted(self.query_result, key=grouping_key), grouping_key
|
||||
):
|
||||
self.grouped_data.append(
|
||||
{
|
||||
self.pipeline_by: pipeline_by,
|
||||
@@ -185,7 +195,7 @@ class SalesPipelineAnalytics:
|
||||
count_or_amount = info.get(based_on)
|
||||
|
||||
if self.filters.get("pipeline_by") == "Owner":
|
||||
if value == "Not Assigned" or value == "[]" or value is None:
|
||||
if value == "Not Assigned" or value == "[]" or value is None or not value:
|
||||
assigned_to = ["Not Assigned"]
|
||||
else:
|
||||
assigned_to = json.loads(value)
|
||||
@@ -227,10 +237,9 @@ class SalesPipelineAnalytics:
|
||||
|
||||
def get_month_list(self):
|
||||
month_list = []
|
||||
current_date = date.today()
|
||||
month_number = date.today().month
|
||||
current_date = getdate(self.filters.get("from_date"))
|
||||
|
||||
for _month in range(month_number, 13):
|
||||
while current_date < getdate(self.filters.get("to_date")):
|
||||
month_list.append(current_date.strftime("%B"))
|
||||
current_date = current_date + relativedelta(months=1)
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
|
||||
|
||||
|
||||
class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
class TestSalesPipelineAnalytics(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Opportunity")
|
||||
create_company()
|
||||
create_customer()
|
||||
create_opportunity()
|
||||
|
||||
def test_sales_pipeline_analytics(self):
|
||||
self.from_date = "2021-01-01"
|
||||
self.to_date = "2021-12-31"
|
||||
self.check_for_monthly_and_number()
|
||||
self.check_for_monthly_and_amount()
|
||||
self.check_for_quarterly_and_number()
|
||||
@@ -28,6 +30,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -43,6 +47,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -59,6 +65,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -74,6 +82,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -90,6 +100,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -105,6 +117,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -121,6 +135,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -136,6 +152,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -153,8 +171,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"opportunity_source": "Cold Calling",
|
||||
"from_date": "2021-08-01",
|
||||
"to_date": "2021-08-31",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
@@ -96,7 +96,7 @@ def add_bank_accounts(response, bank, company):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please setup and enable a group account with the Account Type - {0} for the company {1}"
|
||||
).format(frappe.bold("Bank"), company)
|
||||
).format(frappe.bold(_("Bank")), company)
|
||||
)
|
||||
|
||||
for account in response["accounts"]:
|
||||
|
||||
@@ -10,7 +10,17 @@ source_link = "https://github.com/frappe/erpnext"
|
||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||
|
||||
|
||||
develop_version = "14.x.x-develop"
|
||||
add_to_apps_screen = [
|
||||
{
|
||||
"name": "erpnext",
|
||||
"logo": "/assets/erpnext/images/erpnext-logo-blue.png",
|
||||
"title": "ERPNext",
|
||||
"route": "/app/home",
|
||||
"has_permission": "erpnext.check_app_permission",
|
||||
}
|
||||
]
|
||||
|
||||
develop_version = "15.x.x-develop"
|
||||
|
||||
app_include_js = "erpnext.bundle.js"
|
||||
app_include_css = "erpnext.bundle.css"
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"bom_and_work_order_tab",
|
||||
"raw_materials_consumption_section",
|
||||
"material_consumption",
|
||||
"get_rm_cost_from_consumption_entry",
|
||||
"column_break_3",
|
||||
"backflush_raw_materials_based_on",
|
||||
"capacity_planning",
|
||||
"disable_capacity_planning",
|
||||
"allow_overtime",
|
||||
"allow_production_on_holidays",
|
||||
"column_break_5",
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"validate_components_quantities_per_bom",
|
||||
"bom_section",
|
||||
"update_bom_costs_automatically",
|
||||
"column_break_lhyt",
|
||||
"manufacture_sub_assembly_in_operation",
|
||||
"section_break_6",
|
||||
"default_wip_warehouse",
|
||||
"default_fg_warehouse",
|
||||
@@ -30,8 +29,14 @@
|
||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||
"column_break_24",
|
||||
"job_card_excess_transfer",
|
||||
"capacity_planning",
|
||||
"disable_capacity_planning",
|
||||
"allow_overtime",
|
||||
"allow_production_on_holidays",
|
||||
"column_break_5",
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"other_settings_section",
|
||||
"update_bom_costs_automatically",
|
||||
"set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"column_break_23",
|
||||
"make_serial_no_batch_from_work_order"
|
||||
@@ -149,7 +154,7 @@
|
||||
{
|
||||
"fieldname": "raw_materials_consumption_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Raw Materials Consumption"
|
||||
"label": "Raw Materials Consumption "
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -183,8 +188,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "job_card_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Job Card"
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Job Card and Capacity Planning"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_24",
|
||||
@@ -210,13 +215,41 @@
|
||||
"fieldname": "get_rm_cost_from_consumption_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Get Raw Materials Cost from Consumption Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_and_work_order_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "BOM and Production"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "BOM"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lhyt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
|
||||
"fieldname": "manufacture_sub_assembly_in_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manufacture Sub-assembly in Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||
"fieldname": "validate_components_quantities_per_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Components Quantities Per BOM"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-08 19:00:37.561244",
|
||||
"modified": "2024-09-02 12:12:03.132567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
@@ -234,4 +267,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,22 @@ class ManufacturingSettings(Document):
|
||||
get_rm_cost_from_consumption_entry: DF.Check
|
||||
job_card_excess_transfer: DF.Check
|
||||
make_serial_no_batch_from_work_order: DF.Check
|
||||
manufacture_sub_assembly_in_operation: DF.Check
|
||||
material_consumption: DF.Check
|
||||
mins_between_operations: DF.Int
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
overproduction_percentage_for_work_order: DF.Percent
|
||||
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
|
||||
update_bom_costs_automatically: DF.Check
|
||||
validate_components_quantities_per_bom: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
def before_save(self):
|
||||
self.reset_values()
|
||||
|
||||
def reset_values(self):
|
||||
if self.backflush_raw_materials_based_on != "BOM" and self.validate_components_quantities_per_bom:
|
||||
self.validate_components_quantities_per_bom = 0
|
||||
|
||||
|
||||
def get_mins_between_operations():
|
||||
|
||||
@@ -2053,6 +2053,108 @@ class TestWorkOrder(FrappeTestCase):
|
||||
"BOM",
|
||||
)
|
||||
|
||||
def test_disassemby_order(self):
|
||||
fg_item = "Test Disassembly Item"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=1,
|
||||
source_warehouse=source_warehouse,
|
||||
skip_transfer=1,
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
||||
for row in stock_entry.items:
|
||||
if row.item_code in raw_materials:
|
||||
row.s_warehouse = source_warehouse
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
wo.reload()
|
||||
self.assertEqual(wo.status, "Completed")
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1))
|
||||
stock_entry.save()
|
||||
|
||||
self.assertEqual(stock_entry.purpose, "Disassemble")
|
||||
|
||||
for row in stock_entry.items:
|
||||
if row.item_code == fg_item:
|
||||
self.assertTrue(row.s_warehouse)
|
||||
self.assertFalse(row.t_warehouse)
|
||||
else:
|
||||
self.assertFalse(row.s_warehouse)
|
||||
self.assertTrue(row.t_warehouse)
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_components_qty_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
|
||||
fg_item = "Test FG Item For Component Validation"
|
||||
source_warehouse = "Stores - _TC"
|
||||
raw_materials = ["Test Component Validation RM Item 1", "Test Component Validation RM Item 2"]
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code=item,
|
||||
target=source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=10,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
|
||||
transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
transfer_entry.save()
|
||||
for row in transfer_entry.items:
|
||||
row.qty = 5
|
||||
|
||||
self.assertRaises(frappe.ValidationError, transfer_entry.save)
|
||||
|
||||
transfer_entry.reload()
|
||||
for row in transfer_entry.items:
|
||||
self.assertEqual(row.qty, 10)
|
||||
|
||||
transfer_entry.submit()
|
||||
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
manufacture_entry.save()
|
||||
for row in manufacture_entry.items:
|
||||
if not row.s_warehouse:
|
||||
continue
|
||||
|
||||
row.qty = 5
|
||||
|
||||
self.assertRaises(frappe.ValidationError, manufacture_entry.save)
|
||||
manufacture_entry.reload()
|
||||
manufacture_entry.submit()
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
@@ -2370,6 +2472,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.batch_size = args.batch_size or 0
|
||||
|
||||
if args.source_warehouse:
|
||||
wo_order.source_warehouse = args.source_warehouse
|
||||
for item in wo_order.get("required_items"):
|
||||
item.source_warehouse = args.source_warehouse
|
||||
|
||||
|
||||
@@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Completed") {
|
||||
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
|
||||
frm.add_custom_button(
|
||||
__("BOM"),
|
||||
() => {
|
||||
frm.trigger("make_bom");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
frm.doc.status == "Completed" &&
|
||||
frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture"
|
||||
frm.doc.docstatus === 1 &&
|
||||
["Closed", "Completed"].includes(frm.doc.status) &&
|
||||
frm.doc.produced_qty > 0
|
||||
) {
|
||||
frm.add_custom_button(__("Create BOM"), () => {
|
||||
frm.trigger("make_bom");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Disassembly Order"),
|
||||
() => {
|
||||
frm.trigger("make_disassembly_order");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
frm.trigger("add_custom_button_to_return_components");
|
||||
@@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
},
|
||||
|
||||
make_disassembly_order(frm) {
|
||||
erpnext.work_order
|
||||
.show_prompt_for_qty_input(frm, "Disassemble")
|
||||
.then((data) => {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Disassemble",
|
||||
qty: data.qty,
|
||||
target_warehouse: data.target_warehouse,
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||
});
|
||||
},
|
||||
|
||||
show_progress_for_items: function (frm) {
|
||||
var bars = [];
|
||||
var message = "";
|
||||
@@ -745,6 +779,10 @@ erpnext.work_order = {
|
||||
|
||||
get_max_transferable_qty: (frm, purpose) => {
|
||||
let max = 0;
|
||||
if (purpose === "Disassemble") {
|
||||
return flt(frm.doc.produced_qty);
|
||||
}
|
||||
|
||||
if (frm.doc.skip_transfer) {
|
||||
max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
|
||||
} else {
|
||||
@@ -759,15 +797,38 @@ erpnext.work_order = {
|
||||
|
||||
show_prompt_for_qty_input: function (frm, purpose) {
|
||||
let max = this.get_max_transferable_qty(frm, purpose);
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "Float",
|
||||
label: __("Qty for {0}", [__(purpose)]),
|
||||
fieldname: "qty",
|
||||
description: __("Max: {0}", [max]),
|
||||
default: max,
|
||||
},
|
||||
];
|
||||
|
||||
if (purpose === "Disassemble") {
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
fieldname: "target_warehouse",
|
||||
label: __("Target Warehouse"),
|
||||
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.prompt(
|
||||
{
|
||||
fieldtype: "Float",
|
||||
label: __("Qty for {0}", [__(purpose)]),
|
||||
fieldname: "qty",
|
||||
description: __("Max: {0}", [max]),
|
||||
default: max,
|
||||
},
|
||||
fields,
|
||||
(data) => {
|
||||
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
|
||||
|
||||
|
||||
@@ -1359,7 +1359,7 @@ def set_work_order_ops(name):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
work_order = frappe.get_doc("Work Order", work_order_id)
|
||||
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
|
||||
wip_warehouse = work_order.wip_warehouse
|
||||
@@ -1389,9 +1389,16 @@ def make_stock_entry(work_order_id, purpose, qty=None):
|
||||
stock_entry.to_warehouse = work_order.fg_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
|
||||
if purpose == "Disassemble":
|
||||
stock_entry.from_warehouse = work_order.fg_warehouse
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
return stock_entry.as_dict()
|
||||
|
||||
|
||||
|
||||
@@ -357,6 +357,7 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
@@ -370,3 +371,7 @@ erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_docty
|
||||
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
||||
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
||||
erpnext.patches.v15_0.do_not_use_batchwise_valuation
|
||||
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
|
||||
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
|
||||
erpnext.patches.v15_0.set_standard_stock_entry_type
|
||||
erpnext.patches.v15_0.link_purchase_item_to_asset_doc
|
||||
|
||||
127
erpnext/patches/v14_0/update_pos_return_ledger_entries.py
Normal file
127
erpnext/patches/v14_0/update_pos_return_ledger_entries.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
|
||||
|
||||
def get_valid_against_voucher_ref(pos_returns):
|
||||
sinv = qb.DocType("Sales Invoice")
|
||||
res = (
|
||||
qb.from_(sinv)
|
||||
.select(sinv.name, sinv.return_against)
|
||||
.where(sinv.name.isin(pos_returns) & sinv.return_against.notnull())
|
||||
.orderby(sinv.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def build_dict_of_valid_against_reference(pos_returns):
|
||||
_against_ref_dict = frappe._dict()
|
||||
res = get_valid_against_voucher_ref(pos_returns)
|
||||
for x in res:
|
||||
_against_ref_dict[x.name] = x.return_against
|
||||
return _against_ref_dict
|
||||
|
||||
|
||||
def fix_incorrect_against_voucher_ref(affected_pos_returns):
|
||||
if affected_pos_returns:
|
||||
valid_against_voucher_dict = build_dict_of_valid_against_reference(affected_pos_returns)
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
gles_with_invalid_against = (
|
||||
qb.from_(gle)
|
||||
.select(gle.name, gle.voucher_no)
|
||||
.where(
|
||||
gle.voucher_no.isin(affected_pos_returns)
|
||||
& gle.against_voucher.notnull()
|
||||
& gle.against_voucher.eq(gle.voucher_no)
|
||||
& gle.is_cancelled.eq(0)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
# Update GL
|
||||
if gles_with_invalid_against:
|
||||
for gl in gles_with_invalid_against:
|
||||
frappe.db.set_value(
|
||||
"GL Entry",
|
||||
gl.name,
|
||||
"against_voucher",
|
||||
valid_against_voucher_dict[gl.voucher_no],
|
||||
)
|
||||
|
||||
# Update Payment Ledger
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in affected_pos_returns:
|
||||
qb.update(ple).set(ple.against_voucher_no, valid_against_voucher_dict[x]).where(
|
||||
ple.voucher_no.eq(x) & ple.delinked.eq(0)
|
||||
).run()
|
||||
|
||||
|
||||
def get_pos_returns_with_invalid_against_ref():
|
||||
sinv = qb.DocType("Sales Invoice")
|
||||
pos_returns_without_self = (
|
||||
qb.from_(sinv)
|
||||
.select(sinv.name)
|
||||
.where(
|
||||
sinv.docstatus.eq(1)
|
||||
& sinv.is_pos.eq(1)
|
||||
& sinv.is_return.eq(1)
|
||||
& sinv.return_against.notnull()
|
||||
& sinv.update_outstanding_for_self.eq(0)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
if pos_returns_without_self:
|
||||
pos_returns_without_self = [x[0] for x in pos_returns_without_self]
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
gl_against_references = (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_no, gle.against_voucher)
|
||||
.where(
|
||||
gle.voucher_no.isin(pos_returns_without_self)
|
||||
& gle.against_voucher.notnull()
|
||||
& gle.against_voucher.eq(gle.voucher_no)
|
||||
& gle.is_cancelled.eq(0)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if gl_against_references:
|
||||
_vouchers = list(set([x[0] for x in gl_against_references]))
|
||||
invoice_return_against = (
|
||||
qb.from_(sinv)
|
||||
.select(sinv.name, sinv.return_against)
|
||||
.where(sinv.name.isin(_vouchers) & sinv.return_against.notnull())
|
||||
.orderby(sinv.name)
|
||||
.run()
|
||||
)
|
||||
|
||||
valid_references = set(invoice_return_against)
|
||||
actual_references = set(gl_against_references)
|
||||
|
||||
invalid_references = actual_references.difference(valid_references)
|
||||
if invalid_references:
|
||||
return [x[0] for x in invalid_references]
|
||||
return None
|
||||
|
||||
|
||||
def update_outstanding_for_affected(affected_pos_returns):
|
||||
if affected_pos_returns:
|
||||
sinv = qb.DocType("Sales Invoice")
|
||||
pos_with_accounts = (
|
||||
qb.from_(sinv)
|
||||
.select(sinv.return_against, sinv.debit_to, sinv.customer)
|
||||
.where(sinv.name.isin(affected_pos_returns))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for x in pos_with_accounts:
|
||||
update_voucher_outstanding("Sales Invoice", x.return_against, x.debit_to, "Customer", x.customer)
|
||||
|
||||
|
||||
def execute():
|
||||
affected_pos_returns = get_pos_returns_with_invalid_against_ref()
|
||||
fix_incorrect_against_voucher_ref(affected_pos_returns)
|
||||
update_outstanding_for_affected(affected_pos_returns)
|
||||
@@ -0,0 +1,13 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("Stock Entry Type", "Disassemble"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": "Disassemble",
|
||||
"purpose": "Disassemble",
|
||||
"is_standard": 1,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
@@ -0,0 +1,16 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
table = "tabStock Ledger Entry"
|
||||
index = "posting_datetime_creation_index"
|
||||
|
||||
if not frappe.db.has_index(table, index):
|
||||
return
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop index")
|
||||
74
erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py
Normal file
74
erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Asset", "purchase_invoice_item") and frappe.db.has_column(
|
||||
"Asset", "purchase_receipt_item"
|
||||
):
|
||||
# Get all assets with their related Purchase Invoice and Purchase Receipt
|
||||
assets = frappe.get_all(
|
||||
"Asset",
|
||||
filters={"docstatus": 0},
|
||||
fields=[
|
||||
"name",
|
||||
"item_code",
|
||||
"purchase_invoice",
|
||||
"purchase_receipt",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
"purchase_invoice_item",
|
||||
"purchase_receipt_item",
|
||||
],
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
# Get Purchase Invoice Items
|
||||
if asset.purchase_invoice and not asset.purchase_invoice_item:
|
||||
purchase_invoice_item = get_linked_item(
|
||||
"Purchase Invoice Item",
|
||||
asset.purchase_invoice,
|
||||
asset.item_code,
|
||||
asset.gross_purchase_amount,
|
||||
asset.asset_quantity,
|
||||
)
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_invoice_item", purchase_invoice_item)
|
||||
|
||||
# Get Purchase Receipt Items
|
||||
if asset.purchase_receipt and not asset.purchase_receipt_item:
|
||||
purchase_receipt_item = get_linked_item(
|
||||
"Purchase Receipt Item",
|
||||
asset.purchase_receipt,
|
||||
asset.item_code,
|
||||
asset.gross_purchase_amount,
|
||||
asset.asset_quantity,
|
||||
)
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_receipt_item", purchase_receipt_item)
|
||||
|
||||
|
||||
def get_linked_item(doctype, parent, item_code, amount, quantity):
|
||||
items = frappe.get_all(
|
||||
doctype,
|
||||
filters={
|
||||
"parenttype": doctype.replace(" Item", ""),
|
||||
"parent": parent,
|
||||
"item_code": item_code,
|
||||
},
|
||||
fields=["name", "rate", "amount", "qty", "landed_cost_voucher_amount"],
|
||||
)
|
||||
if len(items) == 1:
|
||||
# If only one item exists, return it directly
|
||||
return items[0].name
|
||||
|
||||
for item in items:
|
||||
landed_cost = item.get("landed_cost_voucher_amount", 0)
|
||||
# Check if the asset is grouped
|
||||
if quantity > 1:
|
||||
if item.amount + landed_cost == amount and item.qty == quantity:
|
||||
return item.name
|
||||
elif item.qty == quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.rate + (landed_cost / item.qty) == amount:
|
||||
return item.name
|
||||
|
||||
return items[0].name if items else None
|
||||
17
erpnext/patches/v15_0/set_standard_stock_entry_type.py
Normal file
17
erpnext/patches/v15_0/set_standard_stock_entry_type.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for stock_entry_type in [
|
||||
"Material Issue",
|
||||
"Material Receipt",
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
]:
|
||||
if frappe.db.exists("Stock Entry Type", stock_entry_type):
|
||||
frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1)
|
||||
@@ -379,6 +379,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
|
||||
target.project = timesheet.parent_project
|
||||
if customer:
|
||||
target.customer = customer
|
||||
default_price_list = frappe.get_value("Customer", customer, "default_price_list")
|
||||
if default_price_list:
|
||||
target.selling_price_list = default_price_list
|
||||
|
||||
if currency:
|
||||
target.currency = currency
|
||||
|
||||
BIN
erpnext/public/images/erpnext-logo-blue.png
Normal file
BIN
erpnext/public/images/erpnext-logo-blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -374,6 +374,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
label: "Cost Center",
|
||||
options: "Cost Center",
|
||||
depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: this.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
|
||||
@@ -534,6 +534,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
quotation_to: me.frm.doc.quotation_to,
|
||||
supplier: me.frm.doc.supplier,
|
||||
currency: me.frm.doc.currency,
|
||||
is_internal_supplier: me.frm.doc.is_internal_supplier,
|
||||
is_internal_customer: me.frm.doc.is_internal_customer,
|
||||
update_stock: update_stock,
|
||||
conversion_rate: me.frm.doc.conversion_rate,
|
||||
price_list: me.frm.doc.selling_price_list || me.frm.doc.buying_price_list,
|
||||
@@ -826,47 +828,80 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
var me = this;
|
||||
var set_pricing = function() {
|
||||
if(me.frm.doc.company && me.frm.fields_dict.currency) {
|
||||
var company_currency = me.get_company_currency();
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
|
||||
if (!me.frm.doc.currency) {
|
||||
me.frm.set_value("currency", company_currency);
|
||||
}
|
||||
|
||||
if (me.frm.doc.currency == company_currency) {
|
||||
me.frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
if (me.frm.doc.price_list_currency == company_currency) {
|
||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||
}
|
||||
if (company_doc){
|
||||
if (company_doc.default_letter_head) {
|
||||
if(me.frm.fields_dict.letter_head) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
|
||||
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||
selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
|
||||
me.frm.set_value("tc_name", company_doc.default_selling_terms);
|
||||
}
|
||||
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
|
||||
"Material Request", "Purchase Receipt"];
|
||||
// Purchase Invoice is excluded as per issue #3345
|
||||
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||
buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
|
||||
me.frm.set_value("tc_name", company_doc.default_buying_terms);
|
||||
}
|
||||
}
|
||||
frappe.run_serially([
|
||||
() => me.frm.script_manager.trigger("currency"),
|
||||
() => get_party_currency(),
|
||||
() => me.update_item_tax_map(),
|
||||
() => me.apply_default_taxes(),
|
||||
() => me.apply_pricing_rule()
|
||||
() => me.apply_pricing_rule(),
|
||||
() => set_terms(),
|
||||
() => set_letter_head(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
var get_party_currency = function() {
|
||||
if (me.is_a_mapped_document()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
|
||||
var party_name = me.frm.doc[party_type.toLowerCase()];
|
||||
if (party_name) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: party_type,
|
||||
filters: { name: party_name },
|
||||
fieldname: "default_currency",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
set_currency(r.message.default_currency);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
set_currency();
|
||||
}
|
||||
}
|
||||
|
||||
var set_currency = function(party_default_currency) {
|
||||
var company_currency = me.get_company_currency();
|
||||
var currency = party_default_currency || company_currency;
|
||||
if (me.frm.doc.currency != currency) {
|
||||
me.frm.set_value("currency", currency);
|
||||
}
|
||||
|
||||
if (me.frm.doc.currency == company_currency) {
|
||||
me.frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
if (me.frm.doc.price_list_currency == company_currency) {
|
||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||
}
|
||||
|
||||
me.frm.script_manager.trigger("currency");
|
||||
}
|
||||
|
||||
var set_terms = function() {
|
||||
if (frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && !me.frm.doc.tc_name) {
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
var selling_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"];
|
||||
var company_terms_fieldname = selling_doctypes.includes(me.frm.doc.doctype) ? "default_selling_terms" : "default_buying_terms";
|
||||
if (company_doc && company_doc[company_terms_fieldname]) {
|
||||
me.frm.set_value("tc_name", company_doc[company_terms_fieldname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var set_letter_head = function() {
|
||||
if(me.frm.fields_dict.letter_head) {
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
if (company_doc && company_doc.default_letter_head) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var set_party_account = function(set_pricing) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
|
||||
if(me.frm.doc.doctype=="Sales Invoice") {
|
||||
@@ -1218,16 +1253,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
is_a_mapped_document(item) {
|
||||
const mapped_item_field_map = {
|
||||
"Delivery Note Item": ["si_detail", "so_detail", "dn_detail"],
|
||||
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
|
||||
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
|
||||
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
|
||||
"Delivery Note": ["si_detail", "so_detail", "dn_detail"],
|
||||
"Sales Invoice": ["dn_detail", "so_detail", "sales_invoice_item"],
|
||||
"Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
|
||||
"Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"],
|
||||
"Sales Order": ["prevdoc_docname", "quotation_item"],
|
||||
};
|
||||
const mappped_fields = mapped_item_field_map[item.doctype] || [];
|
||||
const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || [];
|
||||
|
||||
return mappped_fields
|
||||
.map((field) => item[field])
|
||||
.filter(Boolean).length > 0;
|
||||
if (item) {
|
||||
return mappped_fields
|
||||
.map((field) => item[field])
|
||||
.filter(Boolean).length > 0;
|
||||
} else if (this.frm.doc?.items) {
|
||||
let first_row = this.frm.doc.items[0];
|
||||
let mapped_rows = mappped_fields.filter(d => first_row[d])
|
||||
|
||||
return mapped_rows?.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
batch_no(doc, cdt, cdn) {
|
||||
@@ -1378,12 +1421,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let show = cint(this.frm.doc.discount_amount) ||
|
||||
((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
|
||||
|
||||
if(frappe.meta.get_docfield(cur_frm.doctype, "net_total"))
|
||||
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "net_total")) {
|
||||
this.frm.toggle_display("net_total", show);
|
||||
}
|
||||
|
||||
if(frappe.meta.get_docfield(cur_frm.doctype, "base_net_total"))
|
||||
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "base_net_total")) {
|
||||
this.frm.toggle_display("base_net_total", (show && (me.frm.doc.currency != company_currency)));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
change_grid_labels(company_currency) {
|
||||
@@ -1621,7 +1665,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
|
||||
"conversion_factor": me.frm.doc.conversion_factor,
|
||||
"pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
|
||||
"coupon_code": me.frm.doc.coupon_code
|
||||
"coupon_code": me.frm.doc.coupon_code,
|
||||
"is_internal_supplier": me.frm.doc.is_internal_supplier,
|
||||
"is_internal_customer": me.frm.doc.is_internal_customer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1949,6 +1995,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let item_rates = {};
|
||||
let item_tax_templates = {};
|
||||
|
||||
if (me.frm.doc.is_return && me.frm.doc.return_against) return;
|
||||
|
||||
$.each(this.frm.doc.items || [], function(i, item) {
|
||||
if (item.item_code) {
|
||||
// Use combination of name and item code in case same item is added multiple times
|
||||
|
||||
@@ -116,14 +116,17 @@ erpnext.financial_statements = {
|
||||
erpnext.financial_statements.filters = get_filters();
|
||||
|
||||
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
|
||||
var filters = report.get_values();
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
if (!filters.period_start_date || !filters.period_end_date) {
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (report.page) {
|
||||
const views_menu = report.page.add_custom_button_group(__("Financial Statements"));
|
||||
|
||||
@@ -368,8 +368,28 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
];
|
||||
}
|
||||
|
||||
get_batch_qty(batch_no, callback) {
|
||||
let warehouse = this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse;
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
|
||||
args: {
|
||||
batch_no: batch_no,
|
||||
warehouse: warehouse,
|
||||
item_code: this.item.item_code,
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
posting_time: this.frm.doc.posting_time,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
callback(flt(r.message));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get_dialog_table_fields() {
|
||||
let fields = [];
|
||||
let me = this;
|
||||
|
||||
if (this.item.has_serial_no) {
|
||||
fields.push({
|
||||
@@ -395,6 +415,15 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
in_list_view: 1,
|
||||
change() {
|
||||
let doc = this.doc;
|
||||
if (!doc.qty && me.item.type_of_transaction === "Outward") {
|
||||
me.get_batch_qty(doc.batch_no, (qty) => {
|
||||
doc.qty = qty;
|
||||
this.grid.set_value("qty", qty, doc);
|
||||
});
|
||||
}
|
||||
},
|
||||
get_query: () => {
|
||||
let is_inward = false;
|
||||
if (
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"gender",
|
||||
"lead_name",
|
||||
"opportunity_name",
|
||||
"prospect_name",
|
||||
"account_manager",
|
||||
"image",
|
||||
"defaults_tab",
|
||||
@@ -307,13 +308,15 @@
|
||||
"fetch_from": "customer_primary_contact.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Mobile No"
|
||||
"label": "Mobile No",
|
||||
"options": "Mobile"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer_primary_contact.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Email Id"
|
||||
"label": "Email Id",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
@@ -570,6 +573,14 @@
|
||||
{
|
||||
"fieldname": "column_break_nwor",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Prospect",
|
||||
"no_copy": 1,
|
||||
"options": "Prospect",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
@@ -583,7 +594,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:03:20.716169",
|
||||
"modified": "2024-06-17 03:24:59.612974",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
@@ -668,4 +679,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user