Merge branch 'develop' into fixed-negative-stock-error

This commit is contained in:
rohitwaghchaure 2023-02-04 22:35:58 +05:30 committed by GitHub
commit 7d794bc70a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 2764 additions and 1811 deletions

View File

@ -3,52 +3,71 @@ import requests
from urllib.parse import urlparse from urllib.parse import urlparse
docs_repos = [ WEBSITE_REPOS = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com", "erpnext_com",
"frappe_io", "frappe_io",
] ]
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"frappeframework.com",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def docs_link_exists(body): def is_valid_url(url: str) -> bool:
for line in body.splitlines(): parts = urlparse(url)
for word in line.split(): return all((parts.scheme, parts.netloc, parts.path))
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com": def is_documentation_link(word: str) -> bool:
parts = parsed_url.path.split('/') if not word.startswith("http") or not is_valid_url(word):
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return False
return True
elif parsed_url.netloc == "docs.erpnext.com": parsed_url = urlparse(word)
return True if parsed_url.netloc in DOCUMENTATION_DOMAINS:
return True
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split("/")
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
return True
return False
def contains_documentation_link(body: str) -> bool:
return any(
is_documentation_link(word)
for line in body.splitlines()
for word in line.split()
)
def check_pull_request(number: str) -> "tuple[int, str]":
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
if not response.ok:
return 1, "Pull Request Not Found! ⚠️"
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (
not title.startswith("feat")
or not head_sha
or "no-docs" in body
or "backport" in body
):
return 0, "Skipping documentation checks... 🏃"
if contains_documentation_link(body):
return 0, "Documentation Link Found. You're Awesome! 🎉"
return 1, "Documentation Link Not Found! ⚠️"
if __name__ == "__main__": if __name__ == "__main__":
pr = sys.argv[1] exit_code, message = check_pull_request(sys.argv[1])
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr)) print(message)
sys.exit(exit_code)
if response.ok:
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (title.startswith("feat")
and head_sha
and "no-docs" not in body
and "backport" not in body
):
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")

View File

@ -32,8 +32,8 @@ repos:
- id: black - id: black
additional_dependencies: ['click==8.0.4'] additional_dependencies: ['click==8.0.4']
- repo: https://github.com/timothycrosley/isort - repo: https://github.com/PyCQA/isort
rev: 5.9.1 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
exclude: ".*setup.py$" exclude: ".*setup.py$"

View File

@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = {
accounts = nodes; accounts = nodes;
} }
const get_balances = frappe.call({ frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
method: 'erpnext.accounts.utils.get_account_balances', if(value) {
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
get_balances.then(r => { const get_balances = frappe.call({
if (!r.message || r.message.length == 0) return; method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
for (let account of r.message) { get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
const node = cur_tree.nodes && cur_tree.nodes[account.value]; for (let account of r.message) {
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr const node = cur_tree.nodes && cur_tree.nodes[account.value];
const balance = account.balance_in_account_currency || account.balance; if (!node || node.is_root) continue;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { // show Dr if positive since balance is calculated as debit - credit else show Cr
node.parent && node.parent.find('.balance-area').remove(); const balance = account.balance_in_account_currency || account.balance;
$('<span class="balance-area pull-right">' const dr_or_cr = balance > 0 ? "Dr": "Cr";
+ (account.balance_in_account_currency ? const format = (value, currency) => format_currency(Math.abs(value), currency);
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency) if (account.balance!==undefined) {
+ " " + dr_or_cr node.parent && node.parent.find('.balance-area').remove();
+ '</span>').insertBefore(node.$ul); $('<span class="balance-area pull-right">'
} + (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
}
});
} }
}); });
}, },

View File

@ -1,38 +1,38 @@
{ {
"country_code": "de", "country_code": "de",
"name": "SKR03 mit Kontonummern", "name": "SKR03 mit Kontonummern",
"tree": { "tree": {
"Aktiva": { "Aktiva": {
"is_group": 1, "is_group": 1,
"root_type": "Asset", "root_type": "Asset",
"A - Anlagevermögen": { "A - Anlagevermögen": {
"is_group": 1, "is_group": 1,
"EDV-Software": { "EDV-Software": {
"account_number": "0027", "account_number": "0027",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Gesch\u00e4ftsausstattung": { "Geschäftsausstattung": {
"account_number": "0410", "account_number": "0410",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"B\u00fcroeinrichtung": { "Büroeinrichtung": {
"account_number": "0420", "account_number": "0420",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Darlehen": { "Darlehen": {
"account_number": "0565" "account_number": "0565"
}, },
"Maschinen": { "Maschinen": {
"account_number": "0210", "account_number": "0210",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Betriebsausstattung": { "Betriebsausstattung": {
"account_number": "0400", "account_number": "0400",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Ladeneinrichtung": { "Ladeneinrichtung": {
"account_number": "0430", "account_number": "0430",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Accumulated Depreciation": { "Accumulated Depreciation": {
"account_type": "Accumulated Depreciation" "account_type": "Accumulated Depreciation"
@ -60,36 +60,46 @@
"Durchlaufende Posten": { "Durchlaufende Posten": {
"account_number": "1590" "account_number": "1590"
}, },
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { "Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
"account_number": "1371" "account_number": "1371"
}, },
"Abziehbare Vorsteuer": { "Abziehbare Vorsteuer": {
"account_type": "Tax",
"is_group": 1, "is_group": 1,
"Abziehbare Vorsteuer 7%": { "Abziehbare Vorsteuer 7 %": {
"account_number": "1571" "account_number": "1571",
"account_type": "Tax",
"tax_rate": 7.0
}, },
"Abziehbare Vorsteuer 19%": { "Abziehbare Vorsteuer 19 %": {
"account_number": "1576" "account_number": "1576",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": { "Abziehbare Vorsteuer nach § 13b UStG 19 %": {
"account_number": "1577" "account_number": "1577",
}, "account_type": "Tax",
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { "tax_rate": 19.0
"account_number": "3120"
} }
} }
}, },
"III. Wertpapiere": { "III. Wertpapiere": {
"is_group": 1 "is_group": 1,
"Anteile an verbundenen Unternehmen (Umlaufvermögen)": {
"account_number": "1340"
},
"Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": {
"account_number": "1344"
},
"Sonstige Wertpapiere": {
"account_number": "1348"
}
}, },
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
"is_group": 1, "is_group": 1,
"Kasse": { "Kasse": {
"account_type": "Cash",
"is_group": 1, "is_group": 1,
"account_type": "Cash",
"Kasse": { "Kasse": {
"is_group": 1,
"account_number": "1000", "account_number": "1000",
"account_type": "Cash" "account_type": "Cash"
} }
@ -111,21 +121,21 @@
"C - Rechnungsabgrenzungsposten": { "C - Rechnungsabgrenzungsposten": {
"is_group": 1, "is_group": 1,
"Aktive Rechnungsabgrenzung": { "Aktive Rechnungsabgrenzung": {
"account_number": "0980" "account_number": "0980"
} }
}, },
"D - Aktive latente Steuern": { "D - Aktive latente Steuern": {
"is_group": 1, "is_group": 1,
"Aktive latente Steuern": { "Aktive latente Steuern": {
"account_number": "0983" "account_number": "0983"
} }
}, },
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": { "E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
"is_group": 1 "is_group": 1
} }
}, },
"Passiva": { "Passiva": {
"is_group": 1, "is_group": 1,
"root_type": "Liability", "root_type": "Liability",
"A. Eigenkapital": { "A. Eigenkapital": {
"is_group": 1, "is_group": 1,
@ -200,26 +210,32 @@
}, },
"Umsatzsteuer": { "Umsatzsteuer": {
"is_group": 1, "is_group": 1,
"account_type": "Tax", "Umsatzsteuer 7 %": {
"Umsatzsteuer 7%": { "account_number": "1771",
"account_number": "1771" "account_type": "Tax",
"tax_rate": 7.0
}, },
"Umsatzsteuer 19%": { "Umsatzsteuer 19 %": {
"account_number": "1776" "account_number": "1776",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer-Vorauszahlung": { "Umsatzsteuer-Vorauszahlung": {
"account_number": "1780" "account_number": "1780",
"account_type": "Tax"
}, },
"Umsatzsteuer-Vorauszahlung 1/11": { "Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781" "account_number": "1781"
}, },
"Umsatzsteuer \u00a7 13b UStG 19%": { "Umsatzsteuer nach § 13b UStG 19 %": {
"account_number": "1787" "account_number": "1787",
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer Vorjahr": { "Umsatzsteuer Vorjahr": {
"account_number": "1790" "account_number": "1790"
}, },
"Umsatzsteuer fr\u00fchere Jahre": { "Umsatzsteuer frühere Jahre": {
"account_number": "1791" "account_number": "1791"
} }
} }
@ -234,44 +250,56 @@
"E. Passive latente Steuern": { "E. Passive latente Steuern": {
"is_group": 1 "is_group": 1
} }
}, },
"Erl\u00f6se u. Ertr\u00e4ge 2/8": { "Erlöse u. Erträge 2/8": {
"is_group": 1, "is_group": 1,
"root_type": "Income", "root_type": "Income",
"Erl\u00f6skonten 8": { "Erlöskonten 8": {
"is_group": 1, "is_group": 1,
"Erl\u00f6se": { "Erlöse": {
"account_number": "8200", "account_number": "8200",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erl\u00f6se USt. 19%": { "Erlöse USt. 19 %": {
"account_number": "8400", "account_number": "8400",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erl\u00f6se USt. 7%": { "Erlöse USt. 7 %": {
"account_number": "8300", "account_number": "8300",
"account_type": "Income Account" "account_type": "Income Account"
} }
}, },
"Ertragskonten 2": { "Ertragskonten 2": {
"is_group": 1, "is_group": 1,
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": { "sonstige Zinsen und ähnliche Erträge": {
"account_number": "2650", "account_number": "2650",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Au\u00dferordentliche Ertr\u00e4ge": { "Außerordentliche Erträge": {
"account_number": "2500", "account_number": "2500",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Sonstige Ertr\u00e4ge": { "Sonstige Erträge": {
"account_number": "2700", "account_number": "2700",
"account_type": "Income Account" "account_type": "Income Account"
} }
} }
}, },
"Aufwendungen 2/4": { "Aufwendungen 2/4": {
"is_group": 1, "is_group": 1,
"root_type": "Expense", "root_type": "Expense",
"Fremdleistungen": {
"account_number": "3100",
"account_type": "Expense Account"
},
"Fremdleistungen ohne Vorsteuer": {
"account_number": "3109",
"account_type": "Expense Account"
},
"Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": {
"account_number": "3120",
"account_type": "Expense Account"
},
"Wareneingang": { "Wareneingang": {
"account_number": "3200" "account_number": "3200"
}, },
@ -298,234 +326,234 @@
"Gegenkonto 4996-4998": { "Gegenkonto 4996-4998": {
"account_number": "4999" "account_number": "4999"
}, },
"Abschreibungen": { "Abschreibungen": {
"is_group": 1, "is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": { "Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830", "account_number": "4830",
"account_type": "Accumulated Depreciation" "account_type": "Accumulated Depreciation"
}, },
"Abschreibungen auf Gebäude": { "Abschreibungen auf Gebäude": {
"account_number": "4831", "account_number": "4831",
"account_type": "Depreciation" "account_type": "Depreciation"
}, },
"Abschreibungen auf Kfz": { "Abschreibungen auf Kfz": {
"account_number": "4832", "account_number": "4832",
"account_type": "Depreciation" "account_type": "Depreciation"
}, },
"Sofortabschreibung GWG": { "Sofortabschreibung GWG": {
"account_number": "4855", "account_number": "4855",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Kfz-Kosten": { "Kfz-Kosten": {
"is_group": 1, "is_group": 1,
"Kfz-Steuer": { "Kfz-Steuer": {
"account_number": "4510", "account_number": "4510",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Kfz-Versicherungen": { "Kfz-Versicherungen": {
"account_number": "4520", "account_number": "4520",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"laufende Kfz-Betriebskosten": { "laufende Kfz-Betriebskosten": {
"account_number": "4530", "account_number": "4530",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Kfz-Reparaturen": { "Kfz-Reparaturen": {
"account_number": "4540", "account_number": "4540",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Fremdfahrzeuge": { "Fremdfahrzeuge": {
"account_number": "4570", "account_number": "4570",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"sonstige Kfz-Kosten": { "sonstige Kfz-Kosten": {
"account_number": "4580", "account_number": "4580",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Personalkosten": { "Personalkosten": {
"is_group": 1, "is_group": 1,
"Geh\u00e4lter": { "Gehälter": {
"account_number": "4120", "account_number": "4120",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"gesetzliche soziale Aufwendungen": { "gesetzliche soziale Aufwendungen": {
"account_number": "4130", "account_number": "4130",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aufwendungen f\u00fcr Altersvorsorge": { "Aufwendungen für Altersvorsorge": {
"account_number": "4165", "account_number": "4165",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Verm\u00f6genswirksame Leistungen": { "Vermögenswirksame Leistungen": {
"account_number": "4170", "account_number": "4170",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aushilfsl\u00f6hne": { "Aushilfslöhne": {
"account_number": "4190", "account_number": "4190",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Raumkosten": { "Raumkosten": {
"is_group": 1, "is_group": 1,
"Miete und Nebenkosten": { "Miete und Nebenkosten": {
"account_number": "4210", "account_number": "4210",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": { "Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240", "account_number": "4240",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Reinigung": { "Reinigung": {
"account_number": "4250", "account_number": "4250",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Reparatur/Instandhaltung": { "Reparatur/Instandhaltung": {
"is_group": 1, "is_group": 1,
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": { "Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
"account_number": "4805", "account_number": "4805",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Versicherungsbeitr\u00e4ge": { "Versicherungsbeiträge": {
"is_group": 1, "is_group": 1,
"Versicherungen": { "Versicherungen": {
"account_number": "4360", "account_number": "4360",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Beitr\u00e4ge": { "Beiträge": {
"account_number": "4380", "account_number": "4380",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"sonstige Ausgaben": { "sonstige Ausgaben": {
"account_number": "4390", "account_number": "4390",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": { "steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
"account_number": "4396", "account_number": "4396",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Werbe-/Reisekosten": { "Werbe-/Reisekosten": {
"is_group": 1, "is_group": 1,
"Werbekosten": { "Werbekosten": {
"account_number": "4610", "account_number": "4610",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aufmerksamkeiten": { "Aufmerksamkeiten": {
"account_number": "4653", "account_number": "4653",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": { "nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
"account_number": "4665", "account_number": "4665",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Reisekosten Unternehmer": { "Reisekosten Unternehmer": {
"account_number": "4670", "account_number": "4670",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"verschiedene Kosten": { "verschiedene Kosten": {
"is_group": 1, "is_group": 1,
"Porto": { "Porto": {
"account_number": "4910", "account_number": "4910",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Telekom": { "Telekom": {
"account_number": "4920", "account_number": "4920",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Mobilfunk D2": { "Mobilfunk D2": {
"account_number": "4921", "account_number": "4921",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Internet": { "Internet": {
"account_number": "4922", "account_number": "4922",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"B\u00fcrobedarf": { "Bürobedarf": {
"account_number": "4930", "account_number": "4930",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zeitschriften, B\u00fccher": { "Zeitschriften, Bücher": {
"account_number": "4940", "account_number": "4940",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Fortbildungskosten": { "Fortbildungskosten": {
"account_number": "4945", "account_number": "4945",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Buchf\u00fchrungskosten": { "Buchführungskosten": {
"account_number": "4955", "account_number": "4955",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Abschlu\u00df- u. Pr\u00fcfungskosten": { "Abschluß- u. Prüfungskosten": {
"account_number": "4957", "account_number": "4957",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Nebenkosten des Geldverkehrs": { "Nebenkosten des Geldverkehrs": {
"account_number": "4970", "account_number": "4970",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Werkzeuge und Kleinger\u00e4te": { "Werkzeuge und Kleingeräte": {
"account_number": "4985", "account_number": "4985",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Zinsaufwendungen": { "Zinsaufwendungen": {
"is_group": 1, "is_group": 1,
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": { "Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
"account_number": "2110", "account_number": "2110",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": { "Zinsaufwendungen für KFZ Finanzierung": {
"account_number": "2121", "account_number": "2121",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
} }
}, },
"Anfangsbestand 9": { "Anfangsbestand 9": {
"is_group": 1, "is_group": 1,
"root_type": "Equity", "root_type": "Equity",
"Saldenvortragskonten": { "Saldenvortragskonten": {
"is_group": 1, "is_group": 1,
"Saldenvortrag Sachkonten": { "Saldenvortrag Sachkonten": {
"account_number": "9000" "account_number": "9000"
}, },
"Saldenvortr\u00e4ge Debitoren": { "Saldenvorträge Debitoren": {
"account_number": "9008" "account_number": "9008"
}, },
"Saldenvortr\u00e4ge Kreditoren": { "Saldenvorträge Kreditoren": {
"account_number": "9009" "account_number": "9009"
} }
} }
}, },
"Privatkonten 1": { "Privatkonten 1": {
"is_group": 1, "is_group": 1,
"root_type": "Equity", "root_type": "Equity",
"Privatentnahmen/-einlagen": { "Privatentnahmen/-einlagen": {
"is_group": 1, "is_group": 1,
"Privatentnahme allgemein": { "Privatentnahme allgemein": {
"account_number": "1800" "account_number": "1800"
}, },
"Privatsteuern": { "Privatsteuern": {
"account_number": "1810" "account_number": "1810"
}, },
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": { "Sonderausgaben beschränkt abzugsfähig": {
"account_number": "1820" "account_number": "1820"
}, },
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": { "Sonderausgaben unbeschränkt abzugsfähig": {
"account_number": "1830" "account_number": "1830"
}, },
"Au\u00dfergew\u00f6hnliche Belastungen": { "Außergewöhnliche Belastungen": {
"account_number": "1850" "account_number": "1850"
}, },
"Privateinlagen": { "Privateinlagen": {
"account_number": "1890" "account_number": "1890"
} }
} }
} }
} }
} }

View File

@ -56,7 +56,9 @@
"acc_frozen_upto", "acc_frozen_upto",
"column_break_25", "column_break_25",
"frozen_accounts_modifier", "frozen_accounts_modifier",
"report_settings_sb" "report_settings_sb",
"tab_break_dpet",
"show_balance_in_coa"
], ],
"fields": [ "fields": [
{ {
@ -347,6 +349,17 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account " "label": "Allow multi-currency invoices against single party account "
},
{
"fieldname": "tab_break_dpet",
"fieldtype": "Tab Break",
"label": "Chart Of Accounts"
},
{
"default": "1",
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -354,7 +367,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-11-27 21:49:52.538655", "modified": "2023-01-02 12:07:42.434214",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -184,6 +184,11 @@ def validate_budget_records(args, budget_records, expense_amount):
amount = expense_amount or get_amount(args, budget) amount = expense_amount or get_amount(args, budget)
yearly_action, monthly_action = get_actions(args, budget) yearly_action, monthly_action = get_actions(args, budget)
if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
)
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget( budget_amount = get_accumulated_monthly_budget(
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
@ -195,28 +200,28 @@ def validate_budget_records(args, budget_records, expense_amount):
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
) )
if (
yearly_action in ("Stop", "Warn")
and monthly_action != "Stop"
and yearly_action != monthly_action
):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = amount or get_actual_expense(args) actual_expense = get_actual_expense(args)
if actual_expense > budget_amount: total_expense = actual_expense + amount
diff = actual_expense - budget_amount
if total_expense > budget_amount:
if actual_expense > budget_amount:
error_tense = _("is already")
diff = actual_expense - budget_amount
else:
error_tense = _("will be")
diff = total_expense - budget_amount
currency = frappe.get_cached_value("Company", args.company, "default_currency") currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format( msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
_(action_for), _(action_for),
frappe.bold(args.account), frappe.bold(args.account),
args.budget_against_field, frappe.unscrub(args.budget_against_field),
frappe.bold(budget_against), frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(budget_amount, currency=currency)),
error_tense,
frappe.bold(fmt_money(diff, currency=currency)), frappe.bold(fmt_money(diff, currency=currency)),
) )
@ -227,9 +232,9 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
action = "Warn" action = "Warn"
if action == "Stop": if action == "Stop":
frappe.throw(msg, BudgetError) frappe.throw(msg, BudgetError, title=_("Budget Exceeded"))
else: else:
frappe.msgprint(msg, indicator="orange") frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_actions(args, budget): def get_actions(args, budget):
@ -351,7 +356,9 @@ def get_actual_expense(args):
""" """
select sum(gle.debit) - sum(gle.credit) select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle from `tabGL Entry` gle
where gle.account=%(account)s where
is_cancelled = 0
and gle.account=%(account)s
{condition1} {condition1}
and gle.fiscal_year=%(fiscal_year)s and gle.fiscal_year=%(fiscal_year)s
and gle.company=%(company)s and gle.company=%(company)s

View File

@ -28,9 +28,14 @@ class InvalidDateError(frappe.ValidationError):
class CostCenterAllocation(Document): class CostCenterAllocation(Document):
def __init__(self, *args, **kwargs):
super(CostCenterAllocation, self).__init__(*args, **kwargs)
self._skip_from_date_validation = False
def validate(self): def validate(self):
self.validate_total_allocation_percentage() self.validate_total_allocation_percentage()
self.validate_from_date_based_on_existing_gle() if not self._skip_from_date_validation:
self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation() self.validate_backdated_allocation()
self.validate_main_cost_center() self.validate_main_cost_center()
self.validate_child_cost_centers() self.validate_child_cost_centers()

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry'];
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -69,6 +69,10 @@ class PaymentReconciliation(Document):
def get_jv_entries(self): def get_jv_entries(self):
condition = self.get_conditions() condition = self.get_conditions()
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
dr_or_cr = ( dr_or_cr = (
"credit_in_account_currency" "credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable" if erpnext.get_party_account_type(self.party_type) == "Receivable"

View File

@ -747,6 +747,73 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0) self.assertEqual(len(pr.get("payments")), 0)
def test_cost_center_filter_on_vouchers(self):
"""
Test Cost Center filter is applied on Invoices, Payment Entries and Journals
"""
transaction_date = nowdate()
rate = 100
# 'Main - PR' Cost Center
si1 = self.create_sales_invoice(
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
)
si1.cost_center = self.main_cc.name
si1.submit()
pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe1.cost_center = self.main_cc.name
pe1 = pe1.save().submit()
je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je1.accounts[0].cost_center = self.main_cc.name
je1.accounts[1].cost_center = self.main_cc.name
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer
je1 = je1.save().submit()
# 'Sub - PR' Cost Center
si2 = self.create_sales_invoice(
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
)
si2.cost_center = self.sub_cc.name
si2.submit()
pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe2.cost_center = self.sub_cc.name
pe2 = pe2.save().submit()
je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je2.accounts[0].cost_center = self.sub_cc.name
je2.accounts[1].cost_center = self.sub_cc.name
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2 = je2.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name)
self.assertEqual(len(pr.get("payments")), 2)
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
# Change cost center
pr.cost_center = self.sub_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name)
self.assertEqual(len(pr.get("payments")), 2)
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
def make_customer(customer_name, currency=None): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@ -51,7 +51,7 @@ class PaymentRequest(Document):
if existing_payment_request_amount: if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart": if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account) ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:

View File

@ -675,7 +675,7 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql( reserved_qty = frappe.db.sql(
"""select sum(p_item.qty) as qty """select sum(p_item.stock_qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = '' and ifnull(p.consolidated_invoice, '') = ''

View File

@ -687,11 +687,21 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
def apply_pricing_rule_for_free_items(doc, pricing_rule_args): def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
if pricing_rule_args: if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) args = {(d["item_code"], d["pricing_rules"]): d for d in pricing_rule_args}
for args in pricing_rule_args: for item in doc.items:
if not items or (args.get("item_code"), args.get("pricing_rules")) not in items: if not item.is_free_item:
doc.append("items", args) continue
free_item_data = args.get((item.item_code, item.pricing_rules))
if free_item_data:
free_item_data.pop("item_name")
free_item_data.pop("description")
item.update(free_item_data)
args.pop((item.item_code, item.pricing_rules))
for free_item in args.values():
doc.append("items", free_item)
def get_pricing_rule_items(pr_doc, other_items=False) -> list: def get_pricing_rule_items(pr_doc, other_items=False) -> list:

View File

@ -1426,6 +1426,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total", "fieldname": "tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
@ -1435,12 +1436,13 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total", "fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Base Tax Withholding Net Total", "label": "Base Tax Withholding Net Total",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1554,7 +1556,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:37:38.142688", "modified": "2023-01-28 19:18:56.586321",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -1776,6 +1776,8 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate", "fieldname": "commission_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hide_days": 1, "hide_days": 1,
@ -2141,7 +2143,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-12-12 18:34:33.409895", "modified": "2023-01-28 19:45:47.538163",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1169,6 +1169,46 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def test_bin_details_of_packed_item(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import make_item
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 1", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
si = create_sales_invoice(
item_code="_Test Product Bundle Item New",
update_stock=1,
warehouse="_Test Warehouse - _TC",
transaction_date=add_days(nowdate(), -1),
do_not_submit=1,
)
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
bin_details = frappe.db.get_value(
"Bin",
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
["actual_qty", "projected_qty", "ordered_qty"],
as_dict=1,
)
si.transaction_date = nowdate()
si.save()
packed_item = si.packed_items[0]
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
def test_pos_si_without_payment(self): def test_pos_si_without_payment(self):
make_pos_profile() make_pos_profile()

View File

@ -1,6 +1,6 @@
{ {
"actions": [], "actions": [],
"autoname": "autoincrement", "autoname": "hash",
"creation": "2022-09-13 16:18:59.404842", "creation": "2022-09-13 16:18:59.404842",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -36,11 +36,11 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-09-13 23:40:41.479208", "modified": "2023-01-13 13:40:41.479208",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Withheld Vouchers", "name": "Tax Withheld Vouchers",
"naming_rule": "Autoincrement", "naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@ -410,12 +410,26 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0 tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
## for TDS to be deducted on advances
payment_entry_filters = {
"party_type": "Supplier",
"party": ("in", parties),
"docstatus": 1,
"apply_tax_withholding_amount": 1,
"unallocated_amount": (">", 0),
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"tax_withholding_category": tax_details.get("tax_withholding_category"),
}
field = "sum(tax_withholding_net_total)" field = "sum(tax_withholding_net_total)"
if cint(tax_details.consider_party_ledger_amount): if cint(tax_details.consider_party_ledger_amount):
invoice_filters.pop("apply_tds", None) invoice_filters.pop("apply_tds", None)
field = "sum(grand_total)" field = "sum(grand_total)"
payment_entry_filters.pop("apply_tax_withholding_amount", None)
payment_entry_filters.pop("tax_withholding_category", None)
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
supp_jv_credit_amt = ( supp_jv_credit_amt = (
@ -427,14 +441,28 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
"party": ("in", parties), "party": ("in", parties),
"reference_type": ("!=", "Purchase Invoice"), "reference_type": ("!=", "Purchase Invoice"),
}, },
"sum(credit_in_account_currency)", "sum(credit_in_account_currency - debit_in_account_currency)",
) )
or 0.0 or 0.0
) )
# Get Amount via payment entry
payment_entry_amounts = frappe.db.get_all(
"Payment Entry",
filters=payment_entry_filters,
fields=["sum(unallocated_amount) as amount", "payment_type"],
group_by="payment_type",
)
supp_credit_amt += supp_jv_credit_amt supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.tax_withholding_net_total supp_credit_amt += inv.tax_withholding_net_total
for type in payment_entry_amounts:
if type.payment_type == "Pay":
supp_credit_amt += type.amount
else:
supp_credit_amt -= type.amount
threshold = tax_details.get("threshold", 0) threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0)

View File

@ -16,7 +16,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
def setUpClass(self): def setUpClass(self):
# create relevant supplier, etc # create relevant supplier, etc
create_records() create_records()
create_tax_with_holding_category() create_tax_withholding_category_records()
def tearDown(self): def tearDown(self):
cancel_invoices() cancel_invoices()
@ -38,7 +38,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
pi = create_purchase_invoice(supplier="Test TDS Supplier") pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit() pi.submit()
# assert equal tax deduction on total invoice amount uptil now # assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 3000) self.assertEqual(pi.taxes_and_charges_deducted, 3000)
self.assertEqual(pi.grand_total, 7000) self.assertEqual(pi.grand_total, 7000)
invoices.append(pi) invoices.append(pi)
@ -47,7 +47,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000) pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
pi.submit() pi.submit()
# assert equal tax deduction on total invoice amount uptil now # assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 500) self.assertEqual(pi.taxes_and_charges_deducted, 500)
invoices.append(pi) invoices.append(pi)
@ -130,7 +130,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
invoices.append(si) invoices.append(si)
# create another invoice whose total when added to previously created invoice, # create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold # surpasses cumulative threshold
si = create_sales_invoice(customer="Test TCS Customer", rate=12000) si = create_sales_invoice(customer="Test TCS Customer", rate=12000)
si.submit() si.submit()
@ -329,6 +329,38 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(invoices): for d in reversed(invoices):
d.cancel() d.cancel()
def test_tax_withholding_via_payment_entry_for_advances(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier7", "tax_withholding_category", "Advance TDS Category"
)
# create payment entry
pe1 = create_payment_entry(
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
)
pe1.submit()
self.assertFalse(pe1.get("taxes"))
pe2 = create_payment_entry(
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
)
pe2.submit()
self.assertFalse(pe2.get("taxes"))
pe3 = create_payment_entry(
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier7", paid_amount=4000
)
pe3.apply_tax_withholding_amount = 1
pe3.save()
pe3.submit()
self.assertEquals(pe3.get("taxes")[0].tax_amount, 1200)
pe1.cancel()
pe2.cancel()
pe3.cancel()
def cancel_invoices(): def cancel_invoices():
purchase_invoices = frappe.get_all( purchase_invoices = frappe.get_all(
@ -450,6 +482,32 @@ def create_sales_invoice(**args):
return si return si
def create_payment_entry(**args):
# return payment entry doc object
args = frappe._dict(args)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
"posting_date": today(),
"payment_type": args.payment_type,
"party_type": args.party_type,
"party": args.party,
"company": "_Test Company",
"paid_from": "Cash - _TC",
"paid_to": "Creditors - _TC",
"paid_amount": args.paid_amount or 10000,
"received_amount": args.paid_amount or 10000,
"reference_no": args.reference_no or "12345",
"reference_date": today(),
"paid_from_account_currency": "INR",
"paid_to_account_currency": "INR",
}
)
pe.save()
return pe
def create_records(): def create_records():
# create a new suppliers # create a new suppliers
for name in [ for name in [
@ -460,6 +518,7 @@ def create_records():
"Test TDS Supplier4", "Test TDS Supplier4",
"Test TDS Supplier5", "Test TDS Supplier5",
"Test TDS Supplier6", "Test TDS Supplier6",
"Test TDS Supplier7",
]: ]:
if frappe.db.exists("Supplier", name): if frappe.db.exists("Supplier", name):
continue continue
@ -530,142 +589,129 @@ def create_records():
).insert() ).insert()
def create_tax_with_holding_category(): def create_tax_withholding_category_records():
fiscal_year = get_fiscal_year(today(), company="_Test Company") fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
to_date = fiscal_year[2]
# Cumulative threshold # Cumulative threshold
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"): create_tax_withholding_category(
frappe.get_doc( category_name="Cumulative Threshold TDS",
{ rate=10,
"doctype": "Tax Withholding Category", from_date=from_date,
"name": "Cumulative Threshold TDS", to_date=to_date,
"category_name": "10% TDS", account="TDS - _TC",
"rates": [ single_threshold=0,
{ cumulative_threshold=30000.00,
"from_date": fiscal_year[1], )
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000.00,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): # Category for TCS
frappe.get_doc( create_tax_withholding_category(
{ category_name="Cumulative Threshold TCS",
"doctype": "Tax Withholding Category", rate=10,
"name": "Cumulative Threshold TCS", from_date=from_date,
"category_name": "10% TCS", to_date=to_date,
"rates": [ account="TCS - _TC",
{ single_threshold=0,
"from_date": fiscal_year[1], cumulative_threshold=30000.00,
"to_date": fiscal_year[2], )
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000.00,
}
],
"accounts": [{"company": "_Test Company", "account": "TCS - _TC"}],
}
).insert()
# Single thresold # Single threshold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): create_tax_withholding_category(
frappe.get_doc( category_name="Single Threshold TDS",
{ rate=10,
"doctype": "Tax Withholding Category", from_date=from_date,
"name": "Single Threshold TDS", to_date=to_date,
"category_name": "10% TDS", account="TDS - _TC",
"rates": [ single_threshold=20000,
{ cumulative_threshold=0,
"from_date": fiscal_year[1], )
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 20000.00,
"cumulative_threshold": 0,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): create_tax_withholding_category(
frappe.get_doc( category_name="New TDS Category",
{ rate=10,
"doctype": "Tax Withholding Category", from_date=from_date,
"name": "New TDS Category", to_date=to_date,
"category_name": "New TDS Category", account="TDS - _TC",
"round_off_tax_amount": 1, single_threshold=0,
"consider_party_ledger_amount": 1, cumulative_threshold=30000,
"tax_on_excess_amount": 1, round_off_tax_amount=1,
"rates": [ consider_party_ledger_amount=1,
{ tax_on_excess_amount=1,
"from_date": fiscal_year[1], )
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"): create_tax_withholding_category(
frappe.get_doc( category_name="Test Service Category",
{ rate=10,
"doctype": "Tax Withholding Category", from_date=from_date,
"name": "Test Service Category", to_date=to_date,
"category_name": "Test Service Category", account="TDS - _TC",
"rates": [ single_threshold=2000,
{ cumulative_threshold=2000,
"from_date": fiscal_year[1], )
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 2000,
"cumulative_threshold": 2000,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"): create_tax_withholding_category(
frappe.get_doc( category_name="Test Goods Category",
{ rate=10,
"doctype": "Tax Withholding Category", from_date=from_date,
"name": "Test Goods Category", to_date=to_date,
"category_name": "Test Goods Category", account="TDS - _TC",
"rates": [ single_threshold=2000,
{ cumulative_threshold=2000,
"from_date": fiscal_year[1], )
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 2000,
"cumulative_threshold": 2000,
}
],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}],
}
).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"): create_tax_withholding_category(
category_name="Test Multi Invoice Category",
rate=10,
from_date=from_date,
to_date=to_date,
account="TDS - _TC",
single_threshold=5000,
cumulative_threshold=10000,
)
create_tax_withholding_category(
category_name="Advance TDS Category",
rate=10,
from_date=from_date,
to_date=to_date,
account="TDS - _TC",
single_threshold=5000,
cumulative_threshold=10000,
consider_party_ledger_amount=1,
)
def create_tax_withholding_category(
category_name,
rate,
from_date,
to_date,
account,
single_threshold=0,
cumulative_threshold=0,
round_off_tax_amount=0,
consider_party_ledger_amount=0,
tax_on_excess_amount=0,
):
if not frappe.db.exists("Tax Withholding Category", category_name):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "Tax Withholding Category", "doctype": "Tax Withholding Category",
"name": "Test Multi Invoice Category", "name": category_name,
"category_name": "Test Multi Invoice Category", "category_name": category_name,
"round_off_tax_amount": round_off_tax_amount,
"consider_party_ledger_amount": consider_party_ledger_amount,
"tax_on_excess_amount": tax_on_excess_amount,
"rates": [ "rates": [
{ {
"from_date": fiscal_year[1], "from_date": from_date,
"to_date": fiscal_year[2], "to_date": to_date,
"tax_withholding_rate": 10, "tax_withholding_rate": rate,
"single_threshold": 5000, "single_threshold": single_threshold,
"cumulative_threshold": 10000, "cumulative_threshold": cumulative_threshold,
} }
], ],
"accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], "accounts": [{"company": "_Test Company", "account": account}],
} }
).insert() ).insert()

View File

@ -211,7 +211,13 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) party_details.update(get_company_address(company))
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]: if doctype and doctype in [
"Delivery Note",
"Sales Invoice",
"Sales Order",
"Quotation",
"POS Invoice",
]:
if party_details.company_address: if party_details.company_address:
party_details.update( party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address) get_fetch_values(doctype, "company_address", party_details.company_address)
@ -544,7 +550,7 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
elif term.due_date_based_on == "Day(s) after the end of the invoice month": elif term.due_date_based_on == "Day(s) after the end of the invoice month":
due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days)) due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
else: else:
due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months)) due_date = max(due_date, get_last_day(add_months(due_date, term.credit_months)))
return due_date return due_date

View File

@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object):
ret += [{}] ret += [{}]
# add total row # add total row
if ret is not []: if self.filters.type == "Revenue":
if self.filters.type == "Revenue": total_row = frappe._dict({"name": "Total Deferred Income"})
total_row = frappe._dict({"name": "Total Deferred Income"}) elif self.filters.type == "Expense":
elif self.filters.type == "Expense": total_row = frappe._dict({"name": "Total Deferred Expense"})
total_row = frappe._dict({"name": "Total Deferred Expense"})
for idx, period in enumerate(self.period_list, 0): for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total total_row[period.key] = self.period_total[idx].total
ret.append(total_row) ret.append(total_row)
return ret return ret

View File

@ -526,7 +526,7 @@ def get_columns(filters):
"options": "GL Entry", "options": "GL Entry",
"hidden": 1, "hidden": 1,
}, },
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{ {
"label": _("Account"), "label": _("Account"),
"fieldname": "account", "fieldname": "account",
@ -538,13 +538,13 @@ def get_columns(filters):
"label": _("Debit ({0})").format(currency), "label": _("Debit ({0})").format(currency),
"fieldname": "debit", "fieldname": "debit",
"fieldtype": "Float", "fieldtype": "Float",
"width": 100, "width": 130,
}, },
{ {
"label": _("Credit ({0})").format(currency), "label": _("Credit ({0})").format(currency),
"fieldname": "credit", "fieldname": "credit",
"fieldtype": "Float", "fieldtype": "Float",
"width": 100, "width": 130,
}, },
{ {
"label": _("Balance ({0})").format(currency), "label": _("Balance ({0})").format(currency),

View File

@ -50,6 +50,20 @@ frappe.query_reports["Gross Profit"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Person" "options": "Sales Person"
}, },
{
"fieldname": "warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse",
"get_query": function () {
var company = frappe.query_report.get_filter_value('company');
return {
filters: [
["Warehouse", "company", "=", company]
]
};
},
},
], ],
"tree": true, "tree": true,
"name_field": "parent", "name_field": "parent",

View File

@ -655,10 +655,35 @@ class GrossProfitGenerator(object):
return self.calculate_buying_amount_from_sle( return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code row, my_sle, parenttype, parent, item_row, item_code
) )
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:
return incoming_amount
else: else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code)
return 0.0 return flt(row.qty) * self.get_average_buying_rate(row, item_code)
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
.groupby(delivery_note_item.item_code)
)
incoming_amount = query.run()
return flt(incoming_amount[0][0]) if incoming_amount else 0
def get_average_buying_rate(self, row, item_code): def get_average_buying_rate(self, row, item_code):
args = row args = row
@ -750,6 +775,13 @@ class GrossProfitGenerator(object):
if self.filters.get("item_code"): if self.filters.get("item_code"):
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
if self.filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
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( self.si_list = frappe.db.sql(
""" """
select select
@ -760,7 +792,8 @@ class GrossProfitGenerator(object):
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail, `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`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `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`.name as "item_row", `tabSales Invoice`.is_return,

View File

@ -302,3 +302,82 @@ class TestGrossProfit(FrappeTestCase):
columns, data = execute(filters=filters) columns, data = execute(filters=filters)
self.assertGreater(len(data), 0) self.assertGreater(len(data), 0)
def test_order_connected_dn_and_inv(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
"""
Test gp calculation when invoice and delivery note aren't directly connected.
SO -- INV
|
DN
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=3,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": item.item_code,
"s_warehouse": item.s_warehouse,
"t_warehouse": item.t_warehouse,
"qty": 10,
"basic_rate": 200,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
so = make_sales_order(
customer=self.customer,
company=self.company,
warehouse=self.warehouse,
item=self.item,
qty=4,
do_not_save=False,
do_not_submit=False,
)
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note,
make_sales_invoice,
)
make_delivery_note(so.name).submit()
sinv = make_sales_invoice(so.name).submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 4.0,
"avg._selling_rate": 100.0,
"valuation_rate": 125.0,
"selling_amount": 400.0,
"buying_amount": 500.0,
"gross_profit": -100.0,
"gross_profit_%": -25.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])

View File

@ -4,7 +4,17 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today from frappe.utils import (
add_months,
cint,
flt,
get_last_day,
get_link_to_form,
getdate,
is_last_day_of_the_month,
nowdate,
today,
)
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -400,6 +410,9 @@ def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
) )
if is_last_day_of_the_month(row.depreciation_start_date):
orginal_schedule_date = get_last_day(orginal_schedule_date)
if orginal_schedule_date == posting_date_of_disposal: if orginal_schedule_date == posting_date_of_disposal:
return True return True

View File

@ -126,16 +126,18 @@ def get_asset_value(asset, finance_book=None):
if not asset.calculate_depreciation: if not asset.calculate_depreciation:
return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation)
finance_book_filter = ["finance_book", "is", "not set"] result = frappe.get_all(
if finance_book:
finance_book_filter = ["finance_book", "=", finance_book]
return frappe.db.get_value(
doctype="Asset Finance Book", doctype="Asset Finance Book",
filters=[["parent", "=", asset.asset_id], finance_book_filter], filters={
fieldname="value_after_depreciation", "parent": asset.asset_id,
"finance_book": finance_book or ("is", "not set"),
},
pluck="value_after_depreciation",
limit=1,
) )
return result[0] if result else 0.0
def prepare_chart_data(data, filters): def prepare_chart_data(data, filters):
labels_values_map = {} labels_values_map = {}

View File

@ -15,17 +15,6 @@ class TestBulkTransactionLog(unittest.TestCase):
create_customer() create_customer()
create_item() create_item()
def test_for_single_record(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
data = frappe.db.get_list(
"Sales Invoice",
filters={"posting_date": date.today(), "customer": "Bulk Customer"},
fields=["*"],
)
if not data:
self.fail("No Sales Invoice Created !")
def test_entry_in_log(self): def test_entry_in_log(self):
so_name = create_so() so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")

View File

@ -1221,6 +1221,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "apply_tds",
"fieldname": "tax_withholding_net_total", "fieldname": "tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
@ -1230,12 +1231,13 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "apply_tds",
"fieldname": "base_tax_withholding_net_total", "fieldname": "base_tax_withholding_net_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Base Tax Withholding Net Total", "label": "Base Tax Withholding Net Total",
"no_copy": 1, "no_copy": 1,
"options": "currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1269,7 +1271,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-25 18:08:59.074182", "modified": "2023-01-28 18:59:16.322824",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, getdate, nowdate
from frappe.utils.data import today from frappe.utils.data import today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.party import get_due_date_from_template
from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order
from erpnext.buying.doctype.purchase_order.purchase_order import ( from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_pi_from_po, make_purchase_invoice as make_pi_from_po,
@ -685,6 +686,12 @@ class TestPurchaseOrder(FrappeTestCase):
else: else:
raise Exception raise Exception
def test_default_payment_terms(self):
due_date = get_due_date_from_template(
"_Test Payment Term Template 1", "2023-02-03", None
).strftime("%Y-%m-%d")
self.assertEqual(due_date, "2023-03-31")
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template" po.payment_terms_template = "_Test Payment Term Template"
@ -889,6 +896,11 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(po.status, "Completed") self.assertEqual(po.status, "Completed")
self.assertEqual(mr.status, "Received") self.assertEqual(mr.status, "Received")
def test_variant_item_po(self):
po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1)
self.assertRaises(frappe.ValidationError, po.save)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@ -994,8 +1006,8 @@ def create_purchase_order(**args):
}, },
) )
po.set_missing_values()
if not args.do_not_save: if not args.do_not_save:
po.set_missing_values()
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted: if po.is_subcontracted:

View File

@ -29,6 +29,7 @@
"message_for_supplier", "message_for_supplier",
"terms_section_break", "terms_section_break",
"incoterm", "incoterm",
"named_place",
"tc_name", "tc_name",
"terms", "terms",
"printing_settings", "printing_settings",
@ -278,13 +279,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Incoterm", "label": "Incoterm",
"options": "Incoterm" "options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:26:33.770993", "modified": "2023-01-31 23:22:06.684694",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@ -15,60 +15,4 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestProcurementTracker(FrappeTestCase): class TestProcurementTracker(FrappeTestCase):
def test_result_for_procurement_tracker(self): pass
filters = {"company": "_Test Procurement Company", "cost_center": "Main - _TPC"}
expected_data = self.generate_expected_data()
report = execute(filters)
length = len(report[1])
self.assertEqual(expected_data, report[1][length - 1])
def generate_expected_data(self):
if not frappe.db.exists("Company", "_Test Procurement Company"):
frappe.get_doc(
dict(
doctype="Company",
company_name="_Test Procurement Company",
abbr="_TPC",
default_currency="INR",
country="Pakistan",
)
).insert()
warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company")
mr = make_material_request(
company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC"
)
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.get("items")[0].cost_center = "Main - _TPC"
po.submit()
pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC"
pr.submit()
date_obj = datetime.date(datetime.now())
po.load_from_db()
expected_data = {
"material_request_date": date_obj,
"cost_center": "Main - _TPC",
"project": None,
"requesting_site": "_Test Procurement Warehouse - _TPC",
"requestor": "Administrator",
"material_request_no": mr.name,
"item_code": "_Test Item",
"quantity": 10.0,
"unit_of_measurement": "_Test UOM",
"status": "To Bill",
"purchase_order_date": date_obj,
"purchase_order": po.name,
"supplier": "_Test Supplier",
"estimated_cost": 0.0,
"actual_cost": 0.0,
"purchase_order_amt": po.net_total,
"purchase_order_amt_in_company_currency": po.base_net_total,
"expected_delivery_date": date_obj,
"actual_delivery_date": date_obj,
}
return expected_data

View File

@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
# Used retrun against and supplier and is_retrun because there is an index added for it # Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.db.get_list( data = frappe.get_all(
doctype, doctype,
fields=fields, fields=fields,
filters=[ filters=[

View File

@ -22,7 +22,7 @@ class SellingController(StockController):
def onload(self): def onload(self):
super(SellingController, self).onload() super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items"): for item in self.get("items") + (self.get("packed_items") or []):
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self): def validate(self):

View File

@ -58,7 +58,7 @@ status_map = {
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
], ],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
["On Hold", "eval:self.status=='On Hold'"], ["On Hold", "eval:self.status=='On Hold'"],
], ],
"Purchase Order": [ "Purchase Order": [
@ -79,7 +79,7 @@ status_map = {
["Delivered", "eval:self.status=='Delivered'"], ["Delivered", "eval:self.status=='Delivered'"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["On Hold", "eval:self.status=='On Hold'"], ["On Hold", "eval:self.status=='On Hold'"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
], ],
"Delivery Note": [ "Delivery Note": [
["Draft", None], ["Draft", None],
@ -87,7 +87,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
], ],
"Purchase Receipt": [ "Purchase Receipt": [
["Draft", None], ["Draft", None],
@ -95,7 +95,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"], ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
], ],
"Material Request": [ "Material Request": [
["Draft", None], ["Draft", None],

View File

@ -312,7 +312,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "language", "fieldname": "language",
@ -514,11 +515,10 @@
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2022-10-13 12:42:04.277879", "modified": "2023-01-24 18:20:05.044791",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -282,6 +282,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
"contact_no": "phone_1", "contact_no": "phone_1",
"fax": "fax_1", "fax": "fax_1",
}, },
"field_no_map": ["disabled"],
} }
}, },
target_doc, target_doc,
@ -390,7 +391,7 @@ def get_lead_details(lead, posting_date=None, company=None):
{ {
"territory": lead.territory, "territory": lead.territory,
"customer_name": lead.company_name or lead.lead_name, "customer_name": lead.company_name or lead.lead_name,
"contact_display": " ".join(filter(None, [lead.salutation, lead.lead_name])), "contact_display": " ".join(filter(None, [lead.lead_name])),
"contact_email": lead.email_id, "contact_email": lead.email_id,
"contact_mobile": lead.mobile_no, "contact_mobile": lead.mobile_no,
"contact_phone": lead.phone, "contact_phone": lead.phone,

View File

@ -174,7 +174,10 @@ class TestWebsiteItem(unittest.TestCase):
# Website Item Portal Tests Begin # Website Item Portal Tests Begin
def test_website_item_breadcrumbs(self): def test_website_item_breadcrumbs(self):
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group." """
Check if breadcrumbs include homepage, product listing navigation page,
parent item group(s) and item group
"""
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
item_code = "Test Breadcrumb Item" item_code = "Test Breadcrumb Item"
@ -197,7 +200,7 @@ class TestWebsiteItem(unittest.TestCase):
breadcrumbs = get_parent_item_groups(item.item_group) breadcrumbs = get_parent_item_groups(item.item_group)
self.assertEqual(breadcrumbs[0]["name"], "Home") self.assertEqual(breadcrumbs[0]["name"], "Home")
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category") self.assertEqual(breadcrumbs[1]["name"], "All Products")
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")

View File

@ -65,7 +65,21 @@ frappe.ui.form.on("BOM", {
}); });
}, },
onload_post_render(frm) { validate: function(frm) {
if (frm.doc.fg_based_operating_cost && frm.doc.with_operations) {
frappe.throw({message: __("Please check either with operations or FG Based Operating Cost."), title: __("Mandatory")});
}
},
with_operations: function(frm) {
frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0);
},
fg_based_operating_cost: function(frm) {
frm.set_df_property("with_operations", "hidden", frm.doc.fg_based_operating_cost ? 1 : 0);
},
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
@ -532,18 +546,25 @@ erpnext.bom.update_cost = function(doc) {
}; };
erpnext.bom.calculate_op_cost = function(doc) { erpnext.bom.calculate_op_cost = function(doc) {
var op = doc.operations || [];
doc.operating_cost = 0.0; doc.operating_cost = 0.0;
doc.base_operating_cost = 0.0; doc.base_operating_cost = 0.0;
for(var i=0;i<op.length;i++) { if(doc.with_operations) {
var operating_cost = flt(flt(op[i].hour_rate) * flt(op[i].time_in_mins) / 60, 2); doc.operations.forEach((item) => {
var base_operating_cost = flt(operating_cost * doc.conversion_rate, 2); let operating_cost = flt(flt(item.hour_rate) * flt(item.time_in_mins) / 60, 2);
frappe.model.set_value('BOM Operation',op[i].name, "operating_cost", operating_cost); let base_operating_cost = flt(operating_cost * doc.conversion_rate, 2);
frappe.model.set_value('BOM Operation',op[i].name, "base_operating_cost", base_operating_cost); frappe.model.set_value('BOM Operation',item.name, {
"operating_cost": operating_cost,
"base_operating_cost": base_operating_cost
});
doc.operating_cost += operating_cost; doc.operating_cost += operating_cost;
doc.base_operating_cost += base_operating_cost; doc.base_operating_cost += base_operating_cost;
});
} else if(doc.fg_based_operating_cost) {
let total_operating_cost = doc.quantity * flt(doc.operating_cost_per_bom_quantity);
doc.operating_cost = total_operating_cost;
doc.base_operating_cost = flt(total_operating_cost * doc.conversion_rate, 2);
} }
refresh_field(['operating_cost', 'base_operating_cost']); refresh_field(['operating_cost', 'base_operating_cost']);
}; };

View File

@ -33,6 +33,9 @@
"column_break_23", "column_break_23",
"transfer_material_against", "transfer_material_against",
"routing", "routing",
"fg_based_operating_cost",
"fg_based_section_section",
"operating_cost_per_bom_quantity",
"operations_section", "operations_section",
"operations", "operations",
"materials_section", "materials_section",
@ -575,7 +578,26 @@
{ {
"fieldname": "scrap_items_section", "fieldname": "scrap_items_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"label": "Scrap Items" "label": "Scrap Items"
},
{
"default": "0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "FG based Operating Cost"
},
{
"depends_on": "fg_based_operating_cost",
"fieldname": "fg_based_section_section",
"fieldtype": "Section Break",
"label": "FG Based Operating Cost Section"
},
{
"depends_on": "fg_based_operating_cost",
"fieldname": "operating_cost_per_bom_quantity",
"fieldtype": "Currency",
"label": "Operating Cost Per BOM Quantity"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@ -583,7 +605,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-03 18:42:27.732107", "modified": "2023-01-10 07:47:08.652616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -614,18 +614,26 @@ class BOM(WebsiteGenerator):
"""Update workstation rate and calculates totals""" """Update workstation rate and calculates totals"""
self.operating_cost = 0 self.operating_cost = 0
self.base_operating_cost = 0 self.base_operating_cost = 0
for d in self.get("operations"): if self.get("with_operations"):
if d.workstation: for d in self.get("operations"):
self.update_rate_and_time(d, update_hour_rate) if d.workstation:
self.update_rate_and_time(d, update_hour_rate)
operating_cost = d.operating_cost operating_cost = d.operating_cost
base_operating_cost = d.base_operating_cost base_operating_cost = d.base_operating_cost
if d.set_cost_based_on_bom_qty: if d.set_cost_based_on_bom_qty:
operating_cost = flt(d.cost_per_unit) * flt(self.quantity) operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity) base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
self.operating_cost += flt(operating_cost) self.operating_cost += flt(operating_cost)
self.base_operating_cost += flt(base_operating_cost) self.base_operating_cost += flt(base_operating_cost)
elif self.get("fg_based_operating_cost"):
total_operating_cost = flt(self.get("quantity")) * flt(
self.get("operating_cost_per_bom_quantity")
)
self.operating_cost = total_operating_cost
self.base_operating_cost = flt(total_operating_cost * self.conversion_rate, 2)
def update_rate_and_time(self, row, update_hour_rate=False): def update_rate_and_time(self, row, update_hour_rate=False):
if not row.hour_rate or update_hour_rate: if not row.hour_rate or update_hour_rate:

View File

@ -202,6 +202,33 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.items[0].rate, 20) self.assertEqual(bom.items[0].rate, 20)
def test_bom_cost_with_fg_based_operating_cost(self):
bom = frappe.copy_doc(test_records[4])
bom.insert()
raw_material_cost = 0.0
op_cost = 0.0
op_cost = bom.quantity * bom.operating_cost_per_bom_quantity
for row in bom.items:
raw_material_cost += row.amount
base_raw_material_cost = raw_material_cost * flt(
bom.conversion_rate, bom.precision("conversion_rate")
)
base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
# test amounts in selected currency, almostEqual checks for 7 digits by default
self.assertAlmostEqual(bom.operating_cost, op_cost)
self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
# test amounts in selected currency
self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_subcontractor_sourced_item(self): def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on("Material Transferred for Subcontract") set_backflush_based_on("Material Transferred for Subcontract")

View File

@ -162,5 +162,31 @@
"item": "_Test Variant Item", "item": "_Test Variant Item",
"quantity": 1.0, "quantity": 1.0,
"with_operations": 1 "with_operations": 1
},
{
"items": [
{
"amount": 5000.0,
"doctype": "BOM Item",
"item_code": "_Test Item",
"parentfield": "items",
"qty": 2.0,
"rate": 3000.0,
"uom": "_Test UOM",
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC",
"include_item_in_manufacturing": 1
}
],
"docstatus": 1,
"doctype": "BOM",
"is_active": 1,
"is_default": 1,
"currency": "USD",
"item": "_Test Variant Item",
"quantity": 1.0,
"with_operations": 0,
"fg_based_operating_cost": 1,
"operating_cost_per_bom_quantity": 140
} }
] ]

View File

@ -324,3 +324,5 @@ erpnext.patches.v14_0.create_incoterms_and_migrate_shipment
erpnext.patches.v14_0.setup_clear_repost_logs erpnext.patches.v14_0.setup_clear_repost_logs
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.set_pick_list_status

View File

@ -0,0 +1,12 @@
import frappe
def execute():
if (
frappe.db.sql(
"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tabTax Withheld Vouchers'"""
)[0][0]
== "bigint"
):
frappe.db.change_column_type("Tax Withheld Vouchers", "name", "varchar(140)")

View File

@ -18,9 +18,11 @@ def create_new_cost_center_allocation_records(cc_allocations):
cca = frappe.new_doc("Cost Center Allocation") cca = frappe.new_doc("Cost Center Allocation")
cca.main_cost_center = main_cc cca.main_cost_center = main_cc
cca.valid_from = today() cca.valid_from = today()
cca._skip_from_date_validation = True
for child_cc, percentage in allocations.items(): for child_cc, percentage in allocations.items():
cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage})) cca.append("allocation_percentages", ({"cost_center": child_cc, "percentage": percentage}))
cca.save() cca.save()
cca.submit() cca.submit()

View File

@ -2,7 +2,8 @@ import frappe
from frappe import qb from frappe import qb
from frappe.query_builder import Case, CustomFunction from frappe.query_builder import Case, CustomFunction
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimensions, get_dimensions,
@ -17,9 +18,9 @@ def create_accounting_dimension_fields():
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
def generate_name_for_payment_ledger_entries(gl_entries): def generate_name_for_payment_ledger_entries(gl_entries, start):
for index, entry in enumerate(gl_entries, 1): for index, entry in enumerate(gl_entries, 0):
entry.name = index entry.name = start + index
def get_columns(): def get_columns():
@ -81,6 +82,14 @@ def insert_chunk_into_payment_ledger(insert_query, gl_entries):
def execute(): def execute():
"""
Description:
Migrate records from `tabGL Entry` to `tabPayment Ledger Entry`.
Patch is non-resumable. if patch failed or is terminatted abnormally, clear 'tabPayment Ledger Entry' table manually before re-running. Re-running is safe only during V13->V14 update.
Note: Post successful migration to V14, re-running is NOT-SAFE and SHOULD NOT be attempted.
"""
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
# create accounting dimension fields in Payment Ledger # create accounting dimension fields in Payment Ledger
create_accounting_dimension_fields() create_accounting_dimension_fields()
@ -89,52 +98,90 @@ def execute():
account = qb.DocType("Account") account = qb.DocType("Account")
ifelse = CustomFunction("IF", ["condition", "then", "else"]) ifelse = CustomFunction("IF", ["condition", "then", "else"])
gl_entries = ( # Get Records Count
qb.from_(gl) accounts = (
.inner_join(account) qb.from_(account)
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) .select(account.name)
.select( .where((account.account_type == "Receivable") | (account.account_type == "Payable"))
gl.star, .orderby(account.name)
ConstantColumn(1).as_("docstatus"),
account.account_type.as_("account_type"),
IfNull(
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
).as_("against_voucher_type"),
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
"against_voucher_no"
),
# convert debit/credit to amount
Case()
.when(account.account_type == "Receivable", gl.debit - gl.credit)
.else_(gl.credit - gl.debit)
.as_("amount"),
# convert debit/credit in account currency to amount in account currency
Case()
.when(
account.account_type == "Receivable",
gl.debit_in_account_currency - gl.credit_in_account_currency,
)
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
.as_("amount_in_account_currency"),
)
.where(gl.is_cancelled == 0)
.orderby(gl.creation)
.run(as_dict=True)
) )
un_processed = (
qb.from_(gl)
.select(Count(gl.name))
.where((gl.is_cancelled == 0) & (gl.account.isin(accounts)))
.run()
)[0][0]
# primary key(name) for payment ledger records if un_processed:
generate_name_for_payment_ledger_entries(gl_entries) print(f"Migrating {un_processed} GL Entries to Payment Ledger")
# split data into chunks processed = 0
chunk_size = 1000 last_update_percent = 0
try: batch_size = 5000
for i in range(0, len(gl_entries), chunk_size): last_name = None
insert_query = build_insert_query()
insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) while True:
frappe.db.commit() if last_name:
except Exception as err: where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0)
frappe.db.rollback() else:
ple = qb.DocType("Payment Ledger Entry") where_clause = gl.is_cancelled == 0
qb.from_(ple).delete().where(ple.docstatus >= 0).run()
frappe.db.commit() gl_entries = (
raise err qb.from_(gl)
.inner_join(account)
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
.select(
gl.star,
ConstantColumn(1).as_("docstatus"),
account.account_type.as_("account_type"),
IfNull(
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
).as_("against_voucher_type"),
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
"against_voucher_no"
),
# convert debit/credit to amount
Case()
.when(account.account_type == "Receivable", gl.debit - gl.credit)
.else_(gl.credit - gl.debit)
.as_("amount"),
# convert debit/credit in account currency to amount in account currency
Case()
.when(
account.account_type == "Receivable",
gl.debit_in_account_currency - gl.credit_in_account_currency,
)
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
.as_("amount_in_account_currency"),
)
.where(where_clause)
.orderby(gl.name)
.limit(batch_size)
.run(as_dict=True)
)
if gl_entries:
last_name = gl_entries[-1].name
# primary key(name) for payment ledger records
generate_name_for_payment_ledger_entries(gl_entries, processed)
try:
insert_query = build_insert_query()
insert_chunk_into_payment_ledger(insert_query, gl_entries)
frappe.db.commit()
processed += len(gl_entries)
# Progress message
percent = flt((processed / un_processed) * 100, 2)
if percent - last_update_percent > 1:
print(f"{percent}% ({processed}) records processed")
last_update_percent = percent
except Exception as err:
print("Migration Failed. Clear `tabPayment Ledger Entry` table before re-running")
raise err
else:
break
print(f"{processed} records have been sucessfully migrated")

View File

@ -1,81 +1,98 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.utils import create_batch from frappe.query_builder import CustomFunction
from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt
def remove_duplicate_entries(pl_entries):
unique_vouchers = set()
for x in pl_entries:
unique_vouchers.add(
(x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks)
)
entries = []
for x in unique_vouchers:
entries.append(
frappe._dict(
company=x[0],
account=x[1],
party_type=x[2],
party=x[3],
voucher_type=x[4],
voucher_no=x[5],
gle_remarks=x[6],
)
)
return entries
def execute(): def execute():
"""
Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
"""
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry") gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
# get ple and their remarks from GL Entry # Get empty PLE records
pl_entries = ( un_processed = (
qb.from_(ple) qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
.left_join(gle) )[0][0]
.on(
(ple.account == gle.account)
& (ple.party_type == gle.party_type)
& (ple.party == gle.party)
& (ple.voucher_type == gle.voucher_type)
& (ple.voucher_no == gle.voucher_no)
& (ple.company == gle.company)
)
.select(
ple.company,
ple.account,
ple.party_type,
ple.party,
ple.voucher_type,
ple.voucher_no,
gle.remarks.as_("gle_remarks"),
)
.where((ple.delinked == 0) & (gle.is_cancelled == 0))
.run(as_dict=True)
)
pl_entries = remove_duplicate_entries(pl_entries) if un_processed:
print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry")
if pl_entries: ifelse = CustomFunction("IF", ["condition", "then", "else"])
# split into multiple batches, update and commit for each batch
processed = 0
last_percent_update = 0
batch_size = 1000 batch_size = 1000
for batch in create_batch(pl_entries, batch_size): last_name = None
for entry in batch:
query = (
qb.update(ple)
.set(ple.remarks, entry.gle_remarks)
.where(
(ple.company == entry.company)
& (ple.account == entry.account)
& (ple.party_type == entry.party_type)
& (ple.party == entry.party)
& (ple.voucher_type == entry.voucher_type)
& (ple.voucher_no == entry.voucher_no)
)
)
query.run()
frappe.db.commit() while True:
if last_name:
where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0)
else:
where_clause = (ple.remarks.isnull()) & (ple.delinked == 0)
# results are deterministic
names = (
qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run()
)
if names:
last_name = names[-1][0]
pl_entries = (
qb.from_(ple)
.left_join(gle)
.on(
(ple.account == gle.account)
& (ple.party_type == gle.party_type)
& (ple.party == gle.party)
& (ple.voucher_type == gle.voucher_type)
& (ple.voucher_no == gle.voucher_no)
& (
ple.against_voucher_type
== IfNull(
ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type
)
)
& (
ple.against_voucher_no
== IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no)
)
& (ple.company == gle.company)
& (
((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit)))
| (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit))
)
& (gle.remarks.notnull())
& (gle.is_cancelled == 0)
)
.select(ple.name)
.distinct()
.select(
gle.remarks.as_("gle_remarks"),
)
.where(ple.name.isin(names))
.run(as_dict=True)
)
if pl_entries:
for entry in pl_entries:
query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name))
query.run()
frappe.db.commit()
processed += len(pl_entries)
percentage = flt((processed / un_processed) * 100, 2)
if percentage - last_percent_update > 1:
print(f"{percentage}% ({processed}) PLE records updated")
last_percent_update = percentage
else:
break
print("Remarks succesfully migrated")

View File

@ -0,0 +1,40 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from pypika.terms import ExistsCriterion
def execute():
pl = frappe.qb.DocType("Pick List")
se = frappe.qb.DocType("Stock Entry")
dn = frappe.qb.DocType("Delivery Note")
(
frappe.qb.update(pl).set(
pl.status,
(
frappe.qb.terms.Case()
.when(pl.docstatus == 0, "Draft")
.when(pl.docstatus == 2, "Cancelled")
.else_("Completed")
),
)
).run()
(
frappe.qb.update(pl)
.set(pl.status, "Open")
.where(
(
ExistsCriterion(
frappe.qb.from_(se).select(se.name).where((se.docstatus == 1) & (se.pick_list == pl.name))
)
| ExistsCriterion(
frappe.qb.from_(dn).select(dn.name).where((dn.docstatus == 1) & (dn.pick_list == pl.name))
)
).negate()
& (pl.docstatus == 1)
)
).run()

View File

@ -10,62 +10,6 @@ from frappe.website.serve import get_response
class TestHomepageSection(unittest.TestCase): class TestHomepageSection(unittest.TestCase):
def test_homepage_section_card(self):
try:
frappe.get_doc(
{
"doctype": "Homepage Section",
"name": "Card Section",
"section_based_on": "Cards",
"section_cards": [
{
"title": "Card 1",
"subtitle": "Subtitle 1",
"content": "This is test card 1",
"route": "/card-1",
},
{
"title": "Card 2",
"subtitle": "Subtitle 2",
"content": "This is test card 2",
"image": "test.jpg",
},
],
"no_of_columns": 3,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
set_request(method="GET", path="home")
response = get_response()
self.assertEqual(response.status_code, 200)
html = frappe.safe_decode(response.get_data())
soup = BeautifulSoup(html, "html.parser")
sections = soup.find("main").find_all("section")
self.assertEqual(len(sections), 3)
homepage_section = sections[2]
self.assertEqual(homepage_section.h3.text, "Card Section")
cards = homepage_section.find_all(class_="card")
self.assertEqual(len(cards), 2)
self.assertEqual(cards[0].h5.text, "Card 1")
self.assertEqual(cards[0].a["href"], "/card-1")
self.assertEqual(cards[1].p.text, "Subtitle 2")
img = cards[1].find(class_="card-img-top")
self.assertEqual(img["src"], "test.jpg")
self.assertEqual(img["loading"], "lazy")
# cleanup
frappe.db.rollback()
def test_homepage_section_custom_html(self): def test_homepage_section_custom_html(self):
frappe.get_doc( frappe.get_doc(
{ {

View File

@ -80,7 +80,7 @@ class Task(NestedSet):
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
frappe.throw( frappe.throw(
_( _(
"Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled." "Cannot complete task {0} as its dependant task {1} are not completed / cancelled."
).format(frappe.bold(self.name), frappe.bold(d.task)) ).format(frappe.bold(self.name), frappe.bold(d.task))
) )

View File

@ -387,6 +387,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
"timesheets", "timesheets",
{ {
"time_sheet": timesheet.name, "time_sheet": timesheet.name,
"project_name": time_log.project_name,
"from_time": time_log.from_time,
"to_time": time_log.to_time,
"billing_hours": time_log.billing_hours, "billing_hours": time_log.billing_hours,
"billing_amount": time_log.billing_amount, "billing_amount": time_log.billing_amount,
"timesheet_detail": time_log.name, "timesheet_detail": time_log.name,

View File

@ -122,24 +122,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() { calculate_item_values() {
var me = this; var me = this;
if (!this.discount_amount_applied) { if (!this.discount_amount_applied) {
$.each(this.frm.doc["items"] || [], function(i, item) { for (const item of this.frm.doc.items || []) {
frappe.model.round_floats_in(item); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
if ((!item.qty) && me.frm.doc.is_return) { item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
item.amount = flt(item.rate * -1, precision("amount", item));
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
item.amount = flt(item.rate, precision("amount", item));
} else {
item.amount = flt(item.rate * item.qty, precision("amount", item));
}
item.net_amount = item.amount;
item.item_tax_amount = 0.0; item.item_tax_amount = 0.0;
item.total_weight = flt(item.weight_per_unit * item.stock_qty); item.total_weight = flt(item.weight_per_unit * item.stock_qty);
me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]); me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]);
}); }
} }
} }

View File

@ -1473,6 +1473,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"parenttype": d.parenttype, "parenttype": d.parenttype,
"parent": d.parent, "parent": d.parent,
"pricing_rules": d.pricing_rules, "pricing_rules": d.pricing_rules,
"is_free_item": d.is_free_item,
"warehouse": d.warehouse, "warehouse": d.warehouse,
"serial_no": d.serial_no, "serial_no": d.serial_no,
"batch_no": d.batch_no, "batch_no": d.batch_no,
@ -1690,6 +1691,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this; var me = this;
var valid = true; var valid = true;
if (frappe.flags.ignore_company_party_validation) {
return valid;
}
$.each(["company", "customer"], function(i, fieldname) { $.each(["company", "customer"], function(i, fieldname) {
if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) { if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (!me.frm.doc[fieldname]) { if (!me.frm.doc[fieldname]) {

View File

@ -13,19 +13,11 @@ frappe.setup.on("before_load", function () {
erpnext.setup.slides_settings = [ erpnext.setup.slides_settings = [
{ {
// Brand // Organization
name: 'brand', name: 'organization',
icon: "fa fa-bookmark", title: __("Setup your organization"),
title: __("The Brand"), icon: "fa fa-building",
// help: __('Upload your letter head and logo. (you can edit them later).'),
fields: [ fields: [
{
fieldtype: "Attach Image", fieldname: "attach_logo",
label: __("Attach Logo"),
description: __("100px by 100px"),
is_private: 0,
align: 'center'
},
{ {
fieldname: 'company_name', fieldname: 'company_name',
label: __('Company Name'), label: __('Company Name'),
@ -35,54 +27,9 @@ erpnext.setup.slides_settings = [
{ {
fieldname: 'company_abbr', fieldname: 'company_abbr',
label: __('Company Abbreviation'), label: __('Company Abbreviation'),
fieldtype: 'Data'
}
],
onload: function(slide) {
this.bind_events(slide);
},
bind_events: function (slide) {
slide.get_input("company_name").on("change", function () {
var parts = slide.get_input("company_name").val().split(" ");
var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () {
if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value("");
}
});
},
validate: function() {
if ((this.values.company_name || "").toLowerCase() == "company") {
frappe.msgprint(__("Company Name cannot be Company"));
return false;
}
if (!this.values.company_abbr) {
return false;
}
if (this.values.company_abbr.length > 10) {
return false;
}
return true;
}
},
{
// Organisation
name: 'organisation',
title: __("Your Organization"),
icon: "fa fa-building",
fields: [
{
fieldname: 'company_tagline',
label: __('What does it do?'),
fieldtype: 'Data', fieldtype: 'Data',
placeholder: __('e.g. "Build tools for builders"'), hidden: 1
reqd: 1
}, },
{ fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 },
{ {
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
options: "", fieldtype: 'Select' options: "", fieldtype: 'Select'
@ -94,40 +41,24 @@ erpnext.setup.slides_settings = [
], ],
onload: function (slide) { onload: function (slide) {
this.load_chart_of_accounts(slide);
this.bind_events(slide); this.bind_events(slide);
this.load_chart_of_accounts(slide);
this.set_fy_dates(slide); this.set_fy_dates(slide);
}, },
validate: function () { validate: function () {
let me = this;
let exist;
if (!this.validate_fy_dates()) { if (!this.validate_fy_dates()) {
return false; return false;
} }
// Validate bank name if ((this.values.company_name || "").toLowerCase() == "company") {
if(me.values.bank_account) { frappe.msgprint(__("Company Name cannot be Company"));
frappe.call({ return false;
async: false, }
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", if (!this.values.company_abbr) {
args: { return false;
"coa": me.values.chart_of_accounts, }
"bank_account": me.values.bank_account if (this.values.company_abbr.length > 10) {
}, return false;
callback: function (r) {
if(r.message){
exist = r.message;
me.get_field("bank_account").set_value("");
let message = __('Account {0} already exists. Please enter a different name for your bank account.',
[me.values.bank_account]
);
frappe.msgprint(message);
}
}
});
return !exist; // Return False if exist = true
} }
return true; return true;
@ -151,15 +82,15 @@ erpnext.setup.slides_settings = [
var country = frappe.wizard.values.country; var country = frappe.wizard.values.country;
if (country) { if (country) {
var fy = erpnext.setup.fiscal_years[country]; let fy = erpnext.setup.fiscal_years[country];
var current_year = moment(new Date()).year(); let current_year = moment(new Date()).year();
var next_year = current_year + 1; let next_year = current_year + 1;
if (!fy) { if (!fy) {
fy = ["01-01", "12-31"]; fy = ["01-01", "12-31"];
next_year = current_year; next_year = current_year;
} }
var year_start_date = current_year + "-" + fy[0]; let year_start_date = current_year + "-" + fy[0];
if (year_start_date > frappe.datetime.get_today()) { if (year_start_date > frappe.datetime.get_today()) {
next_year = current_year; next_year = current_year;
current_year -= 1; current_year -= 1;
@ -171,7 +102,7 @@ erpnext.setup.slides_settings = [
load_chart_of_accounts: function (slide) { load_chart_of_accounts: function (slide) {
var country = frappe.wizard.values.country; let country = frappe.wizard.values.country;
if (country) { if (country) {
frappe.call({ frappe.call({
@ -202,12 +133,25 @@ erpnext.setup.slides_settings = [
me.charts_modal(slide, chart_template); me.charts_modal(slide, chart_template);
}); });
slide.get_input("company_name").on("change", function () {
let parts = slide.get_input("company_name").val().split(" ");
let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
slide.get_input("company_abbr").on("change", function () {
if (slide.get_input("company_abbr").val().length > 10) {
frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
slide.get_field("company_abbr").set_value("");
}
});
}, },
charts_modal: function(slide, chart_template) { charts_modal: function(slide, chart_template) {
let parent = __('All Accounts'); let parent = __('All Accounts');
var dialog = new frappe.ui.Dialog({ let dialog = new frappe.ui.Dialog({
title: chart_template, title: chart_template,
fields: [ fields: [
{'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button', {'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button',

View File

@ -491,7 +491,20 @@ erpnext.utils.update_child_items = function(opts) {
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
this.data = []; this.data = frm.doc[opts.child_docname].map((d) => {
return {
"docname": d.name,
"name": d.name,
"item_code": d.item_code,
"delivery_date": d.delivery_date,
"schedule_date": d.schedule_date,
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
"uom": d.uom
}
});
const fields = [{ const fields = [{
fieldtype:'Data', fieldtype:'Data',
fieldname:"docname", fieldname:"docname",
@ -588,7 +601,7 @@ erpnext.utils.update_child_items = function(opts) {
}) })
} }
const dialog = new frappe.ui.Dialog({ new frappe.ui.Dialog({
title: __("Update Items"), title: __("Update Items"),
fields: [ fields: [
{ {
@ -624,24 +637,7 @@ erpnext.utils.update_child_items = function(opts) {
refresh_field("items"); refresh_field("items");
}, },
primary_action_label: __('Update') primary_action_label: __('Update')
}); }).show();
frm.doc[opts.child_docname].forEach(d => {
dialog.fields_dict.trans_items.df.data.push({
"docname": d.name,
"name": d.name,
"item_code": d.item_code,
"delivery_date": d.delivery_date,
"schedule_date": d.schedule_date,
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
"uom": d.uom
});
this.data = dialog.fields_dict.trans_items.df.data;
dialog.fields_dict.trans_items.grid.refresh();
})
dialog.show();
} }
erpnext.utils.map_current_doc = function(opts) { erpnext.utils.map_current_doc = function(opts) {

View File

@ -26,7 +26,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_bom from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
form_grid_templates = {"items": "templates/form_grid/item_grid.html"} form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@ -590,6 +590,23 @@ def make_material_request(source_name, target_doc=None):
target.qty = qty - requested_item_qty.get(source.name, 0) target.qty = qty - requested_item_qty.get(source.name, 0)
target.stock_qty = flt(target.qty) * flt(target.conversion_factor) target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
args = target.as_dict().copy()
args.update(
{
"company": source_parent.get("company"),
"price_list": frappe.db.get_single_value("Buying Settings", "buying_price_list"),
"currency": source_parent.get("currency"),
"conversion_rate": source_parent.get("conversion_rate"),
}
)
target.rate = flt(
get_price_list_rate(args=args, item_doc=frappe.get_cached_doc("Item", target.item_code)).get(
"price_list_rate"
)
)
target.amount = target.qty * target.rate
doc = get_mapped_doc( doc = get_mapped_doc(
"Sales Order", "Sales Order",
source_name, source_name,

View File

@ -552,6 +552,42 @@ class TestSalesOrder(FrappeTestCase):
workflow.is_active = 0 workflow.is_active = 0
workflow.save() workflow.save()
def test_bin_details_of_packed_item(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 1", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
so = make_sales_order(
item_code="_Test Product Bundle Item New",
warehouse="_Test Warehouse - _TC",
transaction_date=add_days(nowdate(), -1),
do_not_submit=1,
)
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
bin_details = frappe.db.get_value(
"Bin",
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
["actual_qty", "projected_qty", "ordered_qty"],
as_dict=1,
)
so.transaction_date = nowdate()
so.save()
packed_item = so.packed_items[0]
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
def test_update_child_product_bundle(self): def test_update_child_product_bundle(self):
# test Update Items with product bundle # test Update Items with product bundle
if not frappe.db.exists("Item", "_Product Bundle Item"): if not frappe.db.exists("Item", "_Product Bundle Item"):

View File

@ -17,45 +17,79 @@ from erpnext.stock.utils import scan_barcode
def search_by_term(search_term, warehouse, price_list): def search_by_term(search_term, warehouse, price_list):
result = search_for_serial_or_batch_or_barcode_number(search_term) or {} result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
item_code = result.get("item_code") or search_term item_code = result.get("item_code", search_term)
serial_no = result.get("serial_no") or "" serial_no = result.get("serial_no", "")
batch_no = result.get("batch_no") or "" batch_no = result.get("batch_no", "")
barcode = result.get("barcode") or "" barcode = result.get("barcode", "")
if result: if not result:
item_info = frappe.db.get_value( return
"Item",
item_code,
[
"name as item_code",
"item_name",
"description",
"stock_uom",
"image as item_image",
"is_stock_item",
],
as_dict=1,
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) item_doc = frappe.get_doc("Item", item_code)
price_list_rate, currency = frappe.db.get_value(
"Item Price",
{"price_list": price_list, "item_code": item_code},
["price_list_rate", "currency"],
) or [None, None]
item_info.update( if not item_doc:
return
item = {
"barcode": barcode,
"batch_no": batch_no,
"description": item_doc.description,
"is_stock_item": item_doc.is_stock_item,
"item_code": item_doc.name,
"item_image": item_doc.image,
"item_name": item_doc.item_name,
"serial_no": serial_no,
"stock_uom": item_doc.stock_uom,
"uom": item_doc.stock_uom,
}
if barcode:
barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None)
if barcode_info and barcode_info.uom:
uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {})
item.update(
{
"uom": barcode_info.uom,
"conversion_factor": uom.get("conversion_factor", 1),
}
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
item_stock_qty = item_stock_qty // item.get("conversion_factor")
item.update({"actual_qty": item_stock_qty})
price = frappe.get_list(
doctype="Item Price",
filters={
"price_list": price_list,
"item_code": item_code,
},
fields=["uom", "stock_uom", "currency", "price_list_rate"],
)
def __sort(p):
p_uom = p.get("uom")
if p_uom == item.get("uom"):
return 0
elif p_uom == item.get("stock_uom"):
return 1
else:
return 2
# sort by fallback preference. always pick exact uom match if available
price = sorted(price, key=__sort)
if len(price) > 0:
p = price.pop(0)
item.update(
{ {
"serial_no": serial_no, "currency": p.get("currency"),
"batch_no": batch_no, "price_list_rate": p.get("price_list_rate"),
"barcode": barcode,
"price_list_rate": price_list_rate,
"currency": currency,
"actual_qty": item_stock_qty,
} }
) )
return {"items": [item_info]} return {"items": [item]}
@frappe.whitelist() @frappe.whitelist()
@ -121,33 +155,43 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
as_dict=1, as_dict=1,
) )
if items_data: # return (empty) list if there are no results
items = [d.item_code for d in items_data] if not items_data:
item_prices_data = frappe.get_all( return result
for item in items_data:
uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
item.uom = item.stock_uom
item_price = frappe.get_all(
"Item Price", "Item Price",
fields=["item_code", "price_list_rate", "currency"], fields=["price_list_rate", "currency", "uom"],
filters={"price_list": price_list, "item_code": ["in", items]}, filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
},
) )
item_prices = {} if not item_price:
for d in item_prices_data: result.append(item)
item_prices[d.item_code] = d
for item in items_data: for price in item_price:
item_code = item.item_code uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
item_price = item_prices.get(item_code) or {}
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
row = {} if price.uom != item.stock_uom and uom and uom.conversion_factor:
row.update(item) item.actual_qty = item.actual_qty // uom.conversion_factor
row.update(
result.append(
{ {
"price_list_rate": item_price.get("price_list_rate"), **item,
"currency": item_price.get("currency"), "price_list_rate": price.get("price_list_rate"),
"actual_qty": item_stock_qty, "currency": price.get("currency"),
"uom": price.uom or item.uom,
} }
) )
result.append(row)
return {"items": result} return {"items": result}

View File

@ -542,12 +542,12 @@ erpnext.PointOfSale.Controller = class {
if (!this.frm.doc.customer) if (!this.frm.doc.customer)
return this.raise_customer_selection_alert(); return this.raise_customer_selection_alert();
const { item_code, batch_no, serial_no, rate } = item; const { item_code, batch_no, serial_no, rate, uom } = item;
if (!item_code) if (!item_code)
return; return;
const new_item = { item_code, batch_no, rate, [field]: value }; const new_item = { item_code, batch_no, rate, uom, [field]: value };
if (serial_no) { if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@ -649,6 +649,7 @@ erpnext.PointOfSale.Controller = class {
const is_stock_item = resp[1]; const is_stock_item = resp[1];
frappe.dom.unfreeze(); frappe.dom.unfreeze();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold(); const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold(); const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold() const bold_available_qty = available_qty.toString().bold()
@ -664,7 +665,7 @@ erpnext.PointOfSale.Controller = class {
} }
} else if (is_stock_item && available_qty < qty_needed) { } else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({ frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]),
indicator: 'orange' indicator: 'orange'
}); });
frappe.utils.play_sound("error"); frappe.utils.play_sound("error");

View File

@ -609,7 +609,7 @@ erpnext.PointOfSale.ItemCart = class {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return ` return `
<div class="item-qty-rate"> <div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0}</span></div> <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount"> <div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div> <div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div> <div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@ -618,7 +618,7 @@ erpnext.PointOfSale.ItemCart = class {
} else { } else {
return ` return `
<div class="item-qty-rate"> <div class="item-qty-rate">
<div class="item-qty"><span>${item_data.qty || 0}</span></div> <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount"> <div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div> <div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div> </div>

View File

@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class {
get_item_html(item) { get_item_html(item) {
const me = this; const me = this;
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item; const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item;
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let indicator_color; let indicator_color;
let qty_to_display = actual_qty; let qty_to_display = actual_qty;
@ -118,7 +118,7 @@ erpnext.PointOfSale.ItemSelector = class {
return ( return (
`<div class="item-wrapper" `<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}" data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}" data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}"> title="${item.item_name}">
@ -128,7 +128,7 @@ erpnext.PointOfSale.ItemSelector = class {
<div class="item-name"> <div class="item-name">
${frappe.ellipsis(item.item_name, 18)} ${frappe.ellipsis(item.item_name, 18)}
</div> </div>
<div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0}</div> <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
</div> </div>
</div>` </div>`
); );

View File

@ -94,7 +94,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_item_html(doc, item_data) { get_item_html(doc, item_data) {
return `<div class="item-row-wrapper"> return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div> <div class="item-name">${item_data.item_name}</div>
<div class="item-qty">${item_data.qty || 0}</div> <div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div> <div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`; </div>`;

View File

@ -41,8 +41,20 @@ def get_columns(filters):
{"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150},
{"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150},
{"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100},
{"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, {
{"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, "label": _("Rate"),
"fieldname": "rate",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{ {
"label": _("Sales Order"), "label": _("Sales Order"),
"fieldtype": "Link", "fieldtype": "Link",
@ -93,8 +105,9 @@ def get_columns(filters):
}, },
{ {
"label": _("Billed Amount"), "label": _("Billed Amount"),
"fieldtype": "currency", "fieldtype": "Currency",
"fieldname": "billed_amount", "fieldname": "billed_amount",
"options": "currency",
"width": 120, "width": 120,
}, },
{ {
@ -104,6 +117,13 @@ def get_columns(filters):
"options": "Company", "options": "Company",
"width": 100, "width": 100,
}, },
{
"label": _("Currency"),
"fieldtype": "Link",
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
] ]
@ -141,31 +161,12 @@ def get_data(filters):
"billed_amount": flt(record.get("billed_amt")), "billed_amount": flt(record.get("billed_amt")),
"company": record.get("company"), "company": record.get("company"),
} }
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
data.append(row) data.append(row)
return data return data
def get_conditions(filters):
conditions = ""
if filters.get("item_group"):
conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group)
if filters.get("from_date"):
conditions += "AND so.transaction_date >= '%s'" % filters.from_date
if filters.get("to_date"):
conditions += "AND so.transaction_date <= '%s'" % filters.to_date
if filters.get("item_code"):
conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code)
if filters.get("customer"):
conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer)
return conditions
def get_customer_details(): def get_customer_details():
details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"])
customer_details = {} customer_details = {}
@ -187,29 +188,50 @@ def get_item_details():
def get_sales_order_details(company_list, filters): def get_sales_order_details(company_list, filters):
conditions = get_conditions(filters) db_so = frappe.qb.DocType("Sales Order")
db_so_item = frappe.qb.DocType("Sales Order Item")
return frappe.db.sql( query = (
""" frappe.qb.from_(db_so)
SELECT .inner_join(db_so_item)
so_item.item_code, so_item.description, so_item.qty, .on(db_so_item.parent == db_so.name)
so_item.uom, so_item.base_rate, so_item.base_amount, .select(
so.name, so.transaction_date, so.customer,so.territory, db_so.name,
so.project, so_item.delivered_qty, db_so.customer,
so_item.billed_amt, so.company db_so.transaction_date,
FROM db_so.territory,
`tabSales Order` so, `tabSales Order Item` so_item db_so.project,
WHERE db_so.company,
so.name = so_item.parent db_so_item.item_code,
AND so.company in ({0}) db_so_item.description,
AND so.docstatus = 1 {1} db_so_item.qty,
""".format( db_so_item.uom,
",".join(["%s"] * len(company_list)), conditions db_so_item.base_rate,
), db_so_item.base_amount,
tuple(company_list), db_so_item.delivered_qty,
as_dict=1, (db_so_item.billed_amt * db_so.conversion_rate).as_("billed_amt"),
)
.where(db_so.docstatus == 1)
.where(db_so.company.isin(tuple(company_list)))
) )
if filters.get("item_group"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group))
if filters.get("from_date"):
query = query.where(db_so.transaction_date >= filters.from_date)
if filters.get("to_date"):
query = query.where(db_so.transaction_date <= filters.to_date)
if filters.get("item_code"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code))
if filters.get("customer"):
query = query.where(db_so.customer == filters.customer)
return query.run(as_dict=1)
def get_chart_data(data): def get_chart_data(data):
item_wise_sales_map = {} item_wise_sales_map = {}

View File

@ -148,12 +148,12 @@ def get_item_for_list_in_html(context):
def get_parent_item_groups(item_group_name, from_item=False): def get_parent_item_groups(item_group_name, from_item=False):
base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} base_nav_page = {"name": _("All Products"), "route": "/all-products"}
if from_item and frappe.request.environ.get("HTTP_REFERER"): if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page # base page after 'Home' will vary on Item page
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
if last_page and last_page in ("shop-by-category", "all-products"): if last_page and last_page == "shop-by-category":
base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}

View File

@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, getdate from frappe.utils import cstr, getdate
from .default_website import website_maker
def create_fiscal_year_and_company(args): def create_fiscal_year_and_company(args):
@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep
).insert() ).insert()
def create_email_digest():
from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True)
if not system_managers:
return
recipients = []
for d in system_managers:
recipients.append({"recipient": d})
companies = frappe.db.sql_list("select name FROM `tabCompany`")
for company in companies:
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
edigest = frappe.get_doc(
{
"doctype": "Email Digest",
"name": "Default Weekly Digest - " + company,
"company": company,
"frequency": "Weekly",
"recipients": recipients,
}
)
for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
if df.fieldname != "scheduler_errors":
edigest.set(df.fieldname, 1)
edigest.insert()
# scheduler errors digest
if companies:
edigest = frappe.new_doc("Email Digest")
edigest.update(
{
"name": "Scheduler Errors",
"company": companies[0],
"frequency": "Daily",
"recipients": recipients,
"scheduler_errors": 1,
"enabled": 1,
}
)
edigest.insert()
def create_logo(args):
if args.get("attach_logo"):
attach_logo = args.get("attach_logo").split(",")
if len(attach_logo) == 3:
filename, filetype, content = attach_logo
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"attached_to_doctype": "Website Settings",
"attached_to_name": "Website Settings",
"decode": True,
}
)
_file.save()
fileurl = _file.file_url
frappe.db.set_value(
"Website Settings",
"Website Settings",
"brand_html",
"<img src='{0}' style='max-width: 40px; max-height: 25px;'> {1}".format(
fileurl, args.get("company_name")
),
)
def create_website(args):
website_maker(args)
def get_fy_details(fy_start_date, fy_end_date): def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year: if start_year == getdate(fy_end_date).year:

View File

@ -1,89 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.utils import nowdate
class website_maker(object):
def __init__(self, args):
self.args = args
self.company = args.company_name
self.tagline = args.company_tagline
self.user = args.get("email")
self.make_web_page()
self.make_website_settings()
self.make_blog()
def make_web_page(self):
# home page
homepage = frappe.get_doc("Homepage", "Homepage")
homepage.company = self.company
homepage.tag_line = self.tagline
homepage.setup_items()
homepage.save()
def make_website_settings(self):
# update in home page in settings
website_settings = frappe.get_doc("Website Settings", "Website Settings")
website_settings.home_page = "home"
website_settings.brand_html = self.company
website_settings.copyright = self.company
website_settings.top_bar_items = []
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"}
)
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"}
)
website_settings.append(
"top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"}
)
website_settings.save()
def make_blog(self):
blog_category = frappe.get_doc(
{"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")}
).insert()
if not self.user:
# Admin setup
return
blogger = frappe.new_doc("Blogger")
user = frappe.get_doc("User", self.user)
blogger.user = self.user
blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "")
blogger.short_name = user.first_name.lower()
blogger.avatar = user.user_image
blogger.insert()
frappe.get_doc(
{
"doctype": "Blog Post",
"title": "Welcome",
"published": 1,
"published_on": nowdate(),
"blogger": blogger.name,
"blog_category": blog_category.name,
"blog_intro": "My First Blog",
"content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(),
}
).insert()
def test():
frappe.delete_doc("Web Page", "test-company")
frappe.delete_doc("Blog Post", "welcome")
frappe.delete_doc("Blogger", "administrator")
frappe.delete_doc("Blog Category", "general")
website_maker(
{
"company": "Test Company",
"company_tagline": "Better Tools for Everyone",
"name": "Administrator",
}
)
frappe.db.commit()

View File

@ -5,7 +5,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from .operations import company_setup
from .operations import install_fixtures as fixtures from .operations import install_fixtures as fixtures
@ -35,7 +34,6 @@ def get_setup_stages(args=None):
"fail_msg": "Failed to set defaults", "fail_msg": "Failed to set defaults",
"tasks": [ "tasks": [
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
{"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")},
], ],
}, },
{ {
@ -60,12 +58,6 @@ def setup_defaults(args):
fixtures.install_defaults(frappe._dict(args)) fixtures.install_defaults(frappe._dict(args))
def stage_four(args):
company_setup.create_website(args)
company_setup.create_email_digest()
company_setup.create_logo(args)
def fin(args): def fin(args):
frappe.local.message_log = [] frappe.local.message_log = []
login_as_first_user(args) login_as_first_user(args)
@ -81,5 +73,4 @@ def setup_complete(args=None):
stage_fixtures(args) stage_fixtures(args)
setup_company(args) setup_company(args)
setup_defaults(args) setup_defaults(args)
stage_four(args)
fin(args) fin(args)

View File

@ -159,13 +159,18 @@ def update_qty(bin_name, args):
last_sle_qty = ( last_sle_qty = (
frappe.qb.from_(sle) frappe.qb.from_(sle)
.select(sle.qty_after_transaction) .select(sle.qty_after_transaction)
.where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) .where(
(sle.item_code == args.get("item_code"))
& (sle.warehouse == args.get("warehouse"))
& (sle.is_cancelled == 0)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
.orderby(sle.creation, order=Order.desc) .orderby(sle.creation, order=Order.desc)
.limit(1) .limit(1)
.run() .run()
) )
actual_qty = 0.0
if last_sle_qty: if last_sle_qty:
actual_qty = last_sle_qty[0][0] actual_qty = last_sle_qty[0][0]

View File

@ -228,6 +228,7 @@ class DeliveryNote(SellingController):
def on_submit(self): def on_submit(self):
self.validate_packed_qty() self.validate_packed_qty()
self.update_pick_list_status()
# Check for Approving Authority # Check for Approving Authority
frappe.get_doc("Authorization Control").validate_approving_authority( frappe.get_doc("Authorization Control").validate_approving_authority(
@ -313,6 +314,11 @@ class DeliveryNote(SellingController):
if has_error: if has_error:
raise frappe.ValidationError raise frappe.ValidationError
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
update_pick_list_status(self.pick_list)
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(
"""select t1.name """select t1.name

View File

@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(gle_warehouse_amount, 1400) self.assertEqual(gle_warehouse_amount, 1400)
def test_bin_details_of_packed_item(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import make_item
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 1", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
si = create_delivery_note(
item_code="_Test Product Bundle Item New",
update_stock=1,
warehouse="_Test Warehouse - _TC",
transaction_date=add_days(nowdate(), -1),
do_not_submit=1,
)
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
bin_details = frappe.db.get_value(
"Bin",
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
["actual_qty", "projected_qty", "ordered_qty"],
as_dict=1,
)
si.transaction_date = nowdate()
si.save()
packed_item = si.packed_items[0]
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
def test_return_for_serialized_items(self): def test_return_for_serialized_items(self):
se = make_serialized_item() se = make_serialized_item()
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
@ -650,6 +690,11 @@ class TestDeliveryNote(FrappeTestCase):
update_delivery_note_status(dn.name, "Closed") update_delivery_note_status(dn.name, "Closed")
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed") self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
# Check cancelling closed delivery note
dn.load_from_db()
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
def test_dn_billing_status_case1(self): def test_dn_billing_status_case1(self):
# SO -> DN -> SI # SO -> DN -> SI
so = make_sales_order() so = make_sales_order()

View File

@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
&& frm.doc.__onload.has_stock_ledger.length) { && frm.doc.__onload.has_stock_ledger.length) {
let allow_to_edit_fields = ['disabled', 'fetch_from_parent', let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
'type_of_transaction', 'condition']; 'type_of_transaction', 'condition', 'mandatory_depends_on'];
frm.fields.forEach((field) => { frm.fields.forEach((field) => {
if (!in_list(allow_to_edit_fields, field.df.fieldname)) { if (!in_list(allow_to_edit_fields, field.df.fieldname)) {

View File

@ -24,6 +24,9 @@
"istable", "istable",
"applicable_condition_example_section", "applicable_condition_example_section",
"condition", "condition",
"conditional_mandatory_section",
"reqd",
"mandatory_depends_on",
"conditional_rule_examples_section", "conditional_rule_examples_section",
"html_19" "html_19"
], ],
@ -153,11 +156,28 @@
"fieldname": "conditional_rule_examples_section", "fieldname": "conditional_rule_examples_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Conditional Rule Examples" "label": "Conditional Rule Examples"
},
{
"description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
"fieldname": "mandatory_depends_on",
"fieldtype": "Small Text",
"label": "Mandatory Depends On"
},
{
"fieldname": "conditional_mandatory_section",
"fieldtype": "Section Break",
"label": "Mandatory Section"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-11-15 15:50:16.767105", "modified": "2023-01-31 13:44:38.507698",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Inventory Dimension", "name": "Inventory Dimension",

View File

@ -126,6 +126,8 @@ class InventoryDimension(Document):
insert_after="inventory_dimension", insert_after="inventory_dimension",
options=self.reference_document, options=self.reference_document,
label=self.dimension_name, label=self.dimension_name,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
), ),
] ]
@ -142,6 +144,8 @@ class InventoryDimension(Document):
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
) and not field_exists("Stock Ledger Entry", self.target_fieldname): ) and not field_exists("Stock Ledger Entry", self.target_fieldname):
dimension_field = dimension_fields[1] dimension_field = dimension_fields[1]
dimension_field["mandatory_depends_on"] = ""
dimension_field["reqd"] = 0
dimension_field["fieldname"] = self.target_fieldname dimension_field["fieldname"] = self.target_fieldname
custom_fields["Stock Ledger Entry"] = dimension_field custom_fields["Stock Ledger Entry"] = dimension_field

View File

@ -85,6 +85,9 @@ class TestInventoryDimension(FrappeTestCase):
condition="parent.purpose == 'Material Issue'", condition="parent.purpose == 'Material Issue'",
) )
inv_dim1.reqd = 0
inv_dim1.save()
create_inventory_dimension( create_inventory_dimension(
reference_document="Shelf", reference_document="Shelf",
type_of_transaction="Inward", type_of_transaction="Inward",
@ -205,6 +208,48 @@ class TestInventoryDimension(FrappeTestCase):
) )
) )
def test_check_mandatory_dimensions(self):
doc = create_inventory_dimension(
reference_document="Pallet",
type_of_transaction="Outward",
dimension_name="Pallet",
apply_to_all_doctypes=0,
document_type="Stock Entry Detail",
)
doc.reqd = 1
doc.save()
self.assertTrue(
frappe.db.get_value(
"Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name"
)
)
doc.load_from_db
doc.reqd = 0
doc.save()
def test_check_mandatory_depends_on_dimensions(self):
doc = create_inventory_dimension(
reference_document="Pallet",
type_of_transaction="Outward",
dimension_name="Pallet",
apply_to_all_doctypes=0,
document_type="Stock Entry Detail",
)
doc.mandatory_depends_on = "t_warehouse"
doc.save()
self.assertTrue(
frappe.db.get_value(
"Custom Field",
{"fieldname": "pallet", "dt": "Stock Entry Detail", "mandatory_depends_on": "t_warehouse"},
"name",
)
)
def prepare_test_data(): def prepare_test_data():
if not frappe.db.exists("DocType", "Shelf"): if not frappe.db.exists("DocType", "Shelf"):
@ -251,6 +296,22 @@ def prepare_test_data():
create_warehouse("Rack Warehouse") create_warehouse("Rack Warehouse")
if not frappe.db.exists("DocType", "Pallet"):
frappe.get_doc(
{
"doctype": "DocType",
"name": "Pallet",
"module": "Stock",
"custom": 1,
"naming_rule": "By fieldname",
"autoname": "field:pallet_name",
"fields": [{"label": "Pallet Name", "fieldname": "pallet_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
],
}
).insert(ignore_permissions=True)
def create_inventory_dimension(**args): def create_inventory_dimension(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -894,6 +894,12 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.uom = frm.doc.stock_uom; new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description; new_child_doc.description = frm.doc.description;
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),
() => {
frappe.flags.ignore_company_party_validation = true;
frappe.model.trigger("item_code", frm.doc.name, new_child_doc);
}
])
}); });
} }

View File

@ -74,11 +74,10 @@ class ItemAttribute(Document):
def validate_duplication(self): def validate_duplication(self):
values, abbrs = [], [] values, abbrs = [], []
for d in self.item_attribute_values: for d in self.item_attribute_values:
d.abbr = d.abbr.upper() if d.attribute_value.lower() in map(str.lower, values):
if d.attribute_value in values: frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
frappe.throw(_("{0} must appear only once").format(d.attribute_value))
values.append(d.attribute_value) values.append(d.attribute_value)
if d.abbr in abbrs: if d.abbr.lower() in map(str.lower, abbrs):
frappe.throw(_("{0} must appear only once").format(d.abbr)) frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title()))
abbrs.append(d.abbr) abbrs.append(d.abbr)

View File

@ -366,10 +366,11 @@ frappe.ui.form.on('Material Request', {
frappe.ui.form.on("Material Request Item", { frappe.ui.form.on("Material Request Item", {
qty: function (frm, doctype, name) { qty: function (frm, doctype, name) {
var d = locals[doctype][name]; const item = locals[doctype][name];
if (flt(d.qty) < flt(d.min_order_qty)) { if (flt(item.qty) < flt(item.min_order_qty)) {
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty")); frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty"));
} }
frm.events.get_item_data(frm, item, false);
}, },
from_warehouse: function(frm, doctype, name) { from_warehouse: function(frm, doctype, name) {

View File

@ -26,7 +26,8 @@
"locations", "locations",
"amended_from", "amended_from",
"print_settings_section", "print_settings_section",
"group_same_items" "group_same_items",
"status"
], ],
"fields": [ "fields": [
{ {
@ -168,11 +169,26 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "read_only": 1
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
"reqd": 1,
"search_index": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-07-19 11:03:04.442174", "modified": "2023-01-24 10:33:43.244476",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",
@ -244,4 +260,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -11,7 +11,8 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import Locate from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today from frappe.utils import cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
@ -77,15 +78,32 @@ class PickList(Document):
) )
def on_submit(self): def on_submit(self):
self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
def on_cancel(self): def on_cancel(self):
self.update_status()
self.update_bundle_picked_qty() self.update_bundle_picked_qty()
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
def update_status(self, status=None, update_modified=True):
if not status:
if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
if self.status == "Draft":
status = "Open"
elif target_document_exists(self.name, self.purpose):
status = "Completed"
elif self.docstatus == 2:
status = "Cancelled"
if status:
frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified)
def update_reference_qty(self): def update_reference_qty(self):
packed_items = [] packed_items = []
so_items = [] so_items = []
@ -162,6 +180,7 @@ class PickList(Document):
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
self.validate_for_qty() self.validate_for_qty()
items = self.aggregate_item_qty() items = self.aggregate_item_qty()
picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict() self.item_location_map = frappe._dict()
from_warehouses = None from_warehouses = None
@ -180,7 +199,11 @@ class PickList(Document):
self.item_location_map.setdefault( self.item_location_map.setdefault(
item_code, item_code,
get_available_item_locations( get_available_item_locations(
item_code, from_warehouses, self.item_count_map.get(item_code), self.company item_code,
from_warehouses,
self.item_count_map.get(item_code),
self.company,
picked_item_details=picked_items_details.get(item_code),
), ),
) )
@ -309,6 +332,56 @@ class PickList(Document):
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
) )
def get_picked_items_details(self, items):
picked_items = frappe._dict()
if items:
pi = frappe.qb.DocType("Pick List")
pi_item = frappe.qb.DocType("Pick List Item")
query = (
frappe.qb.from_(pi)
.inner_join(pi_item)
.on(pi.name == pi_item.parent)
.select(
pi_item.item_code,
pi_item.warehouse,
pi_item.batch_no,
Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_(
"picked_qty"
),
Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"),
)
.where(
(pi_item.item_code.isin([x.item_code for x in items]))
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
& (pi.status != "Completed")
& (pi_item.docstatus != 2)
)
.groupby(
pi_item.item_code,
pi_item.warehouse,
pi_item.batch_no,
)
)
if self.name:
query = query.where(pi_item.parent != self.name)
items_data = query.run(as_dict=True)
for item_data in items_data:
key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse
serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None
data = {"picked_qty": item_data.picked_qty}
if serial_no:
data["serial_no"] = serial_no
if item_data.item_code not in picked_items:
picked_items[item_data.item_code] = {key: data}
else:
picked_items[item_data.item_code][key] = data
return picked_items
def _get_product_bundles(self) -> Dict[str, str]: def _get_product_bundles(self) -> Dict[str, str]:
# Dict[so_item_row: item_code] # Dict[so_item_row: item_code]
product_bundles = {} product_bundles = {}
@ -346,29 +419,30 @@ class PickList(Document):
return int(flt(min(possible_bundles), precision or 6)) return int(flt(min(possible_bundles), precision or 6))
def update_pick_list_status(pick_list):
if pick_list:
doc = frappe.get_doc("Pick List", pick_list)
doc.run_method("update_status")
def get_picked_items_qty(items) -> List[Dict]: def get_picked_items_qty(items) -> List[Dict]:
return frappe.db.sql( pi_item = frappe.qb.DocType("Pick List Item")
f""" return (
SELECT frappe.qb.from_(pi_item)
sales_order_item, .select(
item_code, pi_item.sales_order_item,
sales_order, pi_item.item_code,
SUM(stock_qty) AS stock_qty, pi_item.sales_order,
SUM(picked_qty) AS picked_qty Sum(pi_item.stock_qty).as_("stock_qty"),
FROM Sum(pi_item.picked_qty).as_("picked_qty"),
`tabPick List Item` )
WHERE .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
sales_order_item IN ( .groupby(
{", ".join(frappe.db.escape(d) for d in items)} pi_item.sales_order_item,
) pi_item.sales_order,
AND docstatus = 1 )
GROUP BY .for_update()
sales_order_item, ).run(as_dict=True)
sales_order
FOR UPDATE
""",
as_dict=1,
)
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
@ -434,31 +508,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
def get_available_item_locations( def get_available_item_locations(
item_code, from_warehouses, required_qty, company, ignore_validation=False item_code,
from_warehouses,
required_qty,
company,
ignore_validation=False,
picked_item_details=None,
): ):
locations = [] locations = []
total_picked_qty = (
sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0
)
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
if has_batch_no and has_serial_no: if has_batch_no and has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item( locations = get_available_item_locations_for_serial_and_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty
) )
elif has_serial_no: elif has_serial_no:
locations = get_available_item_locations_for_serialized_item( locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty
) )
elif has_batch_no: elif has_batch_no:
locations = get_available_item_locations_for_batched_item( locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty
) )
else: else:
locations = get_available_item_locations_for_other_item( locations = get_available_item_locations_for_other_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty
) )
total_qty_available = sum(location.get("qty") for location in locations) total_qty_available = sum(location.get("qty") for location in locations)
remaining_qty = required_qty - total_qty_available remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation: if remaining_qty > 0 and not ignore_validation:
@ -469,25 +550,60 @@ def get_available_item_locations(
title=_("Insufficient Stock"), title=_("Insufficient Stock"),
) )
if picked_item_details:
for location in list(locations):
key = (
(location["warehouse"], location["batch_no"])
if location.get("batch_no")
else location["warehouse"]
)
if key in picked_item_details:
picked_detail = picked_item_details[key]
if picked_detail.get("serial_no") and location.get("serial_no"):
location["serial_no"] = list(
set(location["serial_no"]).difference(set(picked_detail["serial_no"]))
)
location["qty"] = len(location["serial_no"])
else:
location["qty"] -= picked_detail.get("picked_qty")
if location["qty"] < 1:
locations.remove(location)
total_qty_available = sum(location.get("qty") for location in locations)
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation:
frappe.msgprint(
_("{0} units of Item {1} is picked in another Pick List.").format(
remaining_qty, frappe.get_desk_link("Item", item_code)
),
title=_("Already Picked"),
)
return locations return locations
def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty=0
): ):
filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) sn = frappe.qb.DocType("Serial No")
query = (
frappe.qb.from_(sn)
.select(sn.name, sn.warehouse)
.where((sn.item_code == item_code) & (sn.company == company))
.orderby(sn.purchase_date)
.limit(cint(required_qty + total_picked_qty))
)
if from_warehouses: if from_warehouses:
filters.warehouse = ["in", from_warehouses] query = query.where(sn.warehouse.isin(from_warehouses))
else:
query = query.where(Coalesce(sn.warehouse, "") != "")
serial_nos = frappe.get_all( serial_nos = query.run(as_list=True)
"Serial No",
fields=["name", "warehouse"],
filters=filters,
limit=required_qty,
order_by="purchase_date",
as_list=1,
)
warehouse_serial_nos_map = frappe._dict() warehouse_serial_nos_map = frappe._dict()
for serial_no, warehouse in serial_nos: for serial_no, warehouse in serial_nos:
@ -501,94 +617,88 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item( def get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty=0
): ):
warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" sle = frappe.qb.DocType("Stock Ledger Entry")
batch_locations = frappe.db.sql( batch = frappe.qb.DocType("Batch")
"""
SELECT query = (
sle.`warehouse`, frappe.qb.from_(sle)
sle.`batch_no`, .from_(batch)
SUM(sle.`actual_qty`) AS `qty` .select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty"))
FROM .where(
`tabStock Ledger Entry` sle, `tabBatch` batch (sle.batch_no == batch.name)
WHERE & (sle.item_code == item_code)
sle.batch_no = batch.name & (sle.company == company)
and sle.`item_code`=%(item_code)s & (batch.disabled == 0)
and sle.`company` = %(company)s & (sle.is_cancelled == 0)
and batch.disabled = 0 & (IfNull(batch.expiry_date, "2200-01-01") > today())
and sle.is_cancelled=0 )
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s .groupby(sle.warehouse, sle.batch_no, sle.item_code)
{warehouse_condition} .having(Sum(sle.actual_qty) > 0)
GROUP BY .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
sle.`warehouse`, .limit(cint(required_qty + total_picked_qty))
sle.`batch_no`,
sle.`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`, sle.`batch_no`, sle.`warehouse`
""".format(
warehouse_condition=warehouse_condition
),
{ # nosec
"item_code": item_code,
"company": company,
"today": today(),
"warehouses": from_warehouses,
},
as_dict=1,
) )
return batch_locations if from_warehouses:
query = query.where(sle.warehouse.isin(from_warehouses))
return query.run(as_dict=True)
def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_serial_and_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company, total_picked_qty=0
): ):
# Get batch nos by FIFO # Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item( locations = get_available_item_locations_for_batched_item(
item_code, from_warehouses, required_qty, company item_code, from_warehouses, required_qty, company
) )
filters = frappe._dict( if locations:
{"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} sn = frappe.qb.DocType("Serial No")
) conditions = (sn.item_code == item_code) & (sn.company == company)
# Get Serial Nos by FIFO for Batch No for location in locations:
for location in locations: location.qty = (
filters.batch_no = location.batch_no required_qty if location.qty > required_qty else location.qty
filters.warehouse = location.warehouse ) # if extra qty in batch
location.qty = (
required_qty if location.qty > required_qty else location.qty
) # if extra qty in batch
serial_nos = frappe.get_list( serial_nos = (
"Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" frappe.qb.from_(sn)
) .select(sn.name)
.where(
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
)
.orderby(sn.purchase_date)
.limit(cint(location.qty + total_picked_qty))
).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos] serial_nos = [sn.name for sn in serial_nos]
location.serial_no = serial_nos location.serial_no = serial_nos
location.qty = len(serial_nos)
return locations return locations
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): def get_available_item_locations_for_other_item(
# gets all items available in different warehouses item_code, from_warehouses, required_qty, company, total_picked_qty=0
warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] ):
bin = frappe.qb.DocType("Bin")
filters = frappe._dict( query = (
{"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} frappe.qb.from_(bin)
.select(bin.warehouse, bin.actual_qty.as_("qty"))
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
.orderby(bin.creation)
.limit(cint(required_qty + total_picked_qty))
) )
if from_warehouses: if from_warehouses:
filters.warehouse = ["in", from_warehouses] query = query.where(bin.warehouse.isin(from_warehouses))
else:
wh = frappe.qb.DocType("Warehouse")
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
item_locations = frappe.get_all( item_locations = query.run(as_dict=True)
"Bin",
fields=["warehouse", "actual_qty as qty"],
filters=filters,
limit=required_qty,
order_by="creation",
)
return item_locations return item_locations

View File

@ -0,0 +1,14 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings['Pick List'] = {
get_indicator: function (doc) {
const status_colors = {
"Draft": "grey",
"Open": "orange",
"Completed": "green",
"Cancelled": "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};

View File

@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase):
pick_list.submit() pick_list.submit()
delivery_note = create_delivery_note(pick_list.name) delivery_note = create_delivery_note(pick_list.name)
pick_list.load_from_db()
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
@ -663,3 +664,147 @@ class TestPickList(FrappeTestCase):
self.assertEqual(dn.items[0].rate, 42) self.assertEqual(dn.items[0].rate, 42)
so.reload() so.reload()
self.assertEqual(so.per_delivered, 100) self.assertEqual(so.per_delivered, 100)
def test_pick_list_status(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(properties={"maintain_stock": 1}).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
so = make_sales_order(item_code=item, qty=10, rate=100)
pl = create_pick_list(so.name)
pl.save()
pl.reload()
self.assertEqual(pl.status, "Draft")
pl.submit()
pl.reload()
self.assertEqual(pl.status, "Open")
dn = create_delivery_note(pl.name)
dn.save()
pl.reload()
self.assertEqual(pl.status, "Open")
dn.submit()
pl.reload()
self.assertEqual(pl.status, "Completed")
dn.cancel()
pl.reload()
self.assertEqual(pl.status, "Completed")
pl.cancel()
pl.reload()
self.assertEqual(pl.status, "Cancelled")
def test_consider_existing_pick_list(self):
def create_items(items_properties):
items = []
for properties in items_properties:
properties.update({"maintain_stock": 1})
item_code = make_item(properties=properties).name
properties.update({"item_code": item_code})
items.append(properties)
return items
def create_stock_entries(items):
warehouses = ["Stores - _TC", "Finished Goods - _TC"]
for item in items:
for warehouse in warehouses:
se = make_stock_entry(
item=item.get("item_code"),
to_warehouse=warehouse,
qty=5,
)
def get_item_list(items, qty, warehouse="All Warehouses - _TC"):
return [
{
"item_code": item.get("item_code"),
"qty": qty,
"warehouse": warehouse,
}
for item in items
]
def get_picked_items_details(pick_list_doc):
items_data = {}
for location in pick_list_doc.locations:
key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None
data = {"picked_qty": location.picked_qty}
if serial_no:
data["serial_no"] = serial_no
if location.item_code not in items_data:
items_data[location.item_code] = {key: data}
else:
items_data[location.item_code][key] = data
return items_data
# Step - 1: Setup - Create Items and Stock Entries
items_properties = [
{
"valuation_rate": 100,
},
{
"valuation_rate": 200,
"has_batch_no": 1,
"create_new_batch": 1,
},
{
"valuation_rate": 300,
"has_serial_no": 1,
"serial_no_series": "SNO.###",
},
{
"valuation_rate": 400,
"has_batch_no": 1,
"create_new_batch": 1,
"has_serial_no": 1,
"serial_no_series": "SNO.###",
},
]
items = create_items(items_properties)
create_stock_entries(items)
# Step - 2: Create Sales Order [1]
so1 = make_sales_order(item_list=get_item_list(items, qty=6))
# Step - 3: Create and Submit Pick List [1] for Sales Order [1]
pl1 = create_pick_list(so1.name)
pl1.submit()
# Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1]
so2 = make_sales_order(item_list=get_item_list(items, qty=4))
# Step - 5: Create Pick List [2] for Sales Order [2]
pl2 = create_pick_list(so2.name)
pl2.save()
# Step - 6: Assert
picked_items_details = get_picked_items_details(pl1)
for location in pl2.locations:
key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse
item_data = picked_items_details.get(location.item_code, {}).get(key, {})
picked_qty = item_data.get("picked_qty", 0)
picked_serial_no = picked_items_details.get("serial_no", [])
bin_actual_qty = frappe.db.get_value(
"Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty"
)
# Available Qty to pick should be equal to [Actual Qty - Picked Qty]
self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty)
# Serial No should not be in the Picked Serial No list
if location.serial_no:
a = set(picked_serial_no)
b = set([x for x in location.serial_no.split("\n") if x])
self.assertSetEqual(b, b.difference(a))

View File

@ -888,7 +888,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty # Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0 total_amount, total_billed_amount = 0, 0
for item in pr_doc.items: for item in pr_doc.items:
return_data = frappe.db.get_list( return_data = frappe.get_all(
"Purchase Receipt", "Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[ filters=[

View File

@ -221,7 +221,7 @@ class QualityInspection(Document):
def item_query(doctype, txt, searchfield, start, page_len, filters): def item_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
from_doctype = cstr(filters.get("doctype")) from_doctype = cstr(filters.get("from"))
if not from_doctype or not frappe.db.exists("DocType", from_doctype): if not from_doctype or not frappe.db.exists("DocType", from_doctype):
return [] return []

View File

@ -169,6 +169,8 @@ frappe.ui.form.on('Stock Entry', {
}, },
refresh: function(frm) { refresh: function(frm) {
frm.trigger("get_items_from_transit_entry");
if(!frm.doc.docstatus) { if(!frm.doc.docstatus) {
frm.trigger('validate_purpose_consumption'); frm.trigger('validate_purpose_consumption');
frm.add_custom_button(__('Material Request'), function() { frm.add_custom_button(__('Material Request'), function() {
@ -337,6 +339,28 @@ frappe.ui.form.on('Stock Entry', {
} }
}, },
get_items_from_transit_entry: function(frm) {
if (frm.doc.docstatus===0) {
frm.add_custom_button(__('Transit Entry'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry",
source_doctype: "Stock Entry",
target: frm,
date_field: "posting_date",
setters: {
stock_entry_type: "Material Transfer",
purpose: "Material Transfer",
},
get_query_filters: {
docstatus: 1,
purpose: "Material Transfer",
add_to_transit: 1,
}
})
}, __("Get Items From"));
}
},
before_save: function(frm) { before_save: function(frm) {
frm.doc.items.forEach((item) => { frm.doc.items.forEach((item) => {
item.uom = item.uom || item.stock_uom; item.uom = item.uom || item.stock_uom;

View File

@ -158,6 +158,7 @@ class StockEntry(StockController):
self.validate_subcontract_order() self.validate_subcontract_order()
self.update_subcontract_order_supplied_items() self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status() self.update_subcontracting_order_status()
self.update_pick_list_status()
self.make_gl_entries() self.make_gl_entries()
@ -2276,6 +2277,11 @@ class StockEntry(StockController):
update_subcontracting_order_status(self.subcontracting_order) update_subcontracting_order_status(self.subcontracting_order)
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
update_pick_list_status(self.pick_list)
def set_missing_values(self): def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc." "Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty() self.set_transfer_qty()

View File

@ -117,6 +117,7 @@ def make_stock_entry(**args):
args.item = "_Test Item" args.item = "_Test Item"
s.company = args.company or erpnext.get_default_company() s.company = args.company or erpnext.get_default_company()
s.add_to_transit = args.add_to_transit or 0
s.purchase_receipt_no = args.purchase_receipt_no s.purchase_receipt_no = args.purchase_receipt_no
s.delivery_note_no = args.delivery_note_no s.delivery_note_no = args.delivery_note_no
s.sales_invoice_no = args.sales_invoice_no s.sales_invoice_no = args.sales_invoice_no

View File

@ -17,6 +17,7 @@ from erpnext.stock.doctype.item.test_item import (
from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.serial_no.serial_no import * # noqa
from erpnext.stock.doctype.stock_entry.stock_entry import ( from erpnext.stock.doctype.stock_entry.stock_entry import (
FinishedGoodError, FinishedGoodError,
make_stock_in_entry,
move_sample_to_retention_warehouse, move_sample_to_retention_warehouse,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -160,6 +161,53 @@ class TestStockEntry(FrappeTestCase):
self.assertTrue(item_code in items) self.assertTrue(item_code in items)
def test_add_to_transit_entry(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item_code = "_Test Transit Item"
company = "_Test Company"
create_warehouse("Test From Warehouse")
create_warehouse("Test Transit Warehouse")
create_warehouse("Test To Warehouse")
create_item(
item_code=item_code,
is_stock_item=1,
is_purchase_item=1,
company=company,
)
# create inward stock entry
make_stock_entry(
item_code=item_code,
target="Test From Warehouse - _TC",
qty=10,
basic_rate=100,
expense_account="Stock Adjustment - _TC",
cost_center="Main - _TC",
)
transit_entry = make_stock_entry(
item_code=item_code,
source="Test From Warehouse - _TC",
target="Test Transit Warehouse - _TC",
add_to_transit=1,
stock_entry_type="Material Transfer",
purpose="Material Transfer",
qty=10,
basic_rate=100,
expense_account="Stock Adjustment - _TC",
cost_center="Main - _TC",
)
end_transit_entry = make_stock_in_entry(transit_entry.name)
self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry)
self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)
# create add to transit
def test_material_receipt_gl_entry(self): def test_material_receipt_gl_entry(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

View File

@ -236,8 +236,10 @@ def validate_item_details(args, item):
validate_end_of_life(item.name, item.end_of_life, item.disabled) validate_end_of_life(item.name, item.end_of_life, item.disabled)
if args.transaction_type == "selling" and cint(item.has_variants): if cint(item.has_variants):
throw(_("Item {0} is a template, please select one of its variants").format(item.name)) msg = f"Item {item.name} is a template, please select one of its variants"
throw(_(msg), title=_("Template Item Selected"))
elif args.transaction_type == "buying" and args.doctype != "Material Request": elif args.transaction_type == "buying" and args.doctype != "Material Request":
if args.get("is_subcontracted"): if args.get("is_subcontracted"):

View File

@ -62,7 +62,7 @@ def execute(filters=None):
continue continue
total_stock_value = sum(item_value[(item, item_group)]) total_stock_value = sum(item_value[(item, item_group)])
row = [item, item_group, total_stock_value] row = [item, item_map[item]["item_name"], item_group, total_stock_value]
fifo_queue = item_ageing[item]["fifo_queue"] fifo_queue = item_ageing[item]["fifo_queue"]
average_age = 0.00 average_age = 0.00
@ -89,10 +89,11 @@ def get_columns(filters):
"""return columns""" """return columns"""
columns = [ columns = [
_("Item") + ":Link/Item:180", _("Item") + ":Link/Item:150",
_("Item Group") + "::100", _("Item Name") + ":Link/Item:150",
_("Item Group") + "::120",
_("Value") + ":Currency:120", _("Value") + ":Currency:120",
_("Age") + ":Float:80", _("Age") + ":Float:120",
] ]
return columns return columns
@ -123,7 +124,7 @@ def get_warehouse_list(filters):
def add_warehouse_column(columns, warehouse_list): def add_warehouse_column(columns, warehouse_list):
if len(warehouse_list) > 1: if len(warehouse_list) > 1:
columns += [_("Total Qty") + ":Int:90"] columns += [_("Total Qty") + ":Int:120"]
for wh in warehouse_list: for wh in warehouse_list:
columns += [_(wh.name) + ":Int:120"] columns += [_(wh.name) + ":Int:100"]

View File

@ -1179,7 +1179,7 @@ def get_stock_ledger_entries(
def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
return frappe.db.get_value( return frappe.db.get_value(
"Stock Ledger Entry", "Stock Ledger Entry",
{"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
[ [
"item_code", "item_code",
"warehouse", "warehouse",

View File

@ -11,6 +11,7 @@ frappe.listview_settings['Subcontracting Order'] = {
"Partial Material Transferred": "purple", "Partial Material Transferred": "purple",
"Material Transferred": "blue", "Material Transferred": "blue",
"Closed": "red", "Closed": "red",
"Cancelled": "red",
}; };
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
}, },

View File

@ -1,352 +1,353 @@
{ {
"actions": [], "actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2022-04-01 19:26:31.475015", "creation": "2022-04-01 19:26:31.475015",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"item_code", "item_code",
"item_name", "item_name",
"bom", "bom",
"include_exploded_items", "include_exploded_items",
"column_break_3", "column_break_3",
"schedule_date", "schedule_date",
"expected_delivery_date", "expected_delivery_date",
"description_section", "description_section",
"description", "description",
"column_break_8", "column_break_8",
"image", "image",
"image_view", "image_view",
"quantity_and_rate_section", "quantity_and_rate_section",
"qty", "qty",
"received_qty", "received_qty",
"returned_qty", "returned_qty",
"column_break_13", "column_break_13",
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"section_break_16", "section_break_16",
"rate", "rate",
"amount", "amount",
"column_break_19", "column_break_19",
"rm_cost_per_qty", "rm_cost_per_qty",
"service_cost_per_qty", "service_cost_per_qty",
"additional_cost_per_qty", "additional_cost_per_qty",
"warehouse_section", "warehouse_section",
"warehouse", "warehouse",
"accounting_details_section", "accounting_details_section",
"expense_account", "expense_account",
"manufacture_section", "manufacture_section",
"manufacturer", "manufacturer",
"manufacturer_part_no", "manufacturer_part_no",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "project",
"section_break_34", "section_break_34",
"page_break" "page_break"
], ],
"fields": [ "fields": [
{ {
"bold": 1, "bold": 1,
"columns": 2, "columns": 2,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Item Code", "label": "Item Code",
"options": "Item", "options": "Item",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
{ {
"fetch_from": "item_code.item_name", "fetch_from": "item_code.item_name",
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Item Name", "label": "Item Name",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"bold": 1, "bold": 1,
"columns": 2, "columns": 2,
"fieldname": "schedule_date", "fieldname": "schedule_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Required By", "label": "Required By",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 1, "bold": 1,
"fieldname": "expected_delivery_date", "fieldname": "expected_delivery_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Delivery Date", "label": "Expected Delivery Date",
"search_index": 1 "search_index": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "description_section", "fieldname": "description_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Description" "label": "Description"
}, },
{ {
"fetch_from": "item_code.description", "fetch_from": "item_code.description",
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Description", "label": "Description",
"print_width": "300px", "print_width": "300px",
"reqd": 1, "reqd": 1,
"width": "300px" "width": "300px"
}, },
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach", "fieldtype": "Attach",
"hidden": 1, "hidden": 1,
"label": "Image" "label": "Image"
}, },
{ {
"fieldname": "image_view", "fieldname": "image_view",
"fieldtype": "Image", "fieldtype": "Image",
"label": "Image View", "label": "Image View",
"options": "image", "options": "image",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "quantity_and_rate_section", "fieldname": "quantity_and_rate_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Quantity and Rate" "label": "Quantity and Rate"
}, },
{ {
"bold": 1, "bold": 1,
"columns": 1, "columns": 1,
"default": "1", "default": "1",
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Quantity", "label": "Quantity",
"print_width": "60px", "print_width": "60px",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"width": "60px" "width": "60px"
}, },
{ {
"fieldname": "column_break_13", "fieldname": "column_break_13",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock UOM", "label": "Stock UOM",
"options": "UOM", "options": "UOM",
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"reqd": 1, "reqd": 1,
"width": "100px" "width": "100px"
}, },
{ {
"default": "1", "default": "1",
"fieldname": "conversion_factor", "fieldname": "conversion_factor",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 1, "hidden": 1,
"label": "Conversion Factor", "label": "Conversion Factor",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "section_break_16", "fieldname": "section_break_16",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"bold": 1, "bold": 1,
"columns": 2, "columns": 2,
"fetch_from": "item_code.standard_rate", "fetch_from": "item_code.standard_rate",
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate", "label": "Rate",
"options": "currency", "options": "currency",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 2, "columns": 2,
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Amount", "label": "Amount",
"options": "currency", "options": "currency",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "warehouse_section", "fieldname": "warehouse_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Warehouse Details" "label": "Warehouse Details"
}, },
{ {
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Warehouse", "label": "Warehouse",
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_details_section", "fieldname": "accounting_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Details" "label": "Accounting Details"
}, },
{ {
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Account", "label": "Expense Account",
"options": "Account", "options": "Account",
"print_hide": 1 "print_hide": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "manufacture_section", "fieldname": "manufacture_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Manufacture" "label": "Manufacture"
}, },
{ {
"fieldname": "manufacturer", "fieldname": "manufacturer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Manufacturer", "label": "Manufacturer",
"options": "Manufacturer" "options": "Manufacturer"
}, },
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number" "label": "Manufacturer Part Number"
}, },
{ {
"depends_on": "item_code", "depends_on": "item_code",
"fetch_from": "item_code.default_bom", "fetch_from": "item_code.default_bom",
"fieldname": "bom", "fetch_if_empty": 1,
"fieldtype": "Link", "fieldname": "bom",
"in_list_view": 1, "fieldtype": "Link",
"label": "BOM", "in_list_view": 1,
"options": "BOM", "label": "BOM",
"print_hide": 1, "options": "BOM",
"reqd": 1 "print_hide": 1,
}, "reqd": 1
{ },
"default": "0", {
"fieldname": "include_exploded_items", "default": "0",
"fieldtype": "Check", "fieldname": "include_exploded_items",
"label": "Include Exploded Items", "fieldtype": "Check",
"print_hide": 1 "label": "Include Exploded Items",
}, "print_hide": 1
{ },
"fieldname": "service_cost_per_qty", {
"fieldtype": "Currency", "fieldname": "service_cost_per_qty",
"label": "Service Cost Per Qty", "fieldtype": "Currency",
"read_only": 1, "label": "Service Cost Per Qty",
"reqd": 1 "read_only": 1,
}, "reqd": 1
{ },
"default": "0", {
"fieldname": "additional_cost_per_qty", "default": "0",
"fieldtype": "Currency", "fieldname": "additional_cost_per_qty",
"label": "Additional Cost Per Qty", "fieldtype": "Currency",
"read_only": 1 "label": "Additional Cost Per Qty",
}, "read_only": 1
{ },
"fieldname": "rm_cost_per_qty", {
"fieldtype": "Currency", "fieldname": "rm_cost_per_qty",
"label": "Raw Material Cost Per Qty", "fieldtype": "Currency",
"no_copy": 1, "label": "Raw Material Cost Per Qty",
"read_only": 1 "no_copy": 1,
}, "read_only": 1
{ },
"allow_on_submit": 1, {
"default": "0", "allow_on_submit": 1,
"fieldname": "page_break", "default": "0",
"fieldtype": "Check", "fieldname": "page_break",
"label": "Page Break", "fieldtype": "Check",
"no_copy": 1, "label": "Page Break",
"print_hide": 1 "no_copy": 1,
}, "print_hide": 1
{ },
"fieldname": "section_break_34", {
"fieldtype": "Section Break" "fieldname": "section_break_34",
}, "fieldtype": "Section Break"
{ },
"depends_on": "received_qty", {
"fieldname": "received_qty", "depends_on": "received_qty",
"fieldtype": "Float", "fieldname": "received_qty",
"label": "Received Qty", "fieldtype": "Float",
"no_copy": 1, "label": "Received Qty",
"print_hide": 1, "no_copy": 1,
"read_only": 1 "print_hide": 1,
}, "read_only": 1
{ },
"depends_on": "returned_qty", {
"fieldname": "returned_qty", "depends_on": "returned_qty",
"fieldtype": "Float", "fieldname": "returned_qty",
"label": "Returned Qty", "fieldtype": "Float",
"no_copy": 1, "label": "Returned Qty",
"print_hide": 1, "no_copy": 1,
"read_only": 1 "print_hide": 1,
}, "read_only": 1
{ },
"collapsible": 1, {
"fieldname": "accounting_dimensions_section", "collapsible": 1,
"fieldtype": "Section Break", "fieldname": "accounting_dimensions_section",
"label": "Accounting Dimensions" "fieldtype": "Section Break",
}, "label": "Accounting Dimensions"
{ },
"fieldname": "cost_center", {
"fieldtype": "Link", "fieldname": "cost_center",
"label": "Cost Center", "fieldtype": "Link",
"options": "Cost Center" "label": "Cost Center",
}, "options": "Cost Center"
{ },
"fieldname": "dimension_col_break", {
"fieldtype": "Column Break" "fieldname": "dimension_col_break",
}, "fieldtype": "Column Break"
{ },
"fieldname": "project", {
"fieldtype": "Link", "fieldname": "project",
"label": "Project", "fieldtype": "Link",
"options": "Project" "label": "Project",
} "options": "Project"
], }
"idx": 1, ],
"index_web_pages_for_search": 1, "idx": 1,
"istable": 1, "index_web_pages_for_search": 1,
"links": [], "istable": 1,
"modified": "2022-08-15 14:25:45.177703", "links": [],
"modified_by": "Administrator", "modified": "2023-01-20 23:25:45.363281",
"module": "Subcontracting", "modified_by": "Administrator",
"name": "Subcontracting Order Item", "module": "Subcontracting",
"naming_rule": "Random", "name": "Subcontracting Order Item",
"owner": "Administrator", "naming_rule": "Random",
"permissions": [], "owner": "Administrator",
"quick_entry": 1, "permissions": [],
"search_fields": "item_name", "quick_entry": 1,
"sort_field": "modified", "search_fields": "item_name",
"sort_order": "DESC", "sort_field": "modified",
"states": [], "sort_order": "DESC",
"track_changes": 1 "states": [],
"track_changes": 1
} }

View File

@ -262,15 +262,17 @@ class SubcontractingReceipt(SubcontractingController):
def get_gl_entries(self, warehouse_account=None): def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
if not erpnext.is_perpetual_inventory_enabled(self.company):
return []
gl_entries = [] gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account) self.make_item_gl_entries(gl_entries, warehouse_account)
return process_gl_map(gl_entries) return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None): def make_item_gl_entries(self, gl_entries, warehouse_account=None):
if erpnext.is_perpetual_inventory_enabled(self.company): stock_rbnb = self.get_company_default("stock_received_but_not_billed")
stock_rbnb = self.get_company_default("stock_received_but_not_billed") expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
warehouse_with_no_account = [] warehouse_with_no_account = []

View File

@ -38,22 +38,27 @@ def get_context(context):
if not frappe.has_website_permission(context.doc): if not frappe.has_website_permission(context.doc):
frappe.throw(_("Not Permitted"), frappe.PermissionError) frappe.throw(_("Not Permitted"), frappe.PermissionError)
# check for the loyalty program of the customer context.available_loyalty_points = 0.0
customer_loyalty_program = frappe.db.get_value( if context.doc.get("customer"):
"Customer", context.doc.customer, "loyalty_program" # check for the loyalty program of the customer
) customer_loyalty_program = frappe.db.get_value(
if customer_loyalty_program: "Customer", context.doc.customer, "loyalty_program"
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
) )
loyalty_program_details = get_loyalty_program_details_with_points( if customer_loyalty_program:
context.doc.customer, customer_loyalty_program from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
) get_loyalty_program_details_with_points,
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) )
# show Make Purchase Invoice button based on permission loyalty_program_details = get_loyalty_program_details_with_points(
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create") context.doc.customer, customer_loyalty_program
)
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
context.show_make_pi_button = False
if context.doc.get("supplier"):
# show Make Purchase Invoice button based on permission
context.show_make_pi_button = frappe.has_permission("Purchase Invoice", "create")
def get_attachments(dt, dn): def get_attachments(dt, dn):

View File

@ -3537,7 +3537,7 @@ Quality Feedback Template,Kwaliteit-terugvoersjabloon,
Rules for applying different promotional schemes.,Reëls vir die toepassing van verskillende promosieskemas., Rules for applying different promotional schemes.,Reëls vir die toepassing van verskillende promosieskemas.,
Shift,verskuiwing, Shift,verskuiwing,
Show {0},Wys {0}, Show {0},Wys {0},
"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Spesiale karakters behalwe &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{&quot; En &quot;}&quot; word nie toegelaat in die naamreekse nie", "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Spesiale karakters behalwe &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; En &quot;}}&quot; word nie toegelaat in die naamreekse nie {0}",
Target Details,Teikenbesonderhede, Target Details,Teikenbesonderhede,
{0} already has a Parent Procedure {1}.,{0} het reeds &#39;n ouerprosedure {1}., {0} already has a Parent Procedure {1}.,{0} het reeds &#39;n ouerprosedure {1}.,
API,API, API,API,

Can't render this file because it is too large.

View File

@ -3537,7 +3537,7 @@ Quality Feedback Template,የጥራት ግብረ መልስ አብነት።,
Rules for applying different promotional schemes.,የተለያዩ የማስተዋወቂያ ዘዴዎችን ለመተግበር ህጎች።, Rules for applying different promotional schemes.,የተለያዩ የማስተዋወቂያ ዘዴዎችን ለመተግበር ህጎች።,
Shift,ቀይር, Shift,ቀይር,
Show {0},አሳይ {0}, Show {0},አሳይ {0},
"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",ከ &quot;-&quot; ፣ &quot;#&quot; ፣ &quot;፣&quot; ፣ &quot;/&quot; ፣ &quot;{&quot; እና &quot;}&quot; በስተቀር ልዩ ቁምፊዎች ከመለያ መሰየሚያ አይፈቀድም, "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",ከ &quot;-&quot; ፣ &quot;#&quot; ፣ &quot;፣&quot; ፣ &quot;/&quot; ፣ &quot;{{&quot; እና &quot;}}&quot; በስተቀር ልዩ ቁምፊዎች ከመለያ መሰየሚያ አይፈቀድም {0},
Target Details,የ Detailsላማ ዝርዝሮች።, Target Details,የ Detailsላማ ዝርዝሮች።,
{0} already has a Parent Procedure {1}.,{0} ቀድሞውኑ የወላጅ አሰራር ሂደት አለው {1}።, {0} already has a Parent Procedure {1}.,{0} ቀድሞውኑ የወላጅ አሰራር ሂደት አለው {1}።,
API,ኤ ፒ አይ, API,ኤ ፒ አይ,

Can't render this file because it is too large.

View File

@ -3537,7 +3537,7 @@ Quality Feedback Template,قالب ملاحظات الجودة,
Rules for applying different promotional schemes.,قواعد تطبيق المخططات الترويجية المختلفة., Rules for applying different promotional schemes.,قواعد تطبيق المخططات الترويجية المختلفة.,
Shift,تحول, Shift,تحول,
Show {0},عرض {0}, Show {0},عرض {0},
"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series",الأحرف الخاصة باستثناء &quot;-&quot; ، &quot;#&quot; ، &quot;.&quot; ، &quot;/&quot; ، &quot;{&quot; و &quot;}&quot; غير مسموح في سلسلة التسمية, "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}",{0} الأحرف الخاصة باستثناء &quot;-&quot; ، &quot;#&quot; ، &quot;.&quot; ، &quot;/&quot; ، &quot;{{&quot; و &quot;}}&quot; غير مسموح في سلسلة التسمية,
Target Details,تفاصيل الهدف, Target Details,تفاصيل الهدف,
{0} already has a Parent Procedure {1}.,{0} يحتوي بالفعل على إجراء الأصل {1}., {0} already has a Parent Procedure {1}.,{0} يحتوي بالفعل على إجراء الأصل {1}.,
API,API, API,API,

Can't render this file because it is too large.

View File

@ -3537,7 +3537,7 @@ Quality Feedback Template,Качествен обратен шаблон,
Rules for applying different promotional schemes.,Правила за прилагане на различни промоционални схеми., Rules for applying different promotional schemes.,Правила за прилагане на различни промоционални схеми.,
Shift,изместване, Shift,изместване,
Show {0},Показване на {0}, Show {0},Показване на {0},
"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специални символи, с изключение на &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{&quot; И &quot;}&quot; не са позволени в именуването на серии", "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Специални символи, с изключение на &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; И &quot;}}&quot; не са позволени в именуването на серии {0}",
Target Details,Детайли за целта, Target Details,Детайли за целта,
{0} already has a Parent Procedure {1}.,{0} вече има родителска процедура {1}., {0} already has a Parent Procedure {1}.,{0} вече има родителска процедура {1}.,
API,API, API,API,

Can't render this file because it is too large.

View File

@ -3537,7 +3537,7 @@ Quality Feedback Template,গুণমান প্রতিক্রিয়
Rules for applying different promotional schemes.,বিভিন্ন প্রচারমূলক স্কিম প্রয়োগ করার নিয়ম।, Rules for applying different promotional schemes.,বিভিন্ন প্রচারমূলক স্কিম প্রয়োগ করার নিয়ম।,
Shift,পরিবর্তন, Shift,পরিবর্তন,
Show {0},{0} দেখান, Show {0},{0} দেখান,
"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","নামকরণ সিরিজে &quot;-&quot;, &quot;#&quot;, &quot;।&quot;, &quot;/&quot;, &quot;{&quot; এবং &quot;}&quot; ব্যতীত বিশেষ অক্ষর অনুমোদিত নয়", "Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","নামকরণ সিরিজে &quot;-&quot;, &quot;#&quot;, &quot;।&quot;, &quot;/&quot;, &quot;{{&quot; এবং &quot;}}&quot; ব্যতীত বিশেষ অক্ষর অনুমোদিত নয় {0}",
Target Details,টার্গেটের বিশদ, Target Details,টার্গেটের বিশদ,
{0} already has a Parent Procedure {1}.,{0} ইতিমধ্যে একটি মূল পদ্ধতি আছে {1}।, {0} already has a Parent Procedure {1}.,{0} ইতিমধ্যে একটি মূল পদ্ধতি আছে {1}।,
API,এপিআই, API,এপিআই,

Can't render this file because it is too large.

Some files were not shown because too many files have changed in this diff Show More