mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-25 20:08:34 +00:00
Compare commits
346 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a8af5ba6 | ||
|
|
45eed5fd81 | ||
|
|
435c852b54 | ||
|
|
bba0a6d950 | ||
|
|
7ad770a83a | ||
|
|
5396b141dd | ||
|
|
4ffb161ec8 | ||
|
|
b37fc6676e | ||
|
|
965fcf8b71 | ||
|
|
61ac18069b | ||
|
|
3ae5de7b11 | ||
|
|
2420122f0e | ||
|
|
c5826f4132 | ||
|
|
09ba9808de | ||
|
|
71248ff40b | ||
|
|
bfe0443530 | ||
|
|
eda4462e5f | ||
|
|
0ba965aae6 | ||
|
|
07eb5c714a | ||
|
|
245bc7d2fb | ||
|
|
773e56808a | ||
|
|
cb17dbd616 | ||
|
|
943e2c00bc | ||
|
|
0866c03e20 | ||
|
|
4078e252c2 | ||
|
|
caa03efbe1 | ||
|
|
4b2ac626c5 | ||
|
|
ae733cd7ad | ||
|
|
cec539629b | ||
|
|
6b286ae03d | ||
|
|
e30b2f1d04 | ||
|
|
d59d30c50a | ||
|
|
946c3554b1 | ||
|
|
bb1a655efb | ||
|
|
dada7c4aa8 | ||
|
|
dabf2c7027 | ||
|
|
76760c2ee3 | ||
|
|
b3bcfd5a64 | ||
|
|
377435fa7d | ||
|
|
82bcb62b21 | ||
|
|
7032197f97 | ||
|
|
ef801d6bb4 | ||
|
|
bb8e5adadc | ||
|
|
397f39e271 | ||
|
|
2fffc9448b | ||
|
|
5a1c61f4d9 | ||
|
|
266658cda1 | ||
|
|
dcf4ac66bb | ||
|
|
813f0fa8c9 | ||
|
|
58d9694173 | ||
|
|
1bf608f835 | ||
|
|
5eb4baccd6 | ||
|
|
ae0acd9ba1 | ||
|
|
97a4a5f1cc | ||
|
|
3bafa360b2 | ||
|
|
de571c9266 | ||
|
|
e7e67902e9 | ||
|
|
800e38453b | ||
|
|
38939005ca | ||
|
|
bce77b6117 | ||
|
|
5a3c027432 | ||
|
|
ee7602e752 | ||
|
|
656b1bcede | ||
|
|
e2a1a7a36d | ||
|
|
283b118773 | ||
|
|
fd48fb49b9 | ||
|
|
a57a9d7548 | ||
|
|
49dca6016c | ||
|
|
0422117003 | ||
|
|
6d32089d9e | ||
|
|
c3626d67ca | ||
|
|
2888b68334 | ||
|
|
f7f10a5930 | ||
|
|
3fcda3a414 | ||
|
|
f06e20d993 | ||
|
|
2fc3e30f9f | ||
|
|
41c7890a6d | ||
|
|
d9f1b0be77 | ||
|
|
fd7f6fd7f3 | ||
|
|
33d48c5575 | ||
|
|
f7332258e7 | ||
|
|
d782c52b76 | ||
|
|
be4fe38998 | ||
|
|
ce7101f555 | ||
|
|
56dd37c9e6 | ||
|
|
460291990a | ||
|
|
80976ae466 | ||
|
|
9a28d4ef5a | ||
|
|
c432506912 | ||
|
|
852c200ee0 | ||
|
|
a66854d16d | ||
|
|
72bb3e26cd | ||
|
|
128c2bf8b9 | ||
|
|
7aa46af0c3 | ||
|
|
631363632b | ||
|
|
3e3c489178 | ||
|
|
79c3bc9bcd | ||
|
|
c6682f130c | ||
|
|
c425944bdf | ||
|
|
0935b181bf | ||
|
|
bc50b94a87 | ||
|
|
02f16715e0 | ||
|
|
f0ab9b6e76 | ||
|
|
d2dc0a4c9a | ||
|
|
e8b46d9815 | ||
|
|
d6333c1562 | ||
|
|
53e18a9beb | ||
|
|
0576752d3b | ||
|
|
e3a2b310d8 | ||
|
|
97a6610c0c | ||
|
|
9ec30319e4 | ||
|
|
e511503597 | ||
|
|
ca79f6478a | ||
|
|
9ece276e76 | ||
|
|
4fe968961a | ||
|
|
f2a77d178d | ||
|
|
dbbba7262b | ||
|
|
85d18fa7a4 | ||
|
|
11a6c5b394 | ||
|
|
e12871b408 | ||
|
|
a75c2cb4a0 | ||
|
|
c18ed0862e | ||
|
|
8bddefb18b | ||
|
|
55448b7437 | ||
|
|
ac02af476a | ||
|
|
0a1b532f69 | ||
|
|
383648fb59 | ||
|
|
13239a9dee | ||
|
|
2c13b2cc22 | ||
|
|
4da44e2c3f | ||
|
|
a503460bd5 | ||
|
|
78a3701f4c | ||
|
|
eedb93b2d6 | ||
|
|
ea3042bc74 | ||
|
|
b977366dcd | ||
|
|
0a4198718b | ||
|
|
b340d7c6bb | ||
|
|
0bd3c3b566 | ||
|
|
8bdbb24d73 | ||
|
|
442e46c80f | ||
|
|
83a72b8b30 | ||
|
|
a50836ab07 | ||
|
|
e9c4762309 | ||
|
|
7cd9de211f | ||
|
|
da37fea583 | ||
|
|
a912b78bb8 | ||
|
|
8ba5ef683f | ||
|
|
bde19ab010 | ||
|
|
303dac262c | ||
|
|
3e4bd3040a | ||
|
|
ae490804f9 | ||
|
|
e57f3fe727 | ||
|
|
68099a9b5c | ||
|
|
36e2cf49f3 | ||
|
|
ed42d54989 | ||
|
|
b740846b68 | ||
|
|
5a42ff0c3c | ||
|
|
37ca45ea49 | ||
|
|
19dca36dec | ||
|
|
a6f5b88f9b | ||
|
|
6c9681ba4c | ||
|
|
9519773c5c | ||
|
|
eea8cb5885 | ||
|
|
b034f3d3db | ||
|
|
cf851cfa56 | ||
|
|
7f69556c45 | ||
|
|
4c094c3d86 | ||
|
|
ad92c021f7 | ||
|
|
aedab5c210 | ||
|
|
635a421807 | ||
|
|
816cbdea0d | ||
|
|
66a4823640 | ||
|
|
5b7ee0af66 | ||
|
|
436cb8dbfc | ||
|
|
ad2b8d2455 | ||
|
|
2a1a7fd1f6 | ||
|
|
d0a8639a2d | ||
|
|
9cfd704eef | ||
|
|
29b35494da | ||
|
|
292f17b1b0 | ||
|
|
740dd878e9 | ||
|
|
a8f05cadea | ||
|
|
9c2b4f611d | ||
|
|
350282f0cc | ||
|
|
3d525addbe | ||
|
|
3cf10fafdf | ||
|
|
bda7220b70 | ||
|
|
c62c30f7a3 | ||
|
|
d91cf01970 | ||
|
|
da0776a38c | ||
|
|
9efdcf208a | ||
|
|
fb525fec80 | ||
|
|
f31bb6ad4a | ||
|
|
99f3a7e4cf | ||
|
|
f965b352c8 | ||
|
|
4e910d8a69 | ||
|
|
d93ba985e4 | ||
|
|
195f020636 | ||
|
|
cacca812ed | ||
|
|
4347efdf2c | ||
|
|
68338abe07 | ||
|
|
6ba8725940 | ||
|
|
d0f96c48cf | ||
|
|
12be1dca7d | ||
|
|
2d2bbe5fc2 | ||
|
|
a0da47d7f4 | ||
|
|
c0116bcde5 | ||
|
|
ba17fdd072 | ||
|
|
f1ba825818 | ||
|
|
e42f8ffd5d | ||
|
|
c9955ddb35 | ||
|
|
28929df0e8 | ||
|
|
6027c25c48 | ||
|
|
32c5861919 | ||
|
|
e7297f2fc0 | ||
|
|
d14d09c286 | ||
|
|
5b5d0f56de | ||
|
|
a638dece6b | ||
|
|
d479930bef | ||
|
|
edd2814fc4 | ||
|
|
e5e3b8a6ae | ||
|
|
a1b1011555 | ||
|
|
c30d76ae68 | ||
|
|
2c9c5dcc85 | ||
|
|
e6083a57de | ||
|
|
0cbb7f8714 | ||
|
|
fa01eefd89 | ||
|
|
d9d48da505 | ||
|
|
e1c71e0b8f | ||
|
|
a61ad15998 | ||
|
|
c114f8445b | ||
|
|
c6127575f5 | ||
|
|
12a2e98751 | ||
|
|
1d7ba16caf | ||
|
|
3b12d60877 | ||
|
|
ac1f29d5cb | ||
|
|
f5481dc7d5 | ||
|
|
5f60c0e85e | ||
|
|
7b059a9221 | ||
|
|
78f8922a9c | ||
|
|
09c56abeb0 | ||
|
|
c6937c8375 | ||
|
|
99ad34d686 | ||
|
|
09ab2653d1 | ||
|
|
6413ce467f | ||
|
|
a5156f696c | ||
|
|
528a482240 | ||
|
|
d9d4b9b117 | ||
|
|
a4ad4e8279 | ||
|
|
d516110572 | ||
|
|
6d14cb0c6b | ||
|
|
d30dacce80 | ||
|
|
7e01ae9e4a | ||
|
|
42f94f1aba | ||
|
|
6a681557a9 | ||
|
|
7ab59aa094 | ||
|
|
c3b92075f0 | ||
|
|
4017936bef | ||
|
|
6cecae288c | ||
|
|
fec3a8b511 | ||
|
|
2c74491eb6 | ||
|
|
0e60750bd8 | ||
|
|
4dfc5671b0 | ||
|
|
003eb02e24 | ||
|
|
d5570f83d2 | ||
|
|
bd96868736 | ||
|
|
85f7196eb5 | ||
|
|
19e0d75c22 | ||
|
|
621558a30c | ||
|
|
547fbec55f | ||
|
|
d9bd42965a | ||
|
|
419df361a7 | ||
|
|
6ea4f1a03d | ||
|
|
3570ab8868 | ||
|
|
3ccd1b4a6c | ||
|
|
41c592a1a8 | ||
|
|
65ed4e5cf6 | ||
|
|
ad8c8cb0e8 | ||
|
|
7417432ddb | ||
|
|
7b6c7c3e27 | ||
|
|
df857f8177 | ||
|
|
43fc1ae4bf | ||
|
|
07c56221a5 | ||
|
|
273029d0f0 | ||
|
|
f48b4cda50 | ||
|
|
7226066772 | ||
|
|
afc4c856f8 | ||
|
|
22869b6f9d | ||
|
|
b268de4609 | ||
|
|
44b726c2e3 | ||
|
|
0c395725b7 | ||
|
|
7146c0385c | ||
|
|
e12564daa6 | ||
|
|
a09b73e65d | ||
|
|
654a55260d | ||
|
|
90dc22a57d | ||
|
|
e826e03f9a | ||
|
|
de4e62e308 | ||
|
|
468ec805f1 | ||
|
|
cd8c6eac7c | ||
|
|
90d6bb34dc | ||
|
|
1545904693 | ||
|
|
dadd4b1f95 | ||
|
|
0c89cd5524 | ||
|
|
934b5494f0 | ||
|
|
72a9b58b14 | ||
|
|
5cfd8d1930 | ||
|
|
74bf61e0c1 | ||
|
|
c4b135e1a2 | ||
|
|
853facad96 | ||
|
|
636e1ac1f1 | ||
|
|
df996b8fd3 | ||
|
|
5e6192249e | ||
|
|
398e8d00ec | ||
|
|
6be30bbd71 | ||
|
|
14de520ebb | ||
|
|
770d0e7f7f | ||
|
|
c351d6b1c0 | ||
|
|
a4b099e481 | ||
|
|
624ec19305 | ||
|
|
e1c3125efa | ||
|
|
d4195d31bf | ||
|
|
f349be0a00 | ||
|
|
d89ac99e76 | ||
|
|
9fce694936 | ||
|
|
b5d8477354 | ||
|
|
0e3d276348 | ||
|
|
3489b65f1a | ||
|
|
c8a52ec43c | ||
|
|
f9fd0ffbae | ||
|
|
69dc9e81d5 | ||
|
|
0a87fa5348 | ||
|
|
81e7e96cb6 | ||
|
|
f7770c3225 | ||
|
|
f61305aa45 | ||
|
|
113a6e079a | ||
|
|
c35426b9f9 | ||
|
|
83352b5a34 | ||
|
|
e54bb0da69 | ||
|
|
8d06ee3966 | ||
|
|
6b4101d202 | ||
|
|
386567a6ea | ||
|
|
d3440cf545 | ||
|
|
11544818f1 | ||
|
|
184fa889c3 | ||
|
|
650f874fbd |
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -60,7 +60,7 @@ body:
|
||||
description: Share exact version number of Frappe and ERPNext you are using.
|
||||
placeholder: |
|
||||
Frappe version -
|
||||
ERPNext Verion -
|
||||
ERPNext version -
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.95.0"
|
||||
__version__ = "15.99.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
},
|
||||
"account_number": "1151.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.002",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1152.000"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"Kas": {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
frappe.provide("erpnext.integrations");
|
||||
|
||||
frappe.ui.form.on("Bank", {
|
||||
onload: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
|
||||
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
|
||||
});
|
||||
});
|
||||
|
||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
||||
"bank_transaction_field",
|
||||
"options",
|
||||
options
|
||||
);
|
||||
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
|
||||
|
||||
if (grid) {
|
||||
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
|
||||
)
|
||||
);
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
|
||||
@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
is_company_account: function (frm) {
|
||||
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -98,6 +99,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -252,7 +254,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -52,31 +52,35 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_account()
|
||||
self.validate_is_company_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
def validate_is_company_account(self):
|
||||
if self.is_company_account:
|
||||
if not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is manadatory for company account"))
|
||||
if not self.account:
|
||||
frappe.throw(_("Company Account is mandatory"))
|
||||
|
||||
self.validate_account()
|
||||
|
||||
@deprecated
|
||||
def validate_iban(self):
|
||||
"""Kept for backward compatibility, will be removed in v16."""
|
||||
validate_iban(self.iban, throw=True)
|
||||
|
||||
def validate_account(self):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -136,6 +136,8 @@ class BankTransaction(Document):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
self.delink_payment_entry(payment_entry)
|
||||
|
||||
@@ -370,11 +372,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
|
||||
("unallocated_amount", "bank_account"),
|
||||
as_dict=True,
|
||||
)
|
||||
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
|
||||
|
||||
if bt.bank_account != gl_bank_account:
|
||||
if bt_bank_account != gl_bank_account:
|
||||
frappe.throw(
|
||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
bt_bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe import _, cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
@@ -33,24 +33,6 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
if not self.is_new():
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""",
|
||||
(self.name),
|
||||
)
|
||||
|
||||
if year_start_end_dates:
|
||||
if (
|
||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
@@ -66,28 +48,20 @@ class FiscalYear(Document):
|
||||
frappe.exceptions.InvalidDates,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(year_start_date)s between year_start_date and year_end_date)
|
||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
||||
) and name!=%(name)s""",
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
name = self.name or self.year
|
||||
|
||||
existing_fiscal_years = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where(
|
||||
(fy.year_start_date <= self.year_end_date)
|
||||
& (fy.year_end_date >= self.year_start_date)
|
||||
& (fy.name != name)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
@@ -110,37 +84,30 @@ class FiscalYear(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(existing.name),
|
||||
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_duplicate_fiscal_year(doc):
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
||||
(doc.name),
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_create_fiscal_year():
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
# Skipped auto-creating Short Year, as it has very rare use case.
|
||||
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
|
||||
follow_up_date = add_days(getdate(), days=3)
|
||||
fiscal_year = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
for d in fiscal_year:
|
||||
try:
|
||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||
|
||||
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
|
||||
new_fy = frappe.new_doc("Fiscal Year")
|
||||
new_fy.disabled = cint(current_fy.disabled)
|
||||
|
||||
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
||||
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
||||
@@ -148,6 +115,10 @@ def auto_create_fiscal_year():
|
||||
start_year = cstr(new_fy.year_start_date.year)
|
||||
end_year = cstr(new_fy.year_end_date.year)
|
||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||
|
||||
for row in current_fy.companies:
|
||||
new_fy.append("companies", {"company": row.company})
|
||||
|
||||
new_fy.auto_created = 1
|
||||
|
||||
new_fy.insert(ignore_permissions=True)
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 18:01:53.495929",
|
||||
"modified": "2026-02-20 23:02:26.193606",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
@@ -30,4 +31,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
company: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -172,7 +172,7 @@ class JournalEntry(AccountsController):
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||
queue_submission(self, "_submit")
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -198,7 +198,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
|
||||
"fieldname": "reference_due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
@@ -295,7 +295,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
|
||||
"Fees",
|
||||
"Full and Final Statement",
|
||||
"Payment Entry",
|
||||
"Bank Transaction",
|
||||
]
|
||||
user_remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
frm.refresh_fields();
|
||||
|
||||
const party_currency =
|
||||
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
|
||||
|
||||
var reference_grid = frm.fields_dict["references"].grid;
|
||||
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
|
||||
reference_grid.update_docfield_property(fieldname, "options", party_currency);
|
||||
});
|
||||
|
||||
reference_grid.refresh();
|
||||
},
|
||||
|
||||
show_general_ledger: function (frm) {
|
||||
@@ -506,12 +516,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("contact_email", "");
|
||||
frm.set_value("contact_person", "");
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
||||
frm.set_value("party", "");
|
||||
return;
|
||||
}
|
||||
|
||||
erpnext.utils.get_employee_contact_details(frm);
|
||||
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
@@ -1119,7 +1133,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount: flt(paid_amount),
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
@@ -1455,16 +1469,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
});
|
||||
frm.set_value("taxes", taxes);
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,6 +132,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
|
||||
@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
|
||||
amount_in_account_currency: DF.Currency
|
||||
company: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
project: DF.Link | None
|
||||
delinked: DF.Check
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
|
||||
@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
|
||||
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
|
||||
@@ -536,7 +536,7 @@ class PaymentRequest(Document):
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
@@ -548,6 +548,9 @@ def make_payment_request(**args):
|
||||
if args.dn and not isinstance(args.dn, str):
|
||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||
|
||||
frappe.has_permission("Payment Request", "create", throw=True)
|
||||
frappe.has_permission(args.dt, "read", args.dn, throw=True)
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -822,7 +825,7 @@ def get_print_format_list(ref_doctype):
|
||||
return {"print_format": print_format_list}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def resend_payment_email(docname):
|
||||
return frappe.get_doc("Payment Request", docname).send_email()
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"sec_warehouse",
|
||||
"set_warehouse",
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
@@ -574,7 +573,6 @@
|
||||
"label": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "set_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
@@ -588,15 +586,6 @@
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
"oldfieldname": "update_stock",
|
||||
"oldfieldtype": "Check",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
"fieldtype": "Data",
|
||||
@@ -1582,7 +1571,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified": "2026-02-22 04:18:50.691218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1627,6 +1616,7 @@
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -1635,4 +1625,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,6 @@ class POSInvoice(SalesInvoice):
|
||||
total_taxes_and_charges: DF.Currency
|
||||
update_billed_amount_in_delivery_note: DF.Check
|
||||
update_billed_amount_in_sales_order: DF.Check
|
||||
update_stock: DF.Check
|
||||
write_off_account: DF.Link | None
|
||||
write_off_amount: DF.Currency
|
||||
write_off_cost_center: DF.Link | None
|
||||
@@ -652,7 +651,6 @@ class POSInvoice(SalesInvoice):
|
||||
"tax_category",
|
||||
"ignore_pricing_rule",
|
||||
"company_address",
|
||||
"update_stock",
|
||||
):
|
||||
if not for_validate:
|
||||
self.set(fieldname, profile.get(fieldname))
|
||||
|
||||
@@ -838,6 +838,53 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 5)
|
||||
|
||||
def test_pos_batch_reservation_with_return_qty(self):
|
||||
"""
|
||||
Test POS Invoice reserved qty for batch without bundle with return invoices.
|
||||
"""
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_batch_item_with_batch,
|
||||
)
|
||||
|
||||
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="_Batch Item Reserve Return",
|
||||
qty=30,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
# POS Invoice for the batch without bundle
|
||||
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 4500},
|
||||
)
|
||||
pos_inv.items[0].batch_no = batch_no
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
# POS Invoice return
|
||||
pos_return = make_sales_return(pos_inv.name)
|
||||
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
|
||||
for batch in batches:
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 30)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
BatchNegativeStockError,
|
||||
@@ -1054,7 +1101,6 @@ def create_pos_invoice(**args):
|
||||
|
||||
pos_inv = frappe.new_doc("POS Invoice")
|
||||
pos_inv.update(args)
|
||||
pos_inv.update_stock = 1
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.update_stock = 1
|
||||
|
||||
if not sales_invoice.posting_date:
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
@@ -174,6 +175,7 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.update_stock = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
@@ -697,6 +699,7 @@ def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
& (SalesInvoice.docstatus == 1)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
@@ -297,7 +296,6 @@
|
||||
"options": "Print Format"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
@@ -312,14 +310,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Update Stock",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hide_unavailable_items",
|
||||
@@ -432,7 +422,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-04-14 15:58:20.497426",
|
||||
"modified": "2026-02-22 04:17:03.308876",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -61,7 +61,6 @@ class POSProfile(Document):
|
||||
tax_category: DF.Link | None
|
||||
taxes_and_charges: DF.Link | None
|
||||
tc_name: DF.Link | None
|
||||
update_stock: DF.Check
|
||||
validate_stock_on_save: DF.Check
|
||||
warehouse: DF.Link
|
||||
write_off_account: DF.Link
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Apply On",
|
||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
||||
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -657,7 +657,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified": "2026-02-17 12:24:07.553505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -719,4 +719,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
apply_discount_on_rate: DF.Check
|
||||
apply_multiple_pricing_rules: DF.Check
|
||||
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_recursion_over: DF.Float
|
||||
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||
brands: DF.Table[PricingRuleBrand]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@@ -91,7 +91,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-24 14:48:59.649168",
|
||||
"modified": "2026-02-17 12:17:13.073587",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Brand",
|
||||
@@ -107,4 +107,4 @@
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@@ -91,7 +91,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-24 14:48:59.649168",
|
||||
"modified": "2026-02-17 12:16:57.778471",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Item Group",
|
||||
@@ -107,4 +107,4 @@
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,8 +412,9 @@ def reconcile(doc: None | str = None) -> None:
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||
# reconcile
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
|
||||
|
||||
# If Payment Entry, update details only for newly linked references
|
||||
# This is for performance
|
||||
@@ -503,6 +504,37 @@ def reconcile(doc: None | str = None) -> None:
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
def check_multi_currency(pr_doc):
|
||||
GL = frappe.qb.DocType("GL Entry")
|
||||
Account = frappe.qb.DocType("Account")
|
||||
|
||||
def get_account_currency(voucher_type, voucher_no):
|
||||
currency = (
|
||||
frappe.qb.from_(GL)
|
||||
.join(Account)
|
||||
.on(GL.account == Account.name)
|
||||
.select(Account.account_currency)
|
||||
.where(
|
||||
(GL.voucher_type == voucher_type)
|
||||
& (GL.voucher_no == voucher_no)
|
||||
& (Account.account_type.isin(["Payable", "Receivable"]))
|
||||
)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
return currency[0].account_currency if currency else None
|
||||
|
||||
for allocation in pr_doc.allocation:
|
||||
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
|
||||
|
||||
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
|
||||
|
||||
if reference_currency != invoice_currency:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
|
||||
@@ -603,6 +603,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
@@ -1659,7 +1660,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:19:11.380664",
|
||||
"modified": "2026-02-05 20:45:16.964500",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -38,7 +38,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
@@ -1729,10 +1729,6 @@ class PurchaseInvoice(BuyingController):
|
||||
project_doc.db_update()
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
if getdate(self.bill_date) > getdate(self.posting_date):
|
||||
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
|
||||
|
||||
if self.bill_no:
|
||||
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
|
||||
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
|
||||
@@ -2083,6 +2079,19 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
def remove_items_with_zero_qty(target_parent):
|
||||
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||
|
||||
def set_missing_values(source_parent, target_parent):
|
||||
target_parent.run_method("set_missing_values")
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source_parent, target_parent)
|
||||
target_parent.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
@@ -2122,7 +2131,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"reset_value": not (args and args.get("merge_taxes")),
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
"Packing Slip",
|
||||
];
|
||||
|
||||
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
@@ -117,12 +118,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
return item.delivery_note ? true : false;
|
||||
});
|
||||
|
||||
if (!from_delivery_note && !is_delivered_by_supplier) {
|
||||
cur_frm.add_custom_button(
|
||||
__("Delivery"),
|
||||
cur_frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
if (!is_delivered_by_supplier) {
|
||||
const should_create_delivery_note = doc.items.some(
|
||||
(item) =>
|
||||
item.qty - item.delivered_qty > 0 &&
|
||||
!item.dn_detail &&
|
||||
!item.delivered_by_supplier
|
||||
);
|
||||
if (should_create_delivery_note) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
this.frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -701,6 +701,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -2199,7 +2200,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-09 14:48:59.472826",
|
||||
"modified": "2026-02-05 20:43:44.732805",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -854,9 +854,6 @@ class SalesInvoice(SellingController):
|
||||
if selling_price_list:
|
||||
self.set("selling_price_list", selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
self.update_stock = cint(pos.get("update_stock"))
|
||||
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
@@ -1097,7 +1094,9 @@ class SalesInvoice(SellingController):
|
||||
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
if self.doctype == "POS Invoice" or (
|
||||
self.doctype == "Sales Invoice" and cint(self.update_stock) == 1
|
||||
):
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
|
||||
make_packing_list(self)
|
||||
@@ -2211,7 +2210,9 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"cost_center": "cost_center",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1
|
||||
and not doc.dn_detail
|
||||
and doc.qty - doc.delivered_qty > 0,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
|
||||
@@ -4775,6 +4775,66 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(q[0][0], 1)
|
||||
|
||||
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
|
||||
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
|
||||
item_code = "_Test Item for Expiry Batch Zero Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"has_expiry_date": 1,
|
||||
"shelf_life_in_days": 2,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBATCH-EBZV.####",
|
||||
},
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
# fetch batch no from bundle
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
si = create_sales_invoice(
|
||||
posting_date=add_days(nowdate(), 3),
|
||||
item=item_code,
|
||||
qty=-10,
|
||||
rate=100,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=1,
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
si.items[0].batch_no = batch_no
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
si.reload()
|
||||
# check zero incoming rate in voucher
|
||||
self.assertEqual(si.items[0].incoming_rate, 0.0)
|
||||
|
||||
# chekc zero incoming rate in stock ledger
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Incoming Rate (Costing)",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -983,7 +984,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-12 16:33:55.503777",
|
||||
"modified": "2026-02-23 14:37:14.853941",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -993,4 +994,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@@ -85,7 +85,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-23 16:20:06.436979",
|
||||
"modified": "2026-02-16 20:46:34.592604",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
@@ -95,4 +95,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,12 +804,19 @@ def validate_against_pcv(is_opening, posting_date, company):
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||
)
|
||||
# Local import so you don't have to touch file-level imports
|
||||
from frappe.query_builder.functions import Max
|
||||
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
|
||||
last_pcv_date = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(Max(pcv.period_end_date))
|
||||
.where((pcv.docstatus == 1) & (pcv.company == company))
|
||||
).run(pluck=True)[0]
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date))
|
||||
message = _("Books have been closed till the period ending on {0}.").format(formatdate(last_pcv_date))
|
||||
message += "</br >"
|
||||
message += _("You cannot create/amend any accounting entries till this date.")
|
||||
frappe.throw(message, title=_("Period Closed"))
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
|
||||
|
||||
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
|
||||
|
||||
<p>{{ _("Fiscal Year Details") }}</p>
|
||||
|
||||
<table style="margin-bottom: 1rem; width: 70%">
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
|
||||
<td>{{ doc.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
|
||||
</tr>
|
||||
{% if doc.companies|length > 0 %}
|
||||
<tr>
|
||||
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
|
||||
{% if doc.companies|length < 2 %}
|
||||
{{ _("Company") }}
|
||||
{% else %}
|
||||
{{ _("Companies") }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ doc.companies[0].company }}</td>
|
||||
</tr>
|
||||
{% for idx in range(1, doc.companies|length) %}
|
||||
<tr>
|
||||
<td>{{ doc.companies[idx].company }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if doc.disabled %}
|
||||
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.auto_created",
|
||||
"condition": "doc.auto_created == 1",
|
||||
"creation": "2018-04-25 14:19:05.440361",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
@@ -11,19 +11,22 @@
|
||||
"event": "New",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"message": "<h3>{{_(\"Fiscal Year\")}}</h3>\n\n<p>{{ _(\"New fiscal year created :- \") }} {{ doc.name }}</p>",
|
||||
"modified": "2018-04-25 14:30:38.588534",
|
||||
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
|
||||
"message_type": "HTML",
|
||||
"modified": "2026-02-21 15:59:07.775679",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Notification for new fiscal year",
|
||||
"owner": "Administrator",
|
||||
"recipients": [
|
||||
{
|
||||
"email_by_role": "Accounts User"
|
||||
"receiver_by_role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"email_by_role": "Accounts Manager"
|
||||
"receiver_by_role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"subject": "Notification for new fiscal year {{ doc.name }}"
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "{{ _(\"New Fiscal Year {0} - Review Required\").format(doc.name) }}"
|
||||
}
|
||||
@@ -7,18 +7,16 @@ from frappe import _, msgprint, qb, scrub
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Abs, Count, Date, Sum
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_years,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_last_day,
|
||||
get_timestamp,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
@@ -302,19 +300,9 @@ def complete_contact_details(party_details):
|
||||
contact_details = frappe._dict()
|
||||
|
||||
if party_details.party_type == "Employee":
|
||||
contact_details = frappe.db.get_value(
|
||||
"Employee",
|
||||
party_details.party,
|
||||
[
|
||||
"employee_name as contact_display",
|
||||
"prefered_email as contact_email",
|
||||
"cell_number as contact_mobile",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
|
||||
|
||||
contact_details = get_employee_contact(party_details.party)
|
||||
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||
elif party_details.contact_person:
|
||||
contact_details = frappe.db.get_value(
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<table>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,15 +5,16 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder import Case, Order
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cint, flt, formatdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
@@ -176,7 +177,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
column_names = get_column_names()
|
||||
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
columns[0]["fieldname"] = "sales_invoice"
|
||||
columns[0]["options"] = "Item"
|
||||
columns[0]["width"] = 300
|
||||
# removing Item Code and Item Name columns
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
total_gross_profit = flt(
|
||||
total_base_amount + abs(total_buying_amount)
|
||||
if total_buying_amount < 0
|
||||
else total_base_amount - total_buying_amount,
|
||||
)
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_%": flt(
|
||||
(total_gross_profit / total_base_amount) * 100.0,
|
||||
(total_gross_profit / abs(total_base_amount)) * 100.0,
|
||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||
)
|
||||
if total_base_amount
|
||||
@@ -248,9 +255,13 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
total_gross_profit = flt(
|
||||
total_base_amount + abs(total_buying_amount)
|
||||
if total_buying_amount < 0
|
||||
else total_base_amount - total_buying_amount,
|
||||
)
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
gross_profit_percent = (total_gross_profit / abs(total_base_amount) * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
row.gross_profit = flt(
|
||||
row.base_amount + abs(row.buying_amount)
|
||||
if row.buying_amount < 0
|
||||
else row.base_amount - row.buying_amount,
|
||||
self.currency_precision,
|
||||
)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
(row.gross_profit / abs(row.base_amount)) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
@@ -673,9 +689,14 @@ class GrossProfitGenerator:
|
||||
return new_row
|
||||
|
||||
def set_average_gross_profit(self, new_row):
|
||||
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
|
||||
new_row.gross_profit = flt(
|
||||
new_row.base_amount + abs(new_row.buying_amount)
|
||||
if new_row.buying_amount < 0
|
||||
else new_row.base_amount - new_row.buying_amount,
|
||||
self.currency_precision,
|
||||
)
|
||||
new_row.gross_profit_percent = (
|
||||
flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision)
|
||||
flt(((new_row.gross_profit / abs(new_row.base_amount)) * 100.0), self.currency_precision)
|
||||
if new_row.base_amount
|
||||
else 0
|
||||
)
|
||||
@@ -851,129 +872,173 @@ class GrossProfitGenerator:
|
||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||
|
||||
def load_invoice_items(self):
|
||||
conditions = ""
|
||||
if self.filters.company:
|
||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
||||
if self.filters.from_date:
|
||||
conditions += " and posting_date >= %(from_date)s"
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
self.si_list = []
|
||||
|
||||
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||
base_query = self.prepare_invoice_query()
|
||||
|
||||
if self.filters.include_returned_invoices:
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
invoice_query = base_query.where(
|
||||
(SalesInvoice.is_return == 0)
|
||||
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
|
||||
)
|
||||
else:
|
||||
conditions += " and is_return = 0"
|
||||
invoice_query = base_query.where(SalesInvoice.is_return == 0)
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
|
||||
self.si_list += invoice_query.run(as_dict=True)
|
||||
self.prepare_vouchers_to_ignore()
|
||||
|
||||
if self.filters.sales_person:
|
||||
conditions += """
|
||||
and exists(select 1
|
||||
from `tabSales Team` st
|
||||
where st.parent = `tabSales Invoice`.name
|
||||
and st.sales_person = %(sales_person)s)
|
||||
"""
|
||||
ret_invoice_query = base_query.where(
|
||||
(SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
|
||||
)
|
||||
if self.vouchers_to_ignore:
|
||||
ret_invoice_query = ret_invoice_query.where(
|
||||
SalesInvoice.return_against.notin(self.vouchers_to_ignore)
|
||||
)
|
||||
|
||||
self.si_list += ret_invoice_query.run(as_dict=True)
|
||||
|
||||
def prepare_invoice_query(self):
|
||||
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
SalesTeam = frappe.qb.DocType("Sales Team")
|
||||
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.join(SalesInvoiceItem)
|
||||
.on(SalesInvoiceItem.parent == SalesInvoice.name)
|
||||
.join(Item)
|
||||
.on(Item.name == SalesInvoiceItem.item_code)
|
||||
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
|
||||
)
|
||||
|
||||
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item)
|
||||
|
||||
query = query.select(
|
||||
SalesInvoiceItem.parenttype,
|
||||
SalesInvoiceItem.parent,
|
||||
SalesInvoice.posting_date,
|
||||
SalesInvoice.posting_time,
|
||||
SalesInvoice.project,
|
||||
SalesInvoice.update_stock,
|
||||
SalesInvoice.customer,
|
||||
SalesInvoice.customer_group,
|
||||
SalesInvoice.customer_name,
|
||||
SalesInvoice.territory,
|
||||
SalesInvoiceItem.item_code,
|
||||
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
|
||||
SalesInvoiceItem.item_name,
|
||||
SalesInvoiceItem.description,
|
||||
SalesInvoiceItem.warehouse,
|
||||
SalesInvoiceItem.item_group,
|
||||
SalesInvoiceItem.brand,
|
||||
SalesInvoiceItem.so_detail,
|
||||
SalesInvoiceItem.sales_order,
|
||||
SalesInvoiceItem.dn_detail,
|
||||
SalesInvoiceItem.delivery_note,
|
||||
SalesInvoiceItem.stock_qty.as_("qty"),
|
||||
SalesInvoiceItem.base_net_rate,
|
||||
SalesInvoiceItem.base_net_amount,
|
||||
SalesInvoiceItem.name.as_("item_row"),
|
||||
SalesInvoice.is_return,
|
||||
SalesInvoiceItem.cost_center,
|
||||
SalesInvoiceItem.serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
sales_person_cols = """, sales.sales_person,
|
||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
||||
sales.incentives
|
||||
"""
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
else:
|
||||
sales_person_cols = ""
|
||||
sales_team_table = ""
|
||||
query = query.select(
|
||||
SalesTeam.sales_person,
|
||||
(SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
|
||||
"allocated_amount"
|
||||
),
|
||||
SalesTeam.incentives,
|
||||
)
|
||||
|
||||
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
|
||||
|
||||
if self.filters.group_by == "Payment Term":
|
||||
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||
'{}',
|
||||
coalesce(schedule.payment_term, '{}')) as payment_term,
|
||||
schedule.invoice_portion,
|
||||
schedule.payment_amount """.format(_("Sales Return"), _("No Terms"))
|
||||
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||
`tabSales Invoice`.is_return = 0 """
|
||||
else:
|
||||
payment_term_cols = ""
|
||||
payment_term_table = ""
|
||||
query = query.select(
|
||||
Case()
|
||||
.when(SalesInvoice.is_return == 1, _("Sales Return"))
|
||||
.else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
|
||||
.as_("payment_term"),
|
||||
PaymentSchedule.invoice_portion,
|
||||
PaymentSchedule.payment_amount,
|
||||
)
|
||||
|
||||
if self.filters.get("sales_invoice"):
|
||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||
query = query.left_join(PaymentSchedule).on(
|
||||
(PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
|
||||
)
|
||||
|
||||
if self.filters.get("item_code"):
|
||||
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
|
||||
query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
|
||||
SalesInvoice.posting_time, order=Order.desc
|
||||
)
|
||||
|
||||
if self.filters.get("cost_center"):
|
||||
return query
|
||||
|
||||
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item):
|
||||
if self.filters.company:
|
||||
query = query.where(SalesInvoice.company == self.filters.company)
|
||||
|
||||
if self.filters.from_date:
|
||||
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
|
||||
|
||||
if self.filters.to_date:
|
||||
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
|
||||
|
||||
if self.filters.item_group:
|
||||
query = query.where(get_item_group_condition(self.filters.item_group, Item))
|
||||
|
||||
if self.filters.sales_person:
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(SalesTeam)
|
||||
.select(1)
|
||||
.where(
|
||||
(SalesTeam.parent == SalesInvoice.name)
|
||||
& (SalesTeam.sales_person == self.filters.sales_person)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.sales_invoice:
|
||||
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
|
||||
|
||||
if self.filters.item_code:
|
||||
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
|
||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s"
|
||||
query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
|
||||
|
||||
if self.filters.get("project"):
|
||||
if self.filters.project:
|
||||
self.filters.project = frappe.parse_json(self.filters.get("project"))
|
||||
conditions += " and `tabSales Invoice Item`.project in %(project)s"
|
||||
query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if self.filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||
)
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
else:
|
||||
conditions += (
|
||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
||||
)
|
||||
for dim in get_accounting_dimensions(as_list=False) or []:
|
||||
if self.filters.get(dim.fieldname):
|
||||
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
|
||||
self.filters[dim.fieldname] = get_dimension_with_children(
|
||||
dim.document_type, self.filters.get(dim.fieldname)
|
||||
)
|
||||
query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
if self.filters.warehouse:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
SalesInvoiceItem.warehouse.isin(
|
||||
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
|
||||
)
|
||||
)
|
||||
if warehouse_details:
|
||||
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
|
||||
|
||||
self.si_list = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
|
||||
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
|
||||
{sales_person_cols}
|
||||
{payment_term_cols}
|
||||
from
|
||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
|
||||
{sales_team_table}
|
||||
{payment_term_table}
|
||||
where
|
||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||
order by
|
||||
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format(
|
||||
conditions=conditions,
|
||||
sales_person_cols=sales_person_cols,
|
||||
sales_team_table=sales_team_table,
|
||||
payment_term_cols=payment_term_cols,
|
||||
payment_term_table=payment_term_table,
|
||||
match_cond=get_match_cond("Sales Invoice"),
|
||||
),
|
||||
self.filters,
|
||||
as_dict=1,
|
||||
)
|
||||
return query
|
||||
|
||||
def prepare_vouchers_to_ignore(self):
|
||||
self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
|
||||
|
||||
def get_delivery_notes(self):
|
||||
self.delivery_notes = frappe._dict({})
|
||||
|
||||
@@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
@@ -465,7 +466,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit_%": -100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
@@ -642,21 +643,24 @@ class TestGrossProfit(FrappeTestCase):
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
sales_inv_date = month_start_date
|
||||
return_inv_date = add_days(month_end_date, 1)
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.posting_date = return_inv_date
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
||||
company=self.company, from_date=month_start_date, to_date=month_start_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
@@ -668,7 +672,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
filters.update({"to_date": return_inv_date})
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
@@ -677,3 +681,63 @@ class TestGrossProfit(FrappeTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
# apply filters only on returned period
|
||||
filters.update({"from_date": return_inv_date, "to_date": return_inv_date})
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, -100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, -100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||
|
||||
def test_sales_person_wise_gross_profit(self):
|
||||
sales_person = make_sales_person("_Test Sales Person")
|
||||
|
||||
posting_date = get_first_day(nowdate())
|
||||
qty = 10
|
||||
rate = 100
|
||||
|
||||
sinv = self.create_sales_invoice(qty=qty, rate=rate, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = posting_date
|
||||
sinv.append(
|
||||
"sales_team",
|
||||
{
|
||||
"sales_person": sales_person.name,
|
||||
"allocated_percentage": 100,
|
||||
"allocated_amount": 1000.0,
|
||||
"commission_rate": 5,
|
||||
"incentives": 5,
|
||||
},
|
||||
)
|
||||
sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=posting_date, to_date=posting_date, group_by="Sales Person"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total[5], 1000.0)
|
||||
self.assertEqual(total[6], 0.0)
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
|
||||
def make_sales_person(sales_person_name="_Test Sales Person"):
|
||||
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
|
||||
sales_person_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Person",
|
||||
"is_group": 0,
|
||||
"parent_sales_person": "Sales Team",
|
||||
"sales_person_name": sales_person_name,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
else:
|
||||
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
|
||||
|
||||
return sales_person_doc
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -33,11 +34,19 @@ def execute(filters=None):
|
||||
|
||||
def get_accounts_data(based_on, company):
|
||||
if based_on == "Cost Center":
|
||||
return frappe.db.sql(
|
||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
||||
from `tabCost Center` where company=%s order by name""",
|
||||
company,
|
||||
as_dict=True,
|
||||
cc = qb.DocType("Cost Center")
|
||||
return (
|
||||
qb.from_(cc)
|
||||
.select(
|
||||
cc.name,
|
||||
cc.parent_cost_center.as_("parent_account"),
|
||||
cc.cost_center_name.as_("account_name"),
|
||||
cc.lft,
|
||||
cc.rgt,
|
||||
)
|
||||
.where(cc.company.eq(company))
|
||||
.orderby(cc.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
elif based_on == "Project":
|
||||
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
||||
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
|
||||
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
additional_conditions = []
|
||||
gl = qb.DocType("GL Entry")
|
||||
acc = qb.DocType("Account")
|
||||
|
||||
conditions = []
|
||||
conditions.append(gl.company.eq(company))
|
||||
conditions.append(gl[based_on].notnull())
|
||||
conditions.append(gl.is_cancelled.eq(0))
|
||||
|
||||
if from_date and to_date:
|
||||
conditions.append(gl.posting_date.between(from_date, to_date))
|
||||
elif from_date and not to_date:
|
||||
conditions.append(gl.posting_date.gte(from_date))
|
||||
elif not from_date and to_date:
|
||||
conditions.append(gl.posting_date.lte(to_date))
|
||||
|
||||
if ignore_closing_entries:
|
||||
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
|
||||
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append("and posting_date >= %(from_date)s")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select posting_date, {based_on} as based_on, debit, credit,
|
||||
is_opening, (select root_type from `tabAccount` where name = account) as type
|
||||
from `tabGL Entry` where company=%(company)s
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
{"company": company, "from_date": from_date, "to_date": to_date},
|
||||
as_dict=True,
|
||||
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.posting_date,
|
||||
gl[based_on].as_("based_on"),
|
||||
gl.debit,
|
||||
gl.credit,
|
||||
gl.is_opening,
|
||||
root_subquery.as_("type"),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(gl[based_on], gl.posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
entries = {}
|
||||
for name, details in gle_map.items():
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0
|
||||
tax_withholding_category, rate = None, None
|
||||
bill_no, bill_date = "", ""
|
||||
party = entry.party or entry.against
|
||||
@@ -83,6 +83,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
# back calculate total amount from rate and tax_amount
|
||||
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
|
||||
total_amount = grand_total = base_total
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
else:
|
||||
if tax_amount and rate:
|
||||
@@ -93,12 +94,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
|
||||
grand_total = values[1]
|
||||
base_total = values[2]
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
if voucher_type == "Purchase Invoice":
|
||||
base_tax_withholding_net_total = values[0]
|
||||
bill_no = values[3]
|
||||
bill_date = values[4]
|
||||
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
@@ -125,6 +130,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_tax_withholding_net_total": base_tax_withholding_net_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
@@ -252,14 +258,14 @@ def get_columns(filters):
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"label": _("Tax Withholding Net Total"),
|
||||
"fieldname": "base_tax_withholding_net_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
@@ -270,10 +276,16 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"label": _("Grand Total (Company Currency)"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Transaction Currency)"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
|
||||
{
|
||||
|
||||
@@ -35,9 +35,9 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
result = execute(filters)[1]
|
||||
expected_values = [
|
||||
# Check for JV totals using back calculation logic
|
||||
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||
[jv.name, "TCS", 0.075, -10000.0, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 706.67, 2550.0, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 693.33, 1000.0, 0.52, 1000.52],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -55,8 +55,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
|
||||
)[1]
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 5000, 1000, 6000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -107,8 +107,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
)[1]
|
||||
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -120,6 +120,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.rate,
|
||||
voucher.base_tax_withholding_net_total,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
voucher.grand_total,
|
||||
|
||||
@@ -128,7 +128,7 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
|
||||
@@ -112,6 +112,12 @@ frappe.query_reports["Trial Balance"] = {
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_group_accounts",
|
||||
label: __("Show Group Accounts"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
formatter: erpnext.financial_statements.formatter,
|
||||
tree: true,
|
||||
|
||||
@@ -83,7 +83,7 @@ def validate_filters(filters):
|
||||
|
||||
def get_data(filters):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
@@ -393,6 +393,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": company_currency,
|
||||
"is_group_account": d.is_group,
|
||||
"account_name": (
|
||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||
),
|
||||
@@ -409,6 +410,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
@@ -488,3 +493,12 @@ def prepare_opening_closing(row):
|
||||
row[valid_col] = 0.0
|
||||
else:
|
||||
row[reverse_col] = 0.0
|
||||
|
||||
|
||||
def hide_group_accounts(data):
|
||||
non_group_accounts_data = []
|
||||
for d in data:
|
||||
if not d.get("is_group_account"):
|
||||
d.update(indent=0)
|
||||
non_group_accounts_data.append(d)
|
||||
return non_group_accounts_data
|
||||
|
||||
@@ -454,7 +454,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
|
||||
dimensions_dict = frappe._dict()
|
||||
if entry and active_dimensions:
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
|
||||
if entry_dimension := entry.get(dim.fieldname):
|
||||
dimensions_dict[dim.fieldname] = entry_dimension
|
||||
return dimensions_dict
|
||||
|
||||
|
||||
@@ -1868,6 +1869,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
account=gle.account,
|
||||
party_type=gle.party_type,
|
||||
party=gle.party,
|
||||
project=gle.project,
|
||||
cost_center=gle.cost_center,
|
||||
finance_book=gle.finance_book,
|
||||
due_date=gle.due_date,
|
||||
|
||||
@@ -669,7 +669,10 @@ class Asset(AccountsController):
|
||||
def get_status(self):
|
||||
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
|
||||
if self.docstatus == 0:
|
||||
status = "Draft"
|
||||
if self.is_composite_asset:
|
||||
status = "Work In Progress"
|
||||
else:
|
||||
status = "Draft"
|
||||
elif self.docstatus == 1:
|
||||
status = "Submitted"
|
||||
|
||||
|
||||
@@ -611,14 +611,21 @@ class AssetCapitalization(StockController):
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
if self.docstatus == 2:
|
||||
asset_doc.gross_purchase_amount -= total_target_asset_value
|
||||
asset_doc.purchase_amount -= total_target_asset_value
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
||||
total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
|
||||
else:
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
asset_doc.set_status("Work In Progress")
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
|
||||
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
||||
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
|
||||
|
||||
asset_doc.db_set(
|
||||
{
|
||||
"gross_purchase_amount": gross_purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
"total_asset_cost": total_asset_cost,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, get_link_to_form
|
||||
from frappe.utils import cstr, get_datetime, get_link_to_form
|
||||
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
|
||||
@@ -34,6 +34,7 @@ class AssetMovement(Document):
|
||||
for d in self.assets:
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
self.validate_transaction_date(d)
|
||||
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
@@ -51,6 +52,18 @@ class AssetMovement(Document):
|
||||
else:
|
||||
self.validate_employee(d)
|
||||
|
||||
def validate_transaction_date(self, d):
|
||||
previous_movement_date = frappe.db.get_value(
|
||||
"Asset Movement",
|
||||
[["Asset Movement Item", "asset", "=", d.asset], ["docstatus", "=", 1]],
|
||||
"transaction_date",
|
||||
order_by="transaction_date desc",
|
||||
)
|
||||
if previous_movement_date and get_datetime(previous_movement_date) > get_datetime(
|
||||
self.transaction_date
|
||||
):
|
||||
frappe.throw(_("Transaction date can't be earlier than previous movement date"))
|
||||
|
||||
def validate_location_and_employee(self, d):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now
|
||||
from frappe.utils import add_days, now
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -147,6 +147,33 @@ class TestAssetMovement(unittest.TestCase):
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_movement_transaction_date(self):
|
||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||
asset.save().submit()
|
||||
|
||||
if not frappe.db.exists("Location", "Test Location 2"):
|
||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
||||
|
||||
asset_creation_date = frappe.db.get_value(
|
||||
"Asset Movement",
|
||||
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
||||
"transaction_date",
|
||||
)
|
||||
asset_movement = create_asset_movement(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
assets=[
|
||||
{
|
||||
"asset": asset.name,
|
||||
"source_location": "Test Location",
|
||||
"target_location": "Test Location 2",
|
||||
}
|
||||
],
|
||||
transaction_date=add_days(asset_creation_date, -1),
|
||||
do_not_save=True,
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, asset_movement.save)
|
||||
|
||||
|
||||
def create_asset_movement(**args):
|
||||
args = frappe._dict(args)
|
||||
@@ -165,9 +192,10 @@ def create_asset_movement(**args):
|
||||
"reference_name": args.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
movement.insert()
|
||||
movement.submit()
|
||||
if not args.do_not_save:
|
||||
movement.insert(ignore_if_duplicate=True)
|
||||
if not args.do_not_submit:
|
||||
movement.submit()
|
||||
|
||||
return movement
|
||||
|
||||
|
||||
@@ -196,6 +196,9 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
if self.is_subcontracted:
|
||||
self.status_updater[0]["source_field"] = "fg_item_qty"
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
|
||||
@@ -304,12 +304,17 @@ class RequestforQuotation(BuyingController):
|
||||
else:
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
|
||||
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
||||
subject_source = (
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation")
|
||||
)
|
||||
rendered_subject = frappe.render_template(subject_source, doc_args)
|
||||
if preview:
|
||||
return {
|
||||
"message": self.message_for_supplier,
|
||||
"subject": self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation"),
|
||||
"message": rendered_message,
|
||||
"subject": rendered_subject,
|
||||
}
|
||||
|
||||
attachments = []
|
||||
@@ -333,10 +338,8 @@ class RequestforQuotation(BuyingController):
|
||||
self.send_email(
|
||||
data,
|
||||
sender,
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
or _("Request for Quotation"),
|
||||
self.message_for_supplier,
|
||||
rendered_subject,
|
||||
rendered_message,
|
||||
attachments,
|
||||
)
|
||||
|
||||
|
||||
@@ -139,14 +139,6 @@ frappe.ui.form.on("Supplier", {
|
||||
// indicators
|
||||
erpnext.utils.set_party_dashboard_indicators(frm);
|
||||
}
|
||||
|
||||
frm.set_query("supplier_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
get_supplier_group_details: function (frm) {
|
||||
frappe.call({
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Supplier Group",
|
||||
"link_filters": "[[\"Supplier Group\",\"is_group\",\"=\",0]]",
|
||||
"oldfieldname": "supplier_type",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Supplier Group"
|
||||
@@ -485,7 +486,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:02:57.342931",
|
||||
"modified": "2026-02-06 12:58:01.398824",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
@@ -551,4 +552,4 @@
|
||||
"states": [],
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
|
||||
this.frm.add_custom_button(__("Update Items"), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
child_docname: "items",
|
||||
cannot_add_row: false,
|
||||
});
|
||||
});
|
||||
} else if (this.frm.doc.docstatus === 0) {
|
||||
erpnext.set_unit_price_items_note(this.frm);
|
||||
|
||||
|
||||
@@ -345,3 +345,15 @@ def set_expired_status():
|
||||
""",
|
||||
(nowdate()),
|
||||
)
|
||||
|
||||
|
||||
def get_purchased_items(supplier_quotation: str):
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
|
||||
fields=["supplier_quotation_item", "sum(qty)"],
|
||||
group_by="supplier_quotation_item",
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,15 +2,115 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_update_child_supplier_quotation_add_item(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": 5,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(sq.get("items")[0].qty, 5)
|
||||
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||
|
||||
def test_update_supplier_quotation_child_rate_disallow(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": 300,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
|
||||
def test_update_supplier_quotation_child_remove_item(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
po = make_purchase_order(sq.name)
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
po.get("items")[0].schedule_date = add_days(today(), 1)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
po.submit()
|
||||
sq.reload()
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
frappe.db.savepoint("before_cancel")
|
||||
# check if item having purchase order can be removed
|
||||
self.assertRaises(
|
||||
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
frappe.db.rollback(save_point="before_cancel")
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(len(sq.get("items")), 1)
|
||||
|
||||
def test_supplier_quotation_qty(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.items[0].qty = 0
|
||||
|
||||
@@ -2580,12 +2580,12 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def get_order_details(self):
|
||||
if self.doctype == "Sales Invoice":
|
||||
po_or_so = self.get("items")[0].get("sales_order")
|
||||
po_or_so = self.get("items") and self.get("items")[0].get("sales_order")
|
||||
po_or_so_doctype = "Sales Order"
|
||||
po_or_so_doctype_name = "sales_order"
|
||||
|
||||
else:
|
||||
po_or_so = self.get("items")[0].get("purchase_order")
|
||||
po_or_so = self.get("items") and self.get("items")[0].get("purchase_order")
|
||||
po_or_so_doctype = "Purchase Order"
|
||||
po_or_so_doctype_name = "purchase_order"
|
||||
|
||||
@@ -3678,7 +3678,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
|
||||
if child_doctype == "Purchase Order Item":
|
||||
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||
# Initialized value will update in parent validation
|
||||
child_item.base_rate = 1
|
||||
child_item.base_amount = 1
|
||||
@@ -3696,7 +3696,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
return child_item
|
||||
|
||||
|
||||
def validate_child_on_delete(row, parent):
|
||||
def validate_child_on_delete(row, parent, ordered_item=None):
|
||||
"""Check if partially transacted item (row) is being deleted."""
|
||||
if parent.doctype == "Sales Order":
|
||||
if flt(row.delivered_qty):
|
||||
@@ -3724,13 +3724,17 @@ def validate_child_on_delete(row, parent):
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
if parent.doctype in ["Purchase Order", "Sales Order"]:
|
||||
if flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype == "Quotation":
|
||||
if ordered_item.get(row.name):
|
||||
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||
|
||||
|
||||
def update_bin_on_delete(row, doctype):
|
||||
@@ -3756,7 +3760,7 @@ def update_bin_on_delete(row, doctype):
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data) -> bool:
|
||||
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@@ -3764,7 +3768,7 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
deleted_children.append(item)
|
||||
|
||||
for d in deleted_children:
|
||||
validate_child_on_delete(d, parent)
|
||||
validate_child_on_delete(d, parent, ordered_item)
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
@@ -3773,16 +3777,19 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
|
||||
# need to update ordered qty in Material Request first
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
|
||||
parent.update_prevdoc_status()
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
|
||||
|
||||
def check_doc_permissions(doc, perm_type="create"):
|
||||
try:
|
||||
doc.check_permission(perm_type)
|
||||
@@ -3821,7 +3828,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
)
|
||||
|
||||
def get_new_child_item(item_row):
|
||||
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
|
||||
child_doctype = parent_doctype + " Item"
|
||||
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
||||
|
||||
def is_allowed_zero_qty():
|
||||
@@ -3846,6 +3853,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
|
||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||
parent_doctype == "Supplier Quotation" and not purchased_items
|
||||
):
|
||||
return
|
||||
|
||||
qty_to_check = (
|
||||
ordered_items.get(child_item.name)
|
||||
if parent_doctype == "Quotation"
|
||||
else purchased_items.get(child_item.name)
|
||||
)
|
||||
if qty_to_check:
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
def should_update_supplied_items(doc) -> bool:
|
||||
"""Subcontracted PO can allow following changes *after submit*:
|
||||
|
||||
@@ -3888,7 +3910,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
@@ -3896,7 +3917,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
|
||||
if parent_doctype == "Quotation":
|
||||
ordered_items = get_ordered_items(parent.name)
|
||||
_removed_items = validate_and_delete_children(parent, data, ordered_items)
|
||||
elif parent_doctype == "Supplier Quotation":
|
||||
purchased_items = get_purchased_items(parent.name)
|
||||
_removed_items = validate_and_delete_children(parent, data, purchased_items)
|
||||
else:
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
|
||||
items_added_or_removed |= _removed_items
|
||||
|
||||
for d in data:
|
||||
@@ -3936,7 +3966,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
date_unchanged = (
|
||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
||||
(prev_date == getdate(new_date) if prev_date and new_date else False)
|
||||
if parent_doctype not in ["Quotation", "Supplier Quotation"]
|
||||
else None
|
||||
) # in case of delivery note etc
|
||||
if (
|
||||
rate_unchanged
|
||||
@@ -3949,6 +3981,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||
if not rate_unchanged:
|
||||
frappe.throw(_("Rates cannot be modified for quoted items"))
|
||||
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
@@ -3972,18 +4008,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
rate_unchanged = prev_rate == new_rate
|
||||
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
|
||||
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
)
|
||||
else:
|
||||
child_item.rate = row_rate
|
||||
else:
|
||||
child_item.rate = row_rate
|
||||
|
||||
@@ -4002,6 +4041,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor
|
||||
)
|
||||
|
||||
if child_item.get("total_weight") and child_item.get("weight_per_unit"):
|
||||
child_item.total_weight = flt(
|
||||
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
|
||||
child_item.precision("total_weight"),
|
||||
)
|
||||
|
||||
if d.get("delivery_date") and parent_doctype == "Sales Order":
|
||||
child_item.delivery_date = d.get("delivery_date")
|
||||
|
||||
@@ -4011,26 +4056,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if d.get("bom_no") and parent_doctype == "Sales Order":
|
||||
child_item.bom_no = d.get("bom_no")
|
||||
|
||||
if flt(child_item.price_list_rate):
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
if parent_doctype in ["Sales Order", "Purchase Order"]:
|
||||
if flt(child_item.price_list_rate):
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
@@ -4038,7 +4084,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
child_item.idx = len(parent.items) + 1
|
||||
child_item.insert()
|
||||
else:
|
||||
child_item.save()
|
||||
child_item.save(ignore_permissions=True)
|
||||
|
||||
parent.reload()
|
||||
parent.flags.ignore_validate_update_after_submit = True
|
||||
@@ -4052,13 +4098,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.doctype, parent.company, parent.base_grand_total
|
||||
)
|
||||
|
||||
parent.set_payment_schedule()
|
||||
if parent_doctype != "Supplier Quotation":
|
||||
parent.set_payment_schedule()
|
||||
if parent_doctype == "Purchase Order":
|
||||
parent.set_tax_withholding()
|
||||
parent.validate_minimum_order_qty()
|
||||
parent.validate_budget()
|
||||
if parent.is_against_so():
|
||||
parent.update_status_updater()
|
||||
else:
|
||||
elif parent_doctype == "Sales Order":
|
||||
parent.check_credit_limit()
|
||||
|
||||
# reset index of child table
|
||||
@@ -4091,7 +4139,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
elif parent_doctype == "Sales Order":
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
@@ -4103,9 +4151,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.reload()
|
||||
validate_workflow_conditions(parent)
|
||||
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
@@ -626,7 +626,9 @@ class BuyingController(SubcontractingController):
|
||||
or self.is_return
|
||||
or (self.is_internal_transfer() and self.docstatus == 2)
|
||||
else self.get_package_for_target_warehouse(
|
||||
d, type_of_transaction=type_of_transaction
|
||||
d,
|
||||
type_of_transaction=type_of_transaction,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -714,7 +716,22 @@ class BuyingController(SubcontractingController):
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
|
||||
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
|
||||
def get_package_for_target_warehouse(
|
||||
self, item, warehouse=None, type_of_transaction=None, via_landed_cost_voucher=None
|
||||
) -> str:
|
||||
if via_landed_cost_voucher and item.get("warehouse"):
|
||||
if sabb := frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_detail_no": item.name,
|
||||
"warehouse": item.get("warehouse"),
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
"name",
|
||||
):
|
||||
return sabb
|
||||
|
||||
if not item.serial_and_batch_bundle:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@@ -683,6 +683,29 @@ def get_rate_for_return(
|
||||
else:
|
||||
select_field = "abs(stock_value_difference / actual_qty)"
|
||||
|
||||
item_details = frappe.get_cached_value("Item", item_code, ["has_batch_no", "has_expiry_date"], as_dict=1)
|
||||
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
|
||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||
)
|
||||
|
||||
if (
|
||||
set_zero_rate_for_expired_batch
|
||||
and item_details.has_batch_no
|
||||
and item_details.has_expiry_date
|
||||
and not return_against
|
||||
and voucher_type in ["Sales Invoice", "Delivery Note"]
|
||||
):
|
||||
# set incoming_rate zero explicitly for standalone credit note with expired batch
|
||||
batch_no = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "batch_no")
|
||||
if batch_no and is_batch_expired(batch_no, sle.get("posting_date")):
|
||||
frappe.db.set_value(
|
||||
voucher_type + " Item",
|
||||
voucher_detail_no,
|
||||
"incoming_rate",
|
||||
0,
|
||||
)
|
||||
return 0
|
||||
|
||||
rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
|
||||
if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate")
|
||||
@@ -747,12 +770,34 @@ def get_filters(
|
||||
if reference_voucher_detail_no:
|
||||
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||
|
||||
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
|
||||
filters["warehouse"] = item_row.get("warehouse")
|
||||
warehouses = []
|
||||
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
|
||||
if reference_voucher_detail_no:
|
||||
warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
|
||||
|
||||
if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
|
||||
filters["warehouse"] = item_row.get("warehouse")
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def get_warehouses_for_return(voucher_type, name):
|
||||
warehouses = []
|
||||
warehouse_details = frappe.get_all(
|
||||
voucher_type + " Item",
|
||||
filters={"name": name, "docstatus": 1},
|
||||
fields=["warehouse", "rejected_warehouse"],
|
||||
)
|
||||
|
||||
for d in warehouse_details:
|
||||
if d.warehouse:
|
||||
warehouses.append(d.warehouse)
|
||||
if d.rejected_warehouse:
|
||||
warehouses.append(d.rejected_warehouse)
|
||||
|
||||
return warehouses
|
||||
|
||||
|
||||
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
get_serial_nos as get_serial_nos_from_serial_no,
|
||||
@@ -1152,3 +1197,17 @@ def get_available_serial_nos(serial_nos, warehouse):
|
||||
def get_payment_data(invoice):
|
||||
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
|
||||
return payment
|
||||
|
||||
|
||||
def is_batch_expired(batch_no, posting_date):
|
||||
"""
|
||||
To check whether the batch is expired or not based on the posting date.
|
||||
"""
|
||||
expiry_date = frappe.db.get_value("Batch", batch_no, "expiry_date")
|
||||
if not expiry_date:
|
||||
return
|
||||
|
||||
if getdate(posting_date) > getdate(expiry_date):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, nowtime
|
||||
|
||||
from erpnext.accounts.party import render_address
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, is_batch_expired
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
@@ -279,7 +279,7 @@ class SellingController(StockController):
|
||||
_(
|
||||
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
|
||||
Selling {3} should be atleast {4}.<br><br>Alternatively,
|
||||
you can disable selling price validation in {5} to bypass
|
||||
you can disable '{5}' in {6} to bypass
|
||||
this validation."""
|
||||
).format(
|
||||
idx,
|
||||
@@ -287,6 +287,7 @@ class SellingController(StockController):
|
||||
bold(ref_rate_field),
|
||||
bold("net rate"),
|
||||
bold(rate),
|
||||
bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
),
|
||||
title=_("Invalid Selling Price"),
|
||||
@@ -298,7 +299,6 @@ class SellingController(StockController):
|
||||
return
|
||||
|
||||
is_internal_customer = self.get("is_internal_customer")
|
||||
valuation_rate_map = {}
|
||||
|
||||
for item in self.items:
|
||||
if not item.item_code or item.is_free_item:
|
||||
@@ -308,7 +308,9 @@ class SellingController(StockController):
|
||||
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
|
||||
)
|
||||
|
||||
last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
|
||||
last_purchase_rate_in_sales_uom = flt(
|
||||
last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
|
||||
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
|
||||
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
|
||||
@@ -316,50 +318,16 @@ class SellingController(StockController):
|
||||
if is_internal_customer or not is_stock_item:
|
||||
continue
|
||||
|
||||
valuation_rate_map[(item.item_code, item.warehouse)] = None
|
||||
|
||||
if not valuation_rate_map:
|
||||
return
|
||||
|
||||
or_conditions = (
|
||||
f"""(item_code = {frappe.db.escape(valuation_rate[0])}
|
||||
and warehouse = {frappe.db.escape(valuation_rate[1])})"""
|
||||
for valuation_rate in valuation_rate_map
|
||||
)
|
||||
|
||||
valuation_rates = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
item_code, warehouse, valuation_rate
|
||||
from
|
||||
`tabBin`
|
||||
where
|
||||
({" or ".join(or_conditions)})
|
||||
and valuation_rate > 0
|
||||
""",
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
for rate in valuation_rates:
|
||||
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
||||
|
||||
for item in self.items:
|
||||
if not item.item_code or item.is_free_item:
|
||||
continue
|
||||
|
||||
last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
|
||||
|
||||
if not last_valuation_rate:
|
||||
continue
|
||||
|
||||
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
|
||||
|
||||
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
|
||||
if item.get("incoming_rate") and item.base_net_rate < (
|
||||
valuation_rate := flt(
|
||||
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
):
|
||||
throw_message(
|
||||
item.idx,
|
||||
item.item_name,
|
||||
last_valuation_rate_in_sales_uom,
|
||||
"valuation rate (Moving Average)",
|
||||
valuation_rate,
|
||||
"valuation rate",
|
||||
)
|
||||
|
||||
def get_item_list(self):
|
||||
@@ -515,22 +483,66 @@ class SellingController(StockController):
|
||||
sales_order.update_reserved_qty(so_item_rows)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
def reset_incoming_rate():
|
||||
old_item = next(
|
||||
(
|
||||
item
|
||||
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||
if item.name == d.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_item:
|
||||
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
|
||||
if (
|
||||
old_item.item_code != d.item_code
|
||||
or old_item.warehouse != d.warehouse
|
||||
or old_qty != qty
|
||||
or old_item.serial_no != d.serial_no
|
||||
or get_serial_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_serial_nos(d.serial_and_batch_bundle)
|
||||
or old_item.batch_no != d.batch_no
|
||||
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||
):
|
||||
d.incoming_rate = 0
|
||||
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
|
||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||
)
|
||||
|
||||
is_standalone = self.is_return and not self.return_against
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
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"):
|
||||
continue
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
"Item", d.item_code, ["has_serial_no", "has_batch_no", "has_expiry_date"], as_dict=1
|
||||
)
|
||||
|
||||
if not self.get("return_against") or (
|
||||
if (
|
||||
set_zero_rate_for_expired_batch
|
||||
and item_details.has_batch_no
|
||||
and item_details.has_expiry_date
|
||||
and self.get("is_return")
|
||||
and not self.get("return_against")
|
||||
and is_batch_expired(d.batch_no, self.get("posting_date"))
|
||||
):
|
||||
# set incoming rate as zero for stand-lone credit note with expired batch
|
||||
d.incoming_rate = 0
|
||||
|
||||
elif not self.get("return_against") or (
|
||||
get_valuation_method(d.item_code) == "Moving Average"
|
||||
and self.get("is_return")
|
||||
and not item_details.has_serial_no
|
||||
@@ -539,6 +551,9 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
|
||||
if old_doc:
|
||||
reset_incoming_rate()
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
or self.is_internal_transfer()
|
||||
@@ -556,11 +571,12 @@ class SellingController(StockController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": d.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation_rate"),
|
||||
"batch_no": d.batch_no,
|
||||
"serial_no": d.serial_no,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
raise_error_if_no_rate=is_standalone,
|
||||
fallbacks=not is_standalone,
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -83,7 +83,8 @@ status_map = {
|
||||
],
|
||||
"Delivery Note": [
|
||||
["Draft", None],
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||
@@ -110,7 +111,7 @@ status_map = {
|
||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||
[
|
||||
"Ordered",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
|
||||
],
|
||||
[
|
||||
"Transferred",
|
||||
@@ -341,14 +342,17 @@ class StatusUpdater(Document):
|
||||
):
|
||||
return
|
||||
|
||||
if qty_or_amount == "qty":
|
||||
action_msg = _(
|
||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||
)
|
||||
if args["target_dt"] != "Quotation Item":
|
||||
if qty_or_amount == "qty":
|
||||
action_msg = _(
|
||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||
)
|
||||
else:
|
||||
action_msg = _(
|
||||
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
|
||||
)
|
||||
else:
|
||||
action_msg = _(
|
||||
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
|
||||
)
|
||||
action_msg = None
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -360,8 +364,7 @@ class StatusUpdater(Document):
|
||||
frappe.bold(_(self.doctype)),
|
||||
frappe.bold(item.get("item_code")),
|
||||
)
|
||||
+ "<br><br>"
|
||||
+ action_msg,
|
||||
+ ("<br><br>" + action_msg if action_msg else ""),
|
||||
OverAllowanceError,
|
||||
title=_("Limit Crossed"),
|
||||
)
|
||||
|
||||
@@ -465,7 +465,10 @@ class StockController(AccountsController):
|
||||
if is_rejected:
|
||||
serial_nos = row.get("rejected_serial_no")
|
||||
type_of_transaction = "Inward" if not self.is_return else "Outward"
|
||||
qty = row.get("rejected_qty")
|
||||
qty = flt(
|
||||
row.get("rejected_qty") * row.get("conversion_factor", 1.0),
|
||||
frappe.get_precision("Serial and Batch Entry", "qty"),
|
||||
)
|
||||
warehouse = row.get("rejected_warehouse")
|
||||
|
||||
if (
|
||||
@@ -1588,7 +1591,7 @@ def get_gl_entries_for_preview(doctype, docname, fields):
|
||||
|
||||
def get_columns(raw_columns, fields):
|
||||
return [
|
||||
{"name": d.get("label"), "editable": False, "width": 110}
|
||||
{"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")}
|
||||
for d in raw_columns
|
||||
if not d.get("hidden") and d.get("fieldname") in fields
|
||||
]
|
||||
|
||||
@@ -254,10 +254,10 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
for row in frappe.get_all(
|
||||
f"{self.subcontract_data.order_doctype} Item",
|
||||
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
|
||||
fields=["item_code", "(qty - received_qty) as qty", "parent", "bom"],
|
||||
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
|
||||
):
|
||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
||||
self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
|
||||
|
||||
def __get_transferred_items(self):
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
@@ -829,13 +829,17 @@ class SubcontractingController(StockController):
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
key = (
|
||||
item_row.item_code,
|
||||
item_row.get(self.subcontract_data.order_field),
|
||||
item_row.get("bom"),
|
||||
)
|
||||
|
||||
if self.qty_to_be_received == item_row.qty:
|
||||
return transfer_item.qty
|
||||
|
||||
if self.qty_to_be_received:
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
|
||||
if self.qty_to_be_received.get(key):
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
|
||||
transfer_item.item_details.required_qty = transfer_item.qty
|
||||
|
||||
if transfer_item.serial_no or frappe.get_cached_value(
|
||||
@@ -880,7 +884,11 @@ class SubcontractingController(StockController):
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[
|
||||
(row.item_code, row.get(self.subcontract_data.order_field))
|
||||
(
|
||||
row.item_code,
|
||||
row.get(self.subcontract_data.order_field),
|
||||
row.get("bom"),
|
||||
)
|
||||
] -= row.qty
|
||||
|
||||
def __set_rate_for_serial_and_batch_bundle(self):
|
||||
|
||||
@@ -602,6 +602,11 @@ class calculate_taxes_and_totals:
|
||||
else:
|
||||
self.grand_total_diff = 0
|
||||
|
||||
# Apply rounding adjustment to grand_total_for_distributing_discount
|
||||
# to prevent precision errors during discount distribution
|
||||
if hasattr(self, "grand_total_for_distributing_discount") and not self.discount_amount_applied:
|
||||
self.grand_total_for_distributing_discount += self.grand_total_diff
|
||||
|
||||
def calculate_totals(self):
|
||||
grand_total_diff = self.grand_total_diff
|
||||
|
||||
|
||||
@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(so.total, 1500)
|
||||
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
|
||||
self.assertEqual(so.grand_total, 1400)
|
||||
|
||||
def test_100_percent_discount_with_inclusive_tax(self):
|
||||
"""Test that 100% discount with inclusive taxes results in zero net_total"""
|
||||
so = make_sales_order(do_not_save=1)
|
||||
so.apply_discount_on = "Grand Total"
|
||||
so.items[0].qty = 2
|
||||
so.items[0].rate = 1300
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Account VAT",
|
||||
"included_in_print_rate": True,
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Account Service Tax",
|
||||
"included_in_print_rate": True,
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
so.save()
|
||||
|
||||
# Apply 100% discount
|
||||
so.discount_amount = 2600
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
# net_total should be exactly 0, not 0.01
|
||||
self.assertEqual(so.net_total, 0)
|
||||
self.assertEqual(so.grand_total, 0)
|
||||
|
||||
@@ -38,18 +38,18 @@ class EmailCampaign(Document):
|
||||
def set_date(self):
|
||||
if getdate(self.start_date) < getdate(today()):
|
||||
frappe.throw(_("Start Date cannot be before the current date"))
|
||||
|
||||
# set the end date as start date + max(send after days) in campaign schedule
|
||||
send_after_days = []
|
||||
campaign = frappe.get_doc("Campaign", self.campaign_name)
|
||||
for entry in campaign.get("campaign_schedules"):
|
||||
send_after_days.append(entry.send_after_days)
|
||||
try:
|
||||
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
|
||||
except ValueError:
|
||||
campaign = frappe.get_cached_doc("Campaign", self.campaign_name)
|
||||
send_after_days = [entry.send_after_days for entry in campaign.get("campaign_schedules")]
|
||||
|
||||
if not send_after_days:
|
||||
frappe.throw(
|
||||
_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)
|
||||
)
|
||||
|
||||
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
|
||||
|
||||
def validate_lead(self):
|
||||
lead_email_id = frappe.db.get_value("Lead", self.recipient, "email_id")
|
||||
if not lead_email_id:
|
||||
@@ -77,58 +77,128 @@ class EmailCampaign(Document):
|
||||
start_date = getdate(self.start_date)
|
||||
end_date = getdate(self.end_date)
|
||||
today_date = getdate(today())
|
||||
|
||||
if start_date > today_date:
|
||||
self.db_set("status", "Scheduled", update_modified=False)
|
||||
new_status = "Scheduled"
|
||||
elif end_date >= today_date:
|
||||
self.db_set("status", "In Progress", update_modified=False)
|
||||
elif end_date < today_date:
|
||||
self.db_set("status", "Completed", update_modified=False)
|
||||
new_status = "In Progress"
|
||||
else:
|
||||
new_status = "Completed"
|
||||
|
||||
if self.status != new_status:
|
||||
self.db_set("status", new_status, update_modified=False)
|
||||
|
||||
|
||||
# called through hooks to send campaign mails to leads
|
||||
def send_email_to_leads_or_contacts():
|
||||
today_date = getdate(today())
|
||||
|
||||
# Get all active email campaigns in a single query
|
||||
email_campaigns = frappe.get_all(
|
||||
"Email Campaign", filters={"status": ("not in", ["Unsubscribed", "Completed", "Scheduled"])}
|
||||
"Email Campaign",
|
||||
filters={"status": "In Progress"},
|
||||
fields=["name", "campaign_name", "email_campaign_for", "recipient", "start_date", "sender"],
|
||||
)
|
||||
for camp in email_campaigns:
|
||||
email_campaign = frappe.get_doc("Email Campaign", camp.name)
|
||||
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
|
||||
|
||||
if not email_campaigns:
|
||||
return
|
||||
|
||||
# Process each email campaign
|
||||
for email_campaign in email_campaigns:
|
||||
try:
|
||||
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.log_error(
|
||||
title=_("Email Campaign Error"),
|
||||
message=_("Campaign {0} not found").format(email_campaign.campaign_name),
|
||||
)
|
||||
continue
|
||||
|
||||
# Find schedules that match today
|
||||
for entry in campaign.get("campaign_schedules"):
|
||||
scheduled_date = add_days(email_campaign.get("start_date"), entry.get("send_after_days"))
|
||||
if scheduled_date == getdate(today()):
|
||||
send_mail(entry, email_campaign)
|
||||
try:
|
||||
scheduled_date = add_days(getdate(email_campaign.start_date), entry.get("send_after_days"))
|
||||
if scheduled_date == today_date:
|
||||
send_mail(entry, email_campaign)
|
||||
except Exception:
|
||||
frappe.log_error(
|
||||
title=_("Email Campaign Send Error"),
|
||||
message=_("Failed to send email for campaign {0} to {1}").format(
|
||||
email_campaign.name, email_campaign.recipient
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def send_mail(entry, email_campaign):
|
||||
recipient_list = []
|
||||
if email_campaign.email_campaign_for == "Email Group":
|
||||
for member in frappe.db.get_list(
|
||||
"Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"]
|
||||
):
|
||||
recipient_list.append(member["email"])
|
||||
campaign_for = email_campaign.get("email_campaign_for")
|
||||
recipient = email_campaign.get("recipient")
|
||||
sender_user = email_campaign.get("sender")
|
||||
campaign_name = email_campaign.get("name")
|
||||
|
||||
# Get recipient emails
|
||||
if campaign_for == "Email Group":
|
||||
recipient_list = frappe.get_all(
|
||||
"Email Group Member",
|
||||
filters={"email_group": recipient, "unsubscribed": 0},
|
||||
pluck="email",
|
||||
)
|
||||
else:
|
||||
recipient_list.append(
|
||||
frappe.db.get_value(
|
||||
email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id"
|
||||
email_id = frappe.db.get_value(campaign_for, recipient, "email_id")
|
||||
if not email_id:
|
||||
frappe.log_error(
|
||||
title=_("Email Campaign Error"),
|
||||
message=_("No email found for {0} {1}").format(campaign_for, recipient),
|
||||
)
|
||||
return
|
||||
recipient_list = [email_id]
|
||||
|
||||
if not recipient_list:
|
||||
frappe.log_error(
|
||||
title=_("Email Campaign Error"),
|
||||
message=_("No recipients found for campaign {0}").format(campaign_name),
|
||||
)
|
||||
return
|
||||
|
||||
# Get email template and sender
|
||||
email_template = frappe.get_cached_doc("Email Template", entry.get("email_template"))
|
||||
sender = frappe.db.get_value("User", sender_user, "email") if sender_user else None
|
||||
|
||||
# Build context for template rendering
|
||||
if campaign_for != "Email Group":
|
||||
context = {"doc": frappe.get_doc(campaign_for, recipient)}
|
||||
else:
|
||||
# For email groups, use the email group document as context
|
||||
context = {"doc": frappe.get_doc("Email Group", recipient)}
|
||||
|
||||
# Render template
|
||||
subject = frappe.render_template(email_template.get("subject"), context)
|
||||
content = frappe.render_template(email_template.response_, context)
|
||||
|
||||
try:
|
||||
comm = make(
|
||||
doctype="Email Campaign",
|
||||
name=campaign_name,
|
||||
subject=subject,
|
||||
content=content,
|
||||
sender=sender,
|
||||
recipients=recipient_list,
|
||||
communication_medium="Email",
|
||||
sent_or_received="Sent",
|
||||
send_email=False,
|
||||
email_template=email_template.name,
|
||||
)
|
||||
|
||||
email_template = frappe.get_doc("Email Template", entry.get("email_template"))
|
||||
sender = frappe.db.get_value("User", email_campaign.get("sender"), "email")
|
||||
context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)}
|
||||
# send mail and link communication to document
|
||||
comm = make(
|
||||
doctype="Email Campaign",
|
||||
name=email_campaign.name,
|
||||
subject=frappe.render_template(email_template.get("subject"), context),
|
||||
content=frappe.render_template(email_template.response_, context),
|
||||
sender=sender,
|
||||
bcc=recipient_list,
|
||||
communication_medium="Email",
|
||||
sent_or_received="Sent",
|
||||
send_email=True,
|
||||
email_template=email_template.name,
|
||||
)
|
||||
frappe.sendmail(
|
||||
recipients=recipient_list,
|
||||
subject=subject,
|
||||
content=content,
|
||||
sender=sender,
|
||||
communication=comm["name"],
|
||||
queue_separately=True,
|
||||
)
|
||||
except Exception:
|
||||
frappe.log_error(title="Email Campaign Failed.")
|
||||
|
||||
return comm
|
||||
|
||||
|
||||
@@ -140,7 +210,12 @@ def unsubscribe_recipient(unsubscribe, method):
|
||||
|
||||
# called through hooks to update email campaign status daily
|
||||
def set_email_campaign_status():
|
||||
email_campaigns = frappe.get_all("Email Campaign", filters={"status": ("!=", "Unsubscribed")})
|
||||
for entry in email_campaigns:
|
||||
email_campaign = frappe.get_doc("Email Campaign", entry.name)
|
||||
email_campaigns = frappe.get_all(
|
||||
"Email Campaign",
|
||||
filters={"status": ("!=", "Unsubscribed")},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for name in email_campaigns:
|
||||
email_campaign = frappe.get_doc("Email Campaign", name)
|
||||
email_campaign.update_status()
|
||||
|
||||
@@ -1,96 +1,48 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:market_segment",
|
||||
"beta": 0,
|
||||
"creation": "2018-10-01 09:59:14.479509",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:market_segment",
|
||||
"creation": "2018-10-01 09:59:14.479509",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"market_segment"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Market Segment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Data",
|
||||
"label": "Market Segment",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-10-01 09:59:14.479509",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Market Segment",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-12-17 12:09:34.687368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Market Segment",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1,
|
||||
"translated_doctype": 1
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ standard_portal_menu_items = [
|
||||
"role": "Customer",
|
||||
},
|
||||
{"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
|
||||
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"},
|
||||
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address", "role": "Customer"},
|
||||
{
|
||||
"title": "Timesheets",
|
||||
"route": "/timesheets",
|
||||
|
||||
@@ -553,8 +553,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
|
||||
do_not_explode: d.do_not_explode,
|
||||
},
|
||||
callback: function (r) {
|
||||
d = locals[cdt][cdn];
|
||||
|
||||
$.extend(d, r.message);
|
||||
refresh_field("items");
|
||||
refresh_field("scrap_items");
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
"label": "Batch Size"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.parenttype == \"Routing\"",
|
||||
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"label": "Sequence ID"
|
||||
@@ -196,7 +196,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-31 16:17:47.287117",
|
||||
"modified": "2026-02-17 15:33:28.495850",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && doc.warehouse",
|
||||
@@ -66,13 +67,14 @@
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
"options": "Company",
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-19 19:06:36.481625",
|
||||
"modified": "2026-02-17 11:53:17.940039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Plant Floor",
|
||||
|
||||
@@ -478,7 +478,7 @@ class ProductionPlan(Document):
|
||||
|
||||
item_details = get_item_details(data.item_code, throw=False)
|
||||
if self.combine_items:
|
||||
bom_no = item_details.bom_no
|
||||
bom_no = item_details.get("bom_no")
|
||||
if data.get("bom_no"):
|
||||
bom_no = data.get("bom_no")
|
||||
|
||||
@@ -648,8 +648,8 @@ class ProductionPlan(Document):
|
||||
self.status = "Completed"
|
||||
|
||||
if self.status != "Completed":
|
||||
self.update_ordered_status()
|
||||
self.update_requested_status()
|
||||
self.update_ordered_status()
|
||||
|
||||
if close is not None:
|
||||
self.db_set("status", self.status)
|
||||
@@ -658,25 +658,17 @@ class ProductionPlan(Document):
|
||||
self.update_bin_qty()
|
||||
|
||||
def update_ordered_status(self):
|
||||
update_status = False
|
||||
for d in self.po_items:
|
||||
if d.planned_qty == d.ordered_qty:
|
||||
update_status = True
|
||||
|
||||
if update_status and self.status != "Completed":
|
||||
self.status = "In Process"
|
||||
for child_table in ["po_items", "sub_assembly_items"]:
|
||||
for item in self.get(child_table):
|
||||
if item.ordered_qty:
|
||||
self.status = "In Process"
|
||||
return
|
||||
|
||||
def update_requested_status(self):
|
||||
if not self.mr_items:
|
||||
return
|
||||
|
||||
update_status = True
|
||||
for d in self.mr_items:
|
||||
if d.quantity != d.requested_qty:
|
||||
update_status = False
|
||||
|
||||
if update_status:
|
||||
self.status = "Material Requested"
|
||||
if d.requested_qty:
|
||||
self.status = "Material Requested"
|
||||
break
|
||||
|
||||
def get_production_items(self):
|
||||
item_dict = {}
|
||||
@@ -701,19 +693,21 @@ class ProductionPlan(Document):
|
||||
"project": self.project,
|
||||
}
|
||||
|
||||
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
|
||||
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse, d.planned_start_date)
|
||||
if self.combine_items:
|
||||
key = (d.item_code, d.sales_order, d.warehouse)
|
||||
key = (d.item_code, d.sales_order, d.warehouse, d.planned_start_date)
|
||||
|
||||
if not d.sales_order:
|
||||
key = (d.name, d.item_code, d.warehouse)
|
||||
key = (d.name, d.item_code, d.warehouse, d.planned_start_date)
|
||||
|
||||
if not item_details["project"] and d.sales_order:
|
||||
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
|
||||
|
||||
if self.get_items_from == "Material Request":
|
||||
item_details.update({"qty": d.planned_qty})
|
||||
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
|
||||
item_dict[
|
||||
(d.item_code, d.material_request_item, d.warehouse, d.planned_start_date)
|
||||
] = item_details
|
||||
else:
|
||||
item_details.update(
|
||||
{
|
||||
@@ -796,6 +790,8 @@ class ProductionPlan(Document):
|
||||
"stock_uom",
|
||||
"bom_level",
|
||||
"schedule_date",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
]:
|
||||
if row.get(field):
|
||||
wo_data[field] = row.get(field)
|
||||
@@ -835,6 +831,8 @@ class ProductionPlan(Document):
|
||||
"qty",
|
||||
"description",
|
||||
"production_plan_item",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
]:
|
||||
po_data[field] = row.get(field)
|
||||
|
||||
@@ -1021,6 +1019,10 @@ class ProductionPlan(Document):
|
||||
if not is_group_warehouse:
|
||||
data.fg_warehouse = self.sub_assembly_warehouse
|
||||
|
||||
if not self.combine_sub_items:
|
||||
data.sales_order = row.sales_order
|
||||
data.sales_order_item = row.sales_order_item
|
||||
|
||||
def set_default_supplier_for_subcontracting_order(self):
|
||||
items = [
|
||||
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
|
||||
|
||||
@@ -565,6 +565,90 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertEqual(po_doc.items[0].fg_item, fg_item)
|
||||
self.assertEqual(po_doc.items[0].item_code, service_item)
|
||||
|
||||
def test_sales_order_references_for_sub_assembly_items(self):
|
||||
"""
|
||||
Test that Sales Order and Sales Order Item references in Work Order and Purchase Order
|
||||
are correctly propagated from the Production Plan.
|
||||
"""
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
# Setup Test Items & BOM
|
||||
fg_item = "Test FG Good Item"
|
||||
sub_assembly_item1 = "Test Sub Assembly Item 1"
|
||||
sub_assembly_item2 = "Test Sub Assembly Item 2"
|
||||
|
||||
bom_tree = {
|
||||
fg_item: {
|
||||
sub_assembly_item1: {"Test Raw Material 1": {}},
|
||||
sub_assembly_item2: {"Test Raw Material 2": {}},
|
||||
}
|
||||
}
|
||||
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
# Create Sales Order
|
||||
so = make_sales_order(item_code=fg_item, qty=10)
|
||||
so_item_row = so.items[0].name
|
||||
|
||||
# Create Production Plan from Sales Order
|
||||
production_plan = frappe.new_doc("Production Plan")
|
||||
production_plan.company = so.company
|
||||
production_plan.get_items_from = "Sales Order"
|
||||
production_plan.item_code = fg_item
|
||||
|
||||
production_plan.get_open_sales_orders()
|
||||
self.assertEqual(production_plan.sales_orders[0].sales_order, so.name)
|
||||
|
||||
production_plan.get_so_items()
|
||||
|
||||
production_plan.skip_available_sub_assembly_item = 0
|
||||
production_plan.get_sub_assembly_items()
|
||||
|
||||
self.assertEqual(len(production_plan.sub_assembly_items), 2)
|
||||
|
||||
# Validate Sales Order references in Sub Assembly Items
|
||||
for row in production_plan.sub_assembly_items:
|
||||
if row.production_item == sub_assembly_item1:
|
||||
row.supplier = "_Test Supplier"
|
||||
row.type_of_manufacturing = "Subcontract"
|
||||
|
||||
self.assertEqual(row.sales_order, so.name)
|
||||
self.assertEqual(row.sales_order_item, so_item_row)
|
||||
|
||||
# Submit Production Plan
|
||||
production_plan.save()
|
||||
production_plan.submit()
|
||||
production_plan.make_work_order()
|
||||
|
||||
# Validate Purchase Order (Subcontracted Item)
|
||||
po_items = frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
{
|
||||
"production_plan": production_plan.name,
|
||||
"fg_item": sub_assembly_item1,
|
||||
},
|
||||
["sales_order", "sales_order_item"],
|
||||
)
|
||||
|
||||
self.assertTrue(po_items)
|
||||
self.assertEqual(po_items[0].sales_order, so.name)
|
||||
self.assertEqual(po_items[0].sales_order_item, so_item_row)
|
||||
|
||||
# Validate Work Order (In-house Item)
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
{
|
||||
"production_plan": production_plan.name,
|
||||
"production_item": sub_assembly_item2,
|
||||
},
|
||||
["sales_order", "sales_order_item"],
|
||||
)
|
||||
|
||||
self.assertTrue(work_orders)
|
||||
self.assertEqual(work_orders[0].sales_order, so.name)
|
||||
self.assertEqual(work_orders[0].sales_order_item, so_item_row)
|
||||
|
||||
def test_production_plan_combine_subassembly(self):
|
||||
"""
|
||||
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
|
||||
@@ -867,7 +951,7 @@ class TestProductionPlan(FrappeTestCase):
|
||||
items_data = pln.get_production_items()
|
||||
|
||||
# Update qty
|
||||
items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
|
||||
items_data[(pln.po_items[0].name, item, None, pln.po_items[0].planned_start_date)]["qty"] = qty
|
||||
|
||||
# Create and Submit Work Order for each item in items_data
|
||||
for _key, item in items_data.items():
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"fg_warehouse",
|
||||
"parent_item_code",
|
||||
"schedule_date",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
"column_break_3",
|
||||
"qty",
|
||||
"bom_no",
|
||||
@@ -212,20 +214,36 @@
|
||||
"label": "Ordered Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Order",
|
||||
"options": "Sales Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sales Order Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-10 13:36:24.759101",
|
||||
"modified": "2026-02-17 12:06:02.309032",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ class ProductionPlanSubAssemblyItem(Document):
|
||||
purchase_order: DF.Link | None
|
||||
qty: DF.Float
|
||||
received_qty: DF.Float
|
||||
sales_order: DF.Link | None
|
||||
sales_order_item: DF.Data | None
|
||||
schedule_date: DF.Datetime | None
|
||||
stock_uom: DF.Link | None
|
||||
supplier: DF.Link | None
|
||||
|
||||
@@ -56,7 +56,6 @@ class TestRouting(FrappeTestCase):
|
||||
self.assertEqual(job_card_doc.total_completed_qty, 10)
|
||||
|
||||
wo_doc.cancel()
|
||||
wo_doc.delete()
|
||||
|
||||
def test_update_bom_operation_time(self):
|
||||
"""Update cost shouldn't update routing times."""
|
||||
|
||||
@@ -595,6 +595,33 @@ class TestWorkOrder(FrappeTestCase):
|
||||
work_order1.cancel()
|
||||
work_order.cancel()
|
||||
|
||||
def test_planned_qty_updates_after_closing_work_order(self):
|
||||
item_code = "_Test FG Item"
|
||||
fg_warehouse = "_Test Warehouse 1 - _TC"
|
||||
|
||||
planned_before = (
|
||||
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||
or 0
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(item=item_code, fg_warehouse=fg_warehouse, qty=10)
|
||||
|
||||
planned_after_submit = (
|
||||
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||
or 0
|
||||
)
|
||||
self.assertEqual(planned_after_submit, planned_before + 10)
|
||||
|
||||
close_work_order(wo.name, "Closed")
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "status"), "Closed")
|
||||
|
||||
planned_after_close = (
|
||||
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||
or 0
|
||||
)
|
||||
self.assertEqual(planned_after_close, planned_before)
|
||||
|
||||
def test_work_order_with_non_transfer_item(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
|
||||
@@ -3186,6 +3213,53 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 0)
|
||||
|
||||
def test_reserved_qty_for_pp_with_extra_material_transfer(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
rm_item_code = make_item(
|
||||
"_Test Reserved Qty PP Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
fg_item_code = make_item(
|
||||
"_Test Reserved Qty PP FG Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry_test_record(
|
||||
item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100
|
||||
)
|
||||
|
||||
make_bom(
|
||||
item=fg_item_code,
|
||||
raw_materials=[rm_item_code],
|
||||
)
|
||||
|
||||
wo_order = make_wo_order_test_record(
|
||||
item=fg_item_code,
|
||||
qty=1,
|
||||
source_warehouse="_Test Warehouse - _TC",
|
||||
skip_transfer=0,
|
||||
target_warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
|
||||
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1)
|
||||
|
||||
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
|
||||
s.items[0].qty += 2 # extra material transfer
|
||||
s.submit()
|
||||
|
||||
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
|
||||
|
||||
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
@@ -664,7 +664,7 @@ erpnext.work_order = {
|
||||
set_custom_buttons: function (frm) {
|
||||
var doc = frm.doc;
|
||||
|
||||
if (doc.docstatus === 1 && doc.status !== "Closed") {
|
||||
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
||||
frm.add_custom_button(
|
||||
__("Close"),
|
||||
function () {
|
||||
@@ -674,9 +674,6 @@ erpnext.work_order = {
|
||||
},
|
||||
__("Status")
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
||||
if (doc.status != "Stopped" && doc.status != "Completed") {
|
||||
frm.add_custom_button(
|
||||
__("Stop"),
|
||||
|
||||
@@ -358,7 +358,7 @@ class WorkOrder(Document):
|
||||
if status != self.status:
|
||||
self.db_set("status", status)
|
||||
|
||||
self.update_required_items()
|
||||
self.update_required_items()
|
||||
|
||||
return status or self.status
|
||||
|
||||
@@ -526,12 +526,10 @@ class WorkOrder(Document):
|
||||
else:
|
||||
self.update_work_order_qty_in_so()
|
||||
|
||||
self.delete_job_card()
|
||||
self.update_completed_qty_in_material_request()
|
||||
self.update_planned_qty()
|
||||
self.update_ordered_qty()
|
||||
self.update_reserved_qty_for_production()
|
||||
self.delete_auto_created_batch_and_serial_no()
|
||||
|
||||
def create_serial_no_batch_no(self):
|
||||
if not (self.has_serial_no or self.has_batch_no):
|
||||
@@ -588,13 +586,6 @@ class WorkOrder(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def delete_auto_created_batch_and_serial_no(self):
|
||||
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
|
||||
frappe.delete_doc("Serial No", row.name)
|
||||
|
||||
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
|
||||
frappe.delete_doc("Batch", row.name)
|
||||
|
||||
def make_serial_nos(self, args):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
|
||||
@@ -1027,10 +1018,6 @@ class WorkOrder(Document):
|
||||
if self.actual_start_date and self.actual_end_date:
|
||||
self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60)
|
||||
|
||||
def delete_job_card(self):
|
||||
for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}):
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def validate_production_item(self):
|
||||
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
|
||||
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
|
||||
@@ -1173,6 +1160,7 @@ class WorkOrder(Document):
|
||||
"operation": item.operation or operation,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"stock_uom": item.stock_uom,
|
||||
"description": item.description,
|
||||
"allow_alternative_item": item.allow_alternative_item,
|
||||
"required_qty": item.qty,
|
||||
@@ -1197,7 +1185,7 @@ class WorkOrder(Document):
|
||||
.select(
|
||||
ste_child.item_code,
|
||||
ste_child.original_item,
|
||||
fn.Sum(ste_child.qty).as_("qty"),
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(ste.docstatus == 1)
|
||||
@@ -1227,7 +1215,7 @@ class WorkOrder(Document):
|
||||
.select(
|
||||
ste_child.item_code,
|
||||
ste_child.original_item,
|
||||
fn.Sum(ste_child.qty).as_("qty"),
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(ste.docstatus == 1)
|
||||
@@ -1607,8 +1595,8 @@ def close_work_order(work_order, status):
|
||||
)
|
||||
)
|
||||
|
||||
work_order.on_close_or_cancel()
|
||||
work_order.update_status(status)
|
||||
work_order.on_close_or_cancel()
|
||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||
work_order.notify_update()
|
||||
return work_order.status
|
||||
@@ -1765,6 +1753,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.purpose = "Material Transfer for Manufacture"
|
||||
doc.for_qty = for_qty
|
||||
|
||||
doc.set_item_locations()
|
||||
@@ -1786,6 +1775,9 @@ def get_reserved_qty_for_production(
|
||||
qty_field = wo_item.required_qty
|
||||
else:
|
||||
qty_field = Case()
|
||||
qty_field = qty_field.when(
|
||||
((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0
|
||||
)
|
||||
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
|
||||
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges
|
||||
|
||||
WORK_ORDER_STATUS_LIST = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
@@ -16,119 +18,98 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}]
|
||||
|
||||
columns = [{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 140}]
|
||||
ranges = get_period_date_ranges(filters)
|
||||
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
|
||||
columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120})
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_periodic_data(filters, entry):
|
||||
periodic_data = {
|
||||
"Not Started": {},
|
||||
"Overdue": {},
|
||||
"Pending": {},
|
||||
"Completed": {},
|
||||
"Closed": {},
|
||||
"Stopped": {},
|
||||
}
|
||||
def get_work_orders(filters):
|
||||
from_date = filters.get("from_date")
|
||||
to_date = filters.get("to_date")
|
||||
|
||||
ranges = get_period_date_ranges(filters)
|
||||
WorkOrder = frappe.qb.DocType("Work Order")
|
||||
|
||||
for from_date, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
for d in entry:
|
||||
if getdate(from_date) <= getdate(d.creation) <= getdate(end_date) and d.status not in [
|
||||
"Draft",
|
||||
"Submitted",
|
||||
"Completed",
|
||||
"Cancelled",
|
||||
]:
|
||||
if d.status in ["Not Started", "Closed", "Stopped"]:
|
||||
periodic_data = update_periodic_data(periodic_data, d.status, period)
|
||||
elif getdate(today()) > getdate(d.planned_end_date):
|
||||
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
|
||||
elif getdate(today()) < getdate(d.planned_end_date):
|
||||
periodic_data = update_periodic_data(periodic_data, "Pending", period)
|
||||
|
||||
if (
|
||||
getdate(from_date) <= getdate(d.actual_end_date) <= getdate(end_date)
|
||||
and d.status == "Completed"
|
||||
):
|
||||
periodic_data = update_periodic_data(periodic_data, "Completed", period)
|
||||
|
||||
return periodic_data
|
||||
|
||||
|
||||
def update_periodic_data(periodic_data, status, period):
|
||||
if periodic_data.get(status).get(period):
|
||||
periodic_data[status][period] += 1
|
||||
else:
|
||||
periodic_data[status][period] = 1
|
||||
|
||||
return periodic_data
|
||||
return (
|
||||
frappe.qb.from_(WorkOrder)
|
||||
.select(WorkOrder.creation, WorkOrder.actual_end_date, WorkOrder.planned_end_date, WorkOrder.status)
|
||||
.where(
|
||||
(WorkOrder.docstatus == 1)
|
||||
& (WorkOrder.company == filters.get("company"))
|
||||
& (
|
||||
(WorkOrder.creation.between(from_date, to_date))
|
||||
| (WorkOrder.actual_end_date.between(from_date, to_date))
|
||||
)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def get_data(filters, columns):
|
||||
data = []
|
||||
entry = frappe.get_all(
|
||||
"Work Order",
|
||||
fields=[
|
||||
"creation",
|
||||
"actual_end_date",
|
||||
"planned_end_date",
|
||||
"status",
|
||||
],
|
||||
filters={"docstatus": 1, "company": filters["company"]},
|
||||
)
|
||||
ranges = build_ranges(filters)
|
||||
period_labels = [scrub(pd) for _fd, _td, pd in ranges]
|
||||
periodic_data = {status: {pd: 0 for pd in period_labels} for status in WORK_ORDER_STATUS_LIST}
|
||||
entries = get_work_orders(filters)
|
||||
|
||||
periodic_data = get_periodic_data(filters, entry)
|
||||
for d in entries:
|
||||
if d.status == "Completed":
|
||||
if not d.actual_end_date:
|
||||
continue
|
||||
|
||||
labels = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
|
||||
chart_data = get_chart_data(periodic_data, columns)
|
||||
ranges = get_period_date_ranges(filters)
|
||||
if period := scrub(get_period_for_date(getdate(d.actual_end_date), ranges)):
|
||||
periodic_data["Completed"][period] += 1
|
||||
continue
|
||||
|
||||
for label in labels:
|
||||
work = {}
|
||||
work["Status"] = _(label)
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
if periodic_data.get(label).get(period):
|
||||
work[scrub(period)] = periodic_data.get(label).get(period)
|
||||
creation_date = getdate(d.creation)
|
||||
period = scrub(get_period_for_date(creation_date, ranges))
|
||||
if not period:
|
||||
continue
|
||||
|
||||
if d.status in ("Not Started", "Closed", "Stopped"):
|
||||
periodic_data[d.status][period] += 1
|
||||
else:
|
||||
if d.planned_end_date and getdate(today()) > getdate(d.planned_end_date):
|
||||
periodic_data["Overdue"][period] += 1
|
||||
else:
|
||||
work[scrub(period)] = 0.0
|
||||
data.append(work)
|
||||
periodic_data["Pending"][period] += 1
|
||||
|
||||
return data, chart_data
|
||||
data = []
|
||||
for status in WORK_ORDER_STATUS_LIST:
|
||||
row = {"status": _(status)}
|
||||
for _fd, _td, period in ranges:
|
||||
row[scrub(period)] = periodic_data[status].get(scrub(period), 0)
|
||||
data.append(row)
|
||||
|
||||
chart = get_chart_data(periodic_data, columns)
|
||||
return data, chart
|
||||
|
||||
|
||||
def get_period_for_date(date, ranges):
|
||||
for from_date, to_date, period in ranges:
|
||||
if from_date <= date <= to_date:
|
||||
return period
|
||||
return None
|
||||
|
||||
|
||||
def build_ranges(filters):
|
||||
ranges = []
|
||||
for from_date, end_date in get_period_date_ranges(filters):
|
||||
period = get_period(end_date, filters)
|
||||
ranges.append((getdate(from_date), getdate(end_date), period))
|
||||
return ranges
|
||||
|
||||
|
||||
def get_chart_data(periodic_data, columns):
|
||||
labels = [d.get("label") for d in columns[1:]]
|
||||
period_labels = [d.get("label") for d in columns[1:]]
|
||||
period_fieldnames = [d.get("fieldname") for d in columns[1:]]
|
||||
|
||||
not_start, overdue, pending, completed, closed, stopped = [], [], [], [], [], []
|
||||
datasets = []
|
||||
for status in WORK_ORDER_STATUS_LIST:
|
||||
values = [periodic_data.get(status, {}).get(fieldname, 0) for fieldname in period_fieldnames]
|
||||
datasets.append({"name": _(status), "values": values})
|
||||
|
||||
for d in labels:
|
||||
not_start.append(periodic_data.get("Not Started").get(d))
|
||||
overdue.append(periodic_data.get("Overdue").get(d))
|
||||
pending.append(periodic_data.get("Pending").get(d))
|
||||
completed.append(periodic_data.get("Completed").get(d))
|
||||
closed.append(periodic_data.get("Closed").get(d))
|
||||
stopped.append(periodic_data.get("Stopped").get(d))
|
||||
|
||||
datasets.append({"name": _("Not Started"), "values": not_start})
|
||||
datasets.append({"name": _("Overdue"), "values": overdue})
|
||||
datasets.append({"name": _("Pending"), "values": pending})
|
||||
datasets.append({"name": _("Completed"), "values": completed})
|
||||
datasets.append({"name": _("Closed"), "values": closed})
|
||||
datasets.append({"name": _("Stopped"), "values": stopped})
|
||||
|
||||
chart = {"data": {"labels": labels, "datasets": datasets}}
|
||||
chart["type"] = "line"
|
||||
|
||||
return chart
|
||||
return {"data": {"labels": period_labels, "datasets": datasets}, "type": "line"}
|
||||
|
||||
@@ -406,6 +406,7 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||
erpnext.patches.v14_0.set_update_price_list_based_on
|
||||
erpnext.patches.v15_0.add_bank_transaction_as_journal_entry_reference
|
||||
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
@@ -428,3 +429,6 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
||||
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
||||
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
|
||||
erpnext.patches.v16_0.add_portal_redirects
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""Append Bank Transaction in custom reference_type options."""
|
||||
new_reference_type = "Bank Transaction"
|
||||
property_setters = frappe.get_all(
|
||||
"Property Setter",
|
||||
filters={
|
||||
"doc_type": "Journal Entry Account",
|
||||
"field_name": "reference_type",
|
||||
"property": "options",
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for property_setter in property_setters:
|
||||
existing_value = frappe.db.get_value("Property Setter", property_setter, "value") or ""
|
||||
|
||||
raw_options = [option.strip() for option in existing_value.split("\n")]
|
||||
# Preserve a single leading blank (for the empty select option) but drop spurious trailing blanks
|
||||
options = raw_options[:1] + [o for o in raw_options[1:] if o]
|
||||
|
||||
if new_reference_type in options:
|
||||
continue
|
||||
|
||||
options.append(new_reference_type)
|
||||
frappe.db.set_value(
|
||||
"Property Setter",
|
||||
property_setter,
|
||||
"value",
|
||||
"\n".join(options),
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from pypika.functions import Replace
|
||||
|
||||
|
||||
def execute():
|
||||
sp = frappe.qb.DocType("Sales Partner")
|
||||
qb.update(sp).set(sp.partner_website, Replace(sp.partner_website, "http://", "https://")).where(
|
||||
sp.partner_website.rlike("^http://.*")
|
||||
).run()
|
||||
14
erpnext/patches/v16_0/add_portal_redirects.py
Normal file
14
erpnext/patches/v16_0/add_portal_redirects.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.exists("Portal Menu Item", {"route": "/addresses", "reference_doctype": "Address"}) and (
|
||||
doc := frappe.get_doc("Portal Menu Item", {"route": "/addresses", "reference_doctype": "Address"})
|
||||
):
|
||||
doc.role = "Customer"
|
||||
doc.save()
|
||||
|
||||
website_settings = frappe.get_single("Website Settings")
|
||||
website_settings.append("route_redirects", {"source": "addresses", "target": "address/list"})
|
||||
website_settings.append("route_redirects", {"source": "projects", "target": "project"})
|
||||
website_settings.save()
|
||||
16
erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
Normal file
16
erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
data = frappe.get_all(
|
||||
"Sales Order Item",
|
||||
filters={"quotation_item": ["is", "set"], "docstatus": 1},
|
||||
fields=["quotation_item", "sum(stock_qty) as ordered_qty"],
|
||||
group_by="quotation_item",
|
||||
)
|
||||
if data:
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
frappe.db.bulk_update(
|
||||
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
|
||||
)
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
@@ -308,6 +308,8 @@ class Project(Document):
|
||||
self.gross_margin = flt(self.total_billed_amount) - expense_amount
|
||||
if self.total_billed_amount:
|
||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
||||
else:
|
||||
self.per_gross_margin = 0
|
||||
|
||||
def update_purchase_costing(self):
|
||||
total_purchase_cost = calculate_total_purchase_cost(self.name)
|
||||
@@ -603,7 +605,7 @@ def send_project_update_email_to_users(project):
|
||||
"sent": 0,
|
||||
"date": today(),
|
||||
"time": nowtime(),
|
||||
"naming_series": "UPDATE-.project.-.YY.MM.DD.-",
|
||||
"naming_series": "UPDATE-.project.-.YY.MM.DD.-.####",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
@@ -584,6 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
} else {
|
||||
me.grand_total_diff = 0;
|
||||
}
|
||||
|
||||
// Apply rounding adjustment to grand_total_for_distributing_discount
|
||||
// to prevent precision errors during discount distribution
|
||||
if (me.grand_total_for_distributing_discount && !me.discount_amount_applied) {
|
||||
me.grand_total_for_distributing_discount += me.grand_total_diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,9 +604,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ?
|
||||
flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total;
|
||||
// total taxes and charges is calculated before adjusting base grand total
|
||||
this.frm.doc.total_taxes_and_charges = flt(
|
||||
this.frm.doc.grand_total - this.frm.doc.net_total - grand_total_diff,
|
||||
precision("total_taxes_and_charges")
|
||||
);
|
||||
|
||||
if (
|
||||
["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(
|
||||
this.frm.doc.doctype
|
||||
)
|
||||
) {
|
||||
this.frm.doc.base_grand_total = this.frm.doc.total_taxes_and_charges
|
||||
? flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate)
|
||||
: this.frm.doc.base_net_total;
|
||||
} else {
|
||||
// other charges added/deducted
|
||||
this.frm.doc.taxes_and_charges_added = this.frm.doc.taxes_and_charges_deducted = 0.0;
|
||||
@@ -626,11 +643,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
["taxes_and_charges_added", "taxes_and_charges_deducted"]);
|
||||
}
|
||||
|
||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||
- grand_total_diff, precision("total_taxes_and_charges"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||
|
||||
// Round grand total as per precision
|
||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
||||
|
||||
|
||||
@@ -2726,10 +2726,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
set_warehouse() {
|
||||
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
|
||||
this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse);
|
||||
}
|
||||
|
||||
set_target_warehouse() {
|
||||
this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
|
||||
this.autofill_warehouse(
|
||||
this.frm.doc.packed_items,
|
||||
"target_warehouse",
|
||||
this.frm.doc.set_target_warehouse
|
||||
);
|
||||
}
|
||||
|
||||
set_from_warehouse() {
|
||||
|
||||
@@ -671,7 +671,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
onchange: function () {
|
||||
change: function () {
|
||||
const me = this;
|
||||
|
||||
frm.call({
|
||||
@@ -974,12 +974,12 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
}
|
||||
|
||||
if (query_args.filters || query_args.query) {
|
||||
opts.get_query = () => query_args;
|
||||
opts.get_query = () => JSON.parse(JSON.stringify(query_args));
|
||||
}
|
||||
|
||||
if (opts.source_doctype) {
|
||||
let data_fields = [];
|
||||
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||
if (["Purchase Receipt", "Delivery Note", "Purchase Invoice"].includes(opts.source_doctype)) {
|
||||
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
|
||||
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
|
||||
data_fields.push({
|
||||
|
||||
@@ -138,14 +138,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_selector_trigger_flag(data),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_warehouse(row),
|
||||
() =>
|
||||
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
|
||||
this.show_scan_message(row.idx, !is_new_row, qty);
|
||||
}),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.clean_up(),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.revert_selector_flag(),
|
||||
|
||||
@@ -80,6 +80,14 @@ erpnext.accounts.ledger_preview = {
|
||||
},
|
||||
|
||||
get_datatable(columns, data, wrapper) {
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldtype === "Currency") {
|
||||
col.format = (value) => {
|
||||
return format_currency(value);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const datatable_options = {
|
||||
columns: columns,
|
||||
data: data,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user