mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-25 20:08:34 +00:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
104967881d | ||
|
|
b2ef5f7cd2 | ||
|
|
cd5b8aeb64 | ||
|
|
0003e0cb5b | ||
|
|
b3ccad8cf4 | ||
|
|
84ef814f45 | ||
|
|
ac432ea52f | ||
|
|
635d80dc2f | ||
|
|
0c4de03baa | ||
|
|
a716f780a8 | ||
|
|
6f6598878e | ||
|
|
4ad9aa29ee | ||
|
|
e54b23d71b | ||
|
|
ba9220f9d2 | ||
|
|
354f258543 | ||
|
|
f7d3f136c5 | ||
|
|
ff594831b0 | ||
|
|
b08d36cb7b | ||
|
|
cdb6a4e3f7 | ||
|
|
99188afa16 | ||
|
|
64fda0d4b7 | ||
|
|
0c6a7cef95 | ||
|
|
c930d64e8d | ||
|
|
78e64fa486 | ||
|
|
2a0d7acc2b | ||
|
|
24a3da55e6 | ||
|
|
c3289c265a | ||
|
|
b39f367dbd | ||
|
|
d76e291927 | ||
|
|
ce5419888b | ||
|
|
ffef0cb771 | ||
|
|
2de239c73b | ||
|
|
11978fca4e | ||
|
|
d38a2895b0 | ||
|
|
4492ee4771 | ||
|
|
eca2d96419 | ||
|
|
b66976bd36 | ||
|
|
776c8c6d6a | ||
|
|
452db7ed15 | ||
|
|
10cfcb5e8a | ||
|
|
b91860ddf2 | ||
|
|
eda89a467c | ||
|
|
2121714856 | ||
|
|
8b46da39ea | ||
|
|
6da7b38f6e | ||
|
|
7688239fc0 | ||
|
|
07c25ace5c | ||
|
|
6ae21c92de | ||
|
|
7af45972ae | ||
|
|
f106b9e884 | ||
|
|
b839c53572 | ||
|
|
30b42b22ab | ||
|
|
4834b78ed2 | ||
|
|
72f9308df6 | ||
|
|
dc9b81d216 | ||
|
|
61b20c81c2 | ||
|
|
58f3f2b6d9 | ||
|
|
e3435369a0 | ||
|
|
726c3d32be | ||
|
|
ecdd8493ea | ||
|
|
431a94b961 | ||
|
|
00f990e3f4 | ||
|
|
0070b5ef9a | ||
|
|
ab31eb4ee7 | ||
|
|
357ae939d1 | ||
|
|
b8aa63b5e2 | ||
|
|
9191c54b90 | ||
|
|
30b5d1c400 | ||
|
|
9d5c4ffadf | ||
|
|
85d8540379 | ||
|
|
4ad3e28147 | ||
|
|
ec6a01de4c | ||
|
|
7831744064 | ||
|
|
3aba14f71a | ||
|
|
f3f6d35a84 | ||
|
|
1cf5b23d9c | ||
|
|
de3453e477 | ||
|
|
ee015273c9 | ||
|
|
db3f8d2768 | ||
|
|
e1ecc9a819 | ||
|
|
9e340a8307 | ||
|
|
cbc8b2da0a | ||
|
|
b7ff5fb426 | ||
|
|
2b2a4f66ad | ||
|
|
78ca078474 | ||
|
|
65c9941aa4 | ||
|
|
5e46567a0b | ||
|
|
079cf6899e | ||
|
|
ad648f313c | ||
|
|
9c9c6607f8 | ||
|
|
d6901e51ad | ||
|
|
8733ce5ca9 | ||
|
|
1ee5c2ff87 | ||
|
|
09ef8eeaea | ||
|
|
40bb2cba93 | ||
|
|
95f81d3563 | ||
|
|
88ce59a1ca | ||
|
|
e93ac3c9a8 | ||
|
|
079fc2dc34 | ||
|
|
bd2242b285 | ||
|
|
79d508f4ff | ||
|
|
a0cab4c5e7 | ||
|
|
a19031cf9a | ||
|
|
e9e5ded36b | ||
|
|
fcb6ee22ba | ||
|
|
f09e4273d9 | ||
|
|
4a177113c6 | ||
|
|
972893d00e | ||
|
|
1ed0b6e89b | ||
|
|
dafb575500 | ||
|
|
9cf5e9e61e | ||
|
|
af8b99a5e0 | ||
|
|
a16347f325 | ||
|
|
19b9875ba8 | ||
|
|
43aa670e90 | ||
|
|
4f76ed1c68 | ||
|
|
b93d13feef | ||
|
|
e1a32cc620 | ||
|
|
550f5f280c | ||
|
|
fbbfeb8563 | ||
|
|
418c131331 | ||
|
|
a8bf17e560 | ||
|
|
9961037f71 | ||
|
|
84ee1b86af | ||
|
|
afe86d83aa | ||
|
|
76e4bb44f1 | ||
|
|
c1bc1040b8 | ||
|
|
1d23c9a9fd | ||
|
|
5a211813d3 | ||
|
|
601b1e3821 | ||
|
|
6d1ae68d70 | ||
|
|
19ebebdc47 | ||
|
|
a91483899b | ||
|
|
2f6d55d8d2 | ||
|
|
4a10454d89 | ||
|
|
12b15347bc | ||
|
|
9b63a1a2e9 | ||
|
|
8888957952 | ||
|
|
a9660bf026 | ||
|
|
b877b0d3a2 | ||
|
|
e4082093b8 | ||
|
|
9dc0edfb8a | ||
|
|
e13f05ce19 | ||
|
|
310e7c522c | ||
|
|
e32e0bc8fa | ||
|
|
0c24ba0118 | ||
|
|
60fa87751a | ||
|
|
50bc7785f7 | ||
|
|
d2aea0cd2e | ||
|
|
e9006582fb | ||
|
|
3483e0e2a5 | ||
|
|
65b047c94c | ||
|
|
ada3ce21c0 | ||
|
|
cb89dba5ab | ||
|
|
d73237e896 | ||
|
|
8e0d393cd4 | ||
|
|
292ff1bd6b | ||
|
|
804cf40f69 | ||
|
|
1d31ab1ab7 | ||
|
|
dd493fb46f | ||
|
|
85f48dd6bd | ||
|
|
fa0a137a38 | ||
|
|
5fc8d20e96 | ||
|
|
7ddcfc00fc | ||
|
|
3f02059085 | ||
|
|
71b21086b1 | ||
|
|
c2817bed0b | ||
|
|
dc972718d5 | ||
|
|
7afb19f965 | ||
|
|
0ca137808b | ||
|
|
99e0106193 | ||
|
|
447485ae90 | ||
|
|
98bcb7255d | ||
|
|
2df24ec459 | ||
|
|
3a2f08fbda | ||
|
|
7c9b535514 | ||
|
|
4f0c2909ab | ||
|
|
1741501ea8 | ||
|
|
ea032893d3 | ||
|
|
7601a016b8 | ||
|
|
b5376ce5cb |
@@ -16,8 +16,8 @@ repos:
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.40.2"
|
||||
__version__ = "13.42.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
frappe.ui.form.on('Accounting Dimension Filter', {
|
||||
refresh: function(frm, cdt, cdn) {
|
||||
if (frm.doc.accounting_dimension) {
|
||||
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
|
||||
}
|
||||
|
||||
let help_content =
|
||||
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
<tr><td>
|
||||
@@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
|
||||
frm.clear_table("dimensions");
|
||||
let row = frm.add_child("dimensions");
|
||||
row.accounting_dimension = frm.doc.accounting_dimension;
|
||||
frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
|
||||
frm.refresh_field("dimensions");
|
||||
frm.trigger('setup_filters');
|
||||
},
|
||||
|
||||
@@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', {
|
||||
|
||||
reference_docname: function(frm) {
|
||||
if (frm.doc.reference_docname && frm.doc.reference_doctype) {
|
||||
let fields_to_fetch = ["grand_total"];
|
||||
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
|
||||
|
||||
if (frm.doc.reference_doctype == "Sales Order") {
|
||||
fields_to_fetch.push("project");
|
||||
}
|
||||
|
||||
fields_to_fetch.push(party_field);
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials",
|
||||
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details",
|
||||
args: {
|
||||
"column_list": fields_to_fetch,
|
||||
"doctype": frm.doc.reference_doctype,
|
||||
"docname": frm.doc.reference_docname
|
||||
"bank_guarantee_type": frm.doc.bg_type,
|
||||
"reference_name": frm.doc.reference_docname
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.search import sanitize_searchfield
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -25,14 +22,18 @@ class BankGuarantee(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_vouchar_detials(column_list, doctype, docname):
|
||||
column_list = json.loads(column_list)
|
||||
for col in column_list:
|
||||
sanitize_searchfield(col)
|
||||
return frappe.db.sql(
|
||||
""" select {columns} from `tab{doctype}` where name=%s""".format(
|
||||
columns=", ".join(column_list), doctype=doctype
|
||||
),
|
||||
docname,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
def get_voucher_details(bank_guarantee_type: str, reference_name: str):
|
||||
if not isinstance(reference_name, str):
|
||||
raise TypeError("reference_name must be a string")
|
||||
|
||||
fields_to_fetch = ["grand_total"]
|
||||
|
||||
if bank_guarantee_type == "Receiving":
|
||||
doctype = "Sales Order"
|
||||
fields_to_fetch.append("customer")
|
||||
fields_to_fetch.append("project")
|
||||
else:
|
||||
doctype = "Purchase Order"
|
||||
fields_to_fetch.append("supplier")
|
||||
|
||||
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)
|
||||
|
||||
@@ -169,5 +169,6 @@ def auto_create_fiscal_year():
|
||||
|
||||
|
||||
def get_from_and_to_date(fiscal_year):
|
||||
fields = ["year_start_date as from_date", "year_end_date as to_date"]
|
||||
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
|
||||
fields = ["year_start_date", "year_end_date"]
|
||||
cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
|
||||
return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date)
|
||||
|
||||
@@ -45,21 +45,6 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
frm.trigger("clear_child");
|
||||
switch(frm.doc.voucher_type){
|
||||
case "Opening Entry":
|
||||
frm.set_value("is_opening", "Yes");
|
||||
frappe.call({
|
||||
type:"GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
|
||||
args: {
|
||||
"company": frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
add_accounts(frm.doc, r.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "Bank Entry":
|
||||
case "Cash Entry":
|
||||
frappe.call({
|
||||
|
||||
@@ -20,7 +20,6 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
frm.dashboard.reset();
|
||||
frm.doc.import_in_progress = true;
|
||||
}
|
||||
if (data.user != frappe.session.user) return;
|
||||
if (data.count == data.total) {
|
||||
setTimeout((title) => {
|
||||
frm.doc.import_in_progress = false;
|
||||
|
||||
@@ -271,10 +271,10 @@ def publish(index, total, doctype):
|
||||
dict(
|
||||
title=_("Opening Invoice Creation In Progress"),
|
||||
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
|
||||
user=frappe.session.user,
|
||||
count=index + 1,
|
||||
total=total,
|
||||
),
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_missing_values()
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.validate_bank_accounts()
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
@@ -250,23 +249,6 @@ class PaymentEntry(AccountsController):
|
||||
if not frappe.db.exists(self.party_type, self.party):
|
||||
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
|
||||
|
||||
if self.party_account and self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_account_type(
|
||||
self.party_account, [erpnext.get_party_account_type(self.party_type)]
|
||||
)
|
||||
|
||||
def validate_bank_accounts(self):
|
||||
if self.payment_type in ("Pay", "Internal Transfer"):
|
||||
self.validate_account_type(self.paid_from, ["Bank", "Cash"])
|
||||
|
||||
if self.payment_type in ("Receive", "Internal Transfer"):
|
||||
self.validate_account_type(self.paid_to, ["Bank", "Cash"])
|
||||
|
||||
def validate_account_type(self, account, account_types):
|
||||
account_type = frappe.db.get_value("Account", account, "account_type")
|
||||
# if account_type not in account_types:
|
||||
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||
|
||||
def set_exchange_rate(self, ref_doc=None):
|
||||
self.set_source_exchange_rate(ref_doc)
|
||||
self.set_target_exchange_rate(ref_doc)
|
||||
|
||||
@@ -47,6 +47,10 @@ class PaymentReconciliation(Document):
|
||||
def get_payment_entries(self):
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@@ -61,6 +65,10 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += " and t2.cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -113,6 +121,10 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
condition = self.get_conditions(get_return_invoices=True)
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += " and doc.cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -172,6 +184,9 @@ class PaymentReconciliation(Document):
|
||||
|
||||
condition = self.get_conditions(get_invoices=True)
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type, self.party, self.receivable_payable_account, condition=condition
|
||||
)
|
||||
@@ -357,9 +372,6 @@ class PaymentReconciliation(Document):
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
|
||||
|
||||
@@ -25,7 +25,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
|
||||
frappe.realtime.on('closing_process_complete', async function(data) {
|
||||
await frm.reload_doc();
|
||||
if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) {
|
||||
if (frm.doc.status == 'Failed' && frm.doc.error_message) {
|
||||
frappe.msgprint({
|
||||
title: __('POS Closing Failed'),
|
||||
message: frm.doc.error_message,
|
||||
|
||||
@@ -432,7 +432,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
|
||||
finally:
|
||||
frappe.db.commit()
|
||||
frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
|
||||
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
|
||||
|
||||
|
||||
def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
@@ -459,7 +459,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
|
||||
finally:
|
||||
frappe.db.commit()
|
||||
frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
|
||||
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
|
||||
|
||||
|
||||
def enqueue_job(job, **kwargs):
|
||||
|
||||
@@ -712,6 +712,140 @@ class TestPricingRule(unittest.TestCase):
|
||||
|
||||
item.delete()
|
||||
|
||||
def test_item_group_price_with_blank_uom_pricing_rule(self):
|
||||
group = frappe.get_doc(
|
||||
doctype="Item Group",
|
||||
item_group_name="_Test Pricing Rule Item Group",
|
||||
parent_item_group="All Item Groups",
|
||||
)
|
||||
group.save()
|
||||
properties = {
|
||||
"item_code": "Item with Group Blank UOM",
|
||||
"item_group": "_Test Pricing Rule Item Group",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item with Group Blank UOM", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item with Group Blank UOM Rule",
|
||||
"apply_on": "Item Group",
|
||||
"item_groups": [
|
||||
{
|
||||
"item_group": "_Test Pricing Rule Item Group",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item with Group Blank UOM", uom="Box", conversion_factor=10
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010
|
||||
self.assertEqual(si.items[0].price_list_rate, 1010)
|
||||
self.assertEqual(si.items[0].rate, 1010)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Item with Group Blank UOM", uom="Nos")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM.
|
||||
# rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item with Group Blank UOM"}).delete()
|
||||
item.delete()
|
||||
group.delete()
|
||||
|
||||
def test_item_group_price_with_selling_uom_pricing_rule(self):
|
||||
group = frappe.get_doc(
|
||||
doctype="Item Group",
|
||||
item_group_name="_Test Pricing Rule Item Group UOM",
|
||||
parent_item_group="All Item Groups",
|
||||
)
|
||||
group.save()
|
||||
properties = {
|
||||
"item_code": "Item with Group UOM other than Stock",
|
||||
"item_group": "_Test Pricing Rule Item Group UOM",
|
||||
"stock_uom": "Nos",
|
||||
"sales_uom": "Box",
|
||||
"uoms": [dict(uom="Box", conversion_factor=10)],
|
||||
}
|
||||
item = make_item(properties=properties)
|
||||
|
||||
make_item_price("Item with Group UOM other than Stock", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Item with Group UOM other than Stock Rule",
|
||||
"apply_on": "Item Group",
|
||||
"item_groups": [
|
||||
{
|
||||
"item_group": "_Test Pricing Rule Item Group UOM",
|
||||
"uom": "Box",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 101,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True,
|
||||
item_code="Item with Group UOM other than Stock",
|
||||
uom="Box",
|
||||
conversion_factor=10,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so apply pricing_rule only on Box UOM.
|
||||
# Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor.
|
||||
self.assertEqual(si.items[0].price_list_rate, 101)
|
||||
self.assertEqual(si.items[0].rate, 101)
|
||||
|
||||
si.delete()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=True, item_code="Item with Group UOM other than Stock", uom="Nos"
|
||||
)
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
# UOM is Box so pricing_rule won't apply as selling_uom is Nos.
|
||||
# As Pricing Rule is not applied price of 100 will be fetched from Item Price List.
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Item with Group UOM other than Stock"}).delete()
|
||||
item.delete()
|
||||
group.delete()
|
||||
|
||||
def test_pricing_rule_for_different_currency(self):
|
||||
make_item("Test Sanitizer Item")
|
||||
|
||||
|
||||
@@ -127,7 +127,12 @@ def _get_pricing_rules(apply_on, args, values):
|
||||
values["variant_of"] = args.variant_of
|
||||
elif apply_on_field == "item_group":
|
||||
item_conditions = _get_tree_conditions(args, "Item Group", child_doc, False)
|
||||
|
||||
if args.get("uom", None):
|
||||
item_conditions += (
|
||||
" and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format(
|
||||
child_doc=child_doc, item_uom=args.get("uom")
|
||||
)
|
||||
)
|
||||
conditions += get_other_conditions(conditions, values, args)
|
||||
warehouse_conditions = _get_tree_conditions(args, "Warehouse", "`tabPricing Rule`")
|
||||
if warehouse_conditions:
|
||||
|
||||
@@ -8,7 +8,8 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
},
|
||||
refresh: function(frm){
|
||||
if(!frm.doc.__islocal) {
|
||||
frm.add_custom_button('Send Emails',function(){
|
||||
frm.add_custom_button(__('Send Emails'), function(){
|
||||
if (frm.is_dirty()) frappe.throw(__("Please save before proceeding."))
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
|
||||
args: {
|
||||
@@ -24,8 +25,9 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
||||
}
|
||||
});
|
||||
});
|
||||
frm.add_custom_button('Download',function(){
|
||||
var url = frappe.urllib.get_full_url(
|
||||
frm.add_custom_button(__('Download'), function(){
|
||||
if (frm.is_dirty()) frappe.throw(__("Please save before proceeding."))
|
||||
let url = frappe.urllib.get_full_url(
|
||||
'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
|
||||
+ 'document_name='+encodeURIComponent(frm.doc.name))
|
||||
$.ajax({
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"customers",
|
||||
"preferences",
|
||||
"orientation",
|
||||
"include_break",
|
||||
"include_ageing",
|
||||
"ageing_based_on",
|
||||
"section_break_14",
|
||||
@@ -284,10 +285,16 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Terms and Conditions",
|
||||
"options": "Terms and Conditions"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "include_break",
|
||||
"fieldtype": "Check",
|
||||
"label": "Page Break After Each SoA"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-09-06 21:00:45.732505",
|
||||
"modified": "2022-10-17 17:47:08.662475",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
@@ -320,5 +327,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_months, format_date, getdate, today
|
||||
from frappe.utils.jinja import validate_template
|
||||
@@ -128,7 +129,8 @@ def get_report_pdf(doc, consolidated=True):
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
result = "".join(list(statement_dict.values()))
|
||||
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
|
||||
result = delimiter.join(list(statement_dict.values()))
|
||||
return get_pdf(result, {"orientation": doc.orientation})
|
||||
else:
|
||||
for customer, statement_html in statement_dict.items():
|
||||
@@ -240,8 +242,6 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if int(primary_mandatory):
|
||||
if primary_email == "":
|
||||
continue
|
||||
elif (billing_email == "") and (primary_email == ""):
|
||||
continue
|
||||
|
||||
customer_list.append(
|
||||
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
|
||||
@@ -273,8 +273,12 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
|
||||
link.link_doctype='Customer'
|
||||
and link.link_name=%s
|
||||
and contact.is_billing_contact=1
|
||||
{mcond}
|
||||
ORDER BY
|
||||
contact.creation desc""",
|
||||
contact.creation desc
|
||||
""".format(
|
||||
mcond=get_match_cond("Contact")
|
||||
),
|
||||
customer_name,
|
||||
)
|
||||
|
||||
@@ -313,6 +317,8 @@ def send_emails(document_name, from_scheduler=False):
|
||||
attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
|
||||
|
||||
recipients, cc = get_recipients_and_cc(customer, doc)
|
||||
if not recipients:
|
||||
continue
|
||||
context = get_context(customer, doc)
|
||||
subject = frappe.render_template(doc.subject, context)
|
||||
message = frappe.render_template(doc.body, context)
|
||||
|
||||
@@ -569,6 +569,10 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
frm.clear_table("tax_withheld_vouchers");
|
||||
}
|
||||
},
|
||||
|
||||
is_subcontracted: function(frm) {
|
||||
|
||||
@@ -67,6 +67,9 @@ class PurchaseInvoice(BuyingController):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
|
||||
if self.is_new():
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
def before_save(self):
|
||||
if not self.on_hold:
|
||||
self.release_date = ""
|
||||
@@ -1373,7 +1376,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Purchase Invoice",
|
||||
"Tax Withheld Vouchers",
|
||||
)
|
||||
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
@@ -2187,6 +2187,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
if currency:
|
||||
target_doc.currency = currency
|
||||
|
||||
@@ -916,7 +916,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -1000)
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -500)
|
||||
self.assertEqual(pos_return.get("payments")[1].amount, -500)
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile(
|
||||
|
||||
@@ -298,20 +298,22 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
round_off_account_exists = False
|
||||
round_off_gle = frappe._dict()
|
||||
for d in gl_map:
|
||||
if d.account == round_off_account:
|
||||
round_off_gle = d
|
||||
if d.debit:
|
||||
debit_credit_diff -= flt(d.debit)
|
||||
else:
|
||||
debit_credit_diff += flt(d.credit)
|
||||
round_off_account_exists = True
|
||||
round_off_account_exists = False
|
||||
|
||||
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
|
||||
gl_map.remove(round_off_gle)
|
||||
return
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
for d in gl_map:
|
||||
if d.account == round_off_account:
|
||||
round_off_gle = d
|
||||
if d.debit:
|
||||
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
||||
else:
|
||||
debit_credit_diff += flt(d.credit)
|
||||
round_off_account_exists = True
|
||||
|
||||
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
|
||||
gl_map.remove(round_off_gle)
|
||||
return
|
||||
|
||||
if not round_off_gle:
|
||||
for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]:
|
||||
@@ -334,7 +336,6 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
)
|
||||
|
||||
update_accounting_dimensions(round_off_gle)
|
||||
|
||||
if not round_off_account_exists:
|
||||
gl_map.append(round_off_gle)
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<div class="col-xs-4"><label>Document No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document Date</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.Dt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4 column-break">
|
||||
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||
|
||||
@@ -685,10 +685,10 @@ class ReceivablePayableReport(object):
|
||||
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
|
||||
doc_currency_fields = "debit as debit_in_account_currency, credit as credit_in_account_currency"
|
||||
else:
|
||||
select_fields = "debit, credit"
|
||||
|
||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
||||
|
||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
@@ -367,6 +368,7 @@ class GrossProfitGenerator(object):
|
||||
self.average_buying_rate = {}
|
||||
self.filters = frappe._dict(filters)
|
||||
self.load_invoice_items()
|
||||
self.get_delivery_notes()
|
||||
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
@@ -535,6 +537,21 @@ class GrossProfitGenerator(object):
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
|
||||
def calculate_buying_amount_from_sle(self, row, my_sle, parenttype, parent, item_row, item_code):
|
||||
for i, sle in enumerate(my_sle):
|
||||
# find the stock valution rate from stock ledger entry
|
||||
if (
|
||||
sle.voucher_type == parenttype
|
||||
and parent == sle.voucher_no
|
||||
and sle.voucher_detail_no == item_row
|
||||
):
|
||||
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
|
||||
|
||||
if previous_stock_value:
|
||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
|
||||
def get_buying_amount(self, row, item_code):
|
||||
# IMP NOTE
|
||||
# stock_ledger_entries should already be filtered by item_code and warehouse and
|
||||
@@ -551,19 +568,22 @@ class GrossProfitGenerator(object):
|
||||
if row.dn_detail:
|
||||
parenttype, parent = "Delivery Note", row.delivery_note
|
||||
|
||||
for i, sle in enumerate(my_sle):
|
||||
# find the stock valution rate from stock ledger entry
|
||||
if (
|
||||
sle.voucher_type == parenttype
|
||||
and parent == sle.voucher_no
|
||||
and sle.voucher_detail_no == row.item_row
|
||||
):
|
||||
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
|
||||
|
||||
if previous_stock_value:
|
||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, row.item_row, item_code
|
||||
)
|
||||
elif self.delivery_notes.get((row.parent, row.item_code), None):
|
||||
# check if Invoice has delivery notes
|
||||
dn = self.delivery_notes.get((row.parent, row.item_code))
|
||||
parenttype, parent, item_row, warehouse = (
|
||||
"Delivery Note",
|
||||
dn["delivery_note"],
|
||||
dn["item_row"],
|
||||
dn["warehouse"],
|
||||
)
|
||||
my_sle = self.sle.get((item_code, warehouse))
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
|
||||
@@ -667,6 +687,29 @@ class GrossProfitGenerator(object):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
def get_delivery_notes(self):
|
||||
self.delivery_notes = frappe._dict({})
|
||||
if self.si_list:
|
||||
invoices = [x.parent for x in self.si_list]
|
||||
dni = qb.DocType("Delivery Note Item")
|
||||
delivery_notes = (
|
||||
qb.from_(dni)
|
||||
.select(
|
||||
dni.against_sales_invoice.as_("sales_invoice"),
|
||||
dni.item_code,
|
||||
dni.warehouse,
|
||||
dni.parent.as_("delivery_note"),
|
||||
dni.name.as_("item_row"),
|
||||
)
|
||||
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
|
||||
.groupby(dni.against_sales_invoice, dni.item_code)
|
||||
.orderby(dni.creation, order=Order.desc)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in delivery_notes:
|
||||
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
|
||||
|
||||
def group_items_by_invoice(self):
|
||||
"""
|
||||
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
|
||||
|
||||
208
erpnext/accounts/report/gross_profit/test_gross_profit.py
Normal file
208
erpnext/accounts/report/gross_profit/test_gross_profit.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestGrossProfit(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.create_sales_invoice()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Gross Profit"
|
||||
abbr = "_GP"
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_customer(self):
|
||||
name = "_Test GP Customer"
|
||||
if frappe.db.exists("Customer", name):
|
||||
self.customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return sinv
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"Sales Invoice",
|
||||
"GL Entry",
|
||||
"Stock Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Delivery Note",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def test_invoice_without_only_delivery_note(self):
|
||||
"""
|
||||
Test buying amount for Invoice without `update_stock` flag set but has Delivery Note
|
||||
"""
|
||||
se = make_stock_entry(
|
||||
company=self.company,
|
||||
item_code=self.item,
|
||||
target=self.warehouse,
|
||||
qty=1,
|
||||
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": 1,
|
||||
"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()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
qty=1,
|
||||
rate=100,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
|
||||
# Without Delivery Note, buying rate should be 150
|
||||
expected_entry_without_dn = {
|
||||
"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": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 150.0,
|
||||
"selling_amount": 100.0,
|
||||
"buying_amount": 150.0,
|
||||
"gross_profit": -50.0,
|
||||
"gross_profit_%": -50.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0])
|
||||
|
||||
# make delivery note
|
||||
dn = make_delivery_note(sinv.name)
|
||||
dn.items[0].qty = 1
|
||||
dn = dn.save().submit()
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
|
||||
# Without Delivery Note, buying rate should be 100
|
||||
expected_entry_with_dn = {
|
||||
"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": 1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 100.0,
|
||||
"selling_amount": 100.0,
|
||||
"buying_amount": 100.0,
|
||||
"gross_profit": 0.0,
|
||||
"gross_profit_%": 0.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
|
||||
@@ -232,12 +232,12 @@ def get_conditions(filters):
|
||||
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
else:
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabPurchase Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabPurchase Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -390,12 +390,12 @@ def get_conditions(filters):
|
||||
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
else:
|
||||
conditions += (
|
||||
common_condition
|
||||
+ "and ifnull(`tabSales Invoice Item`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
+ "and ifnull(`tabSales Invoice`.{0}, '') in %({0})s)".format(dimension.fieldname)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -28,7 +28,7 @@ def get_currency(filters):
|
||||
filters["presentation_currency"] if filters.get("presentation_currency") else company_currency
|
||||
)
|
||||
|
||||
report_date = filters.get("to_date")
|
||||
report_date = filters.get("to_date") or filters.get("period_end_date")
|
||||
|
||||
if not report_date:
|
||||
fiscal_year_to_date = get_from_and_to_date(filters.get("to_fiscal_year"))["to_date"]
|
||||
|
||||
@@ -835,10 +835,7 @@ def remove_return_pos_invoices(party_type, party, invoice_list):
|
||||
else:
|
||||
return invoice_list
|
||||
|
||||
# remove pos return invoices from invoice_list
|
||||
for idx, inv in enumerate(invoice_list, 0):
|
||||
if inv.voucher_no in return_pos:
|
||||
del invoice_list[idx]
|
||||
invoice_list = [x for x in invoice_list if x.voucher_no not in return_pos]
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -384,7 +384,11 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
set_values_from_purchase_doc: function(frm, doctype, purchase_doc) {
|
||||
frm.set_value('company', purchase_doc.company);
|
||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||
if (purchase_doc.bill_date) {
|
||||
frm.set_value('purchase_date', purchase_doc.bill_date);
|
||||
} else {
|
||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||
}
|
||||
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
doctype_field = frappe.scrub(doctype)
|
||||
|
||||
@@ -825,7 +825,9 @@ def update_maintenance_status():
|
||||
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.name)
|
||||
if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}):
|
||||
if frappe.db.exists(
|
||||
"Asset Repair", {"asset_name": asset.name, "repair_status": "Pending", "docstatus": 0}
|
||||
):
|
||||
asset.set_status("Out of Order")
|
||||
elif frappe.db.exists(
|
||||
"Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}
|
||||
|
||||
@@ -200,11 +200,11 @@ def get_children(doctype, parent=None, location=None, is_root=False):
|
||||
name as value,
|
||||
is_group as expandable
|
||||
from
|
||||
`tab{doctype}` comp
|
||||
`tabLocation` comp
|
||||
where
|
||||
ifnull(parent_location, "")={parent}
|
||||
""".format(
|
||||
doctype=doctype, parent=frappe.db.escape(parent)
|
||||
parent=frappe.db.escape(parent)
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -43,6 +43,11 @@ frappe.ui.form.on("Purchase Order", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
// On cancel and amending a purchase order with advance payment, reset advance paid amount
|
||||
if (frm.is_new()) {
|
||||
frm.set_value("advance_paid", 0)
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function(frm) {
|
||||
|
||||
@@ -1118,7 +1118,8 @@
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Internal Supplier"
|
||||
"label": "Is Internal Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "supplier.represents_company",
|
||||
@@ -1169,7 +1170,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-16 17:45:04.954055",
|
||||
"modified": "2022-11-17 12:34:36.033363",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -22,6 +22,13 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
frm.set_query('warehouse', 'items', () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
|
||||
@@ -479,7 +479,7 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
|
||||
conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date"))
|
||||
|
||||
rfq_data = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
distinct rfq.name, rfq.transaction_date,
|
||||
rfq.company
|
||||
@@ -487,15 +487,18 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt
|
||||
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
|
||||
where
|
||||
rfq.name = rfq_supplier.parent
|
||||
and rfq_supplier.supplier = '{0}'
|
||||
and rfq_supplier.supplier = %(supplier)s
|
||||
and rfq.docstatus = 1
|
||||
and rfq.company = '{1}'
|
||||
{2}
|
||||
and rfq.company = %(company)s
|
||||
{conditions}
|
||||
order by rfq.transaction_date ASC
|
||||
limit %(page_len)s offset %(start)s """.format(
|
||||
filters.get("supplier"), filters.get("company"), conditions
|
||||
),
|
||||
{"page_len": page_len, "start": start},
|
||||
limit %(page_len)s offset %(start)s """,
|
||||
{
|
||||
"page_len": page_len,
|
||||
"start": start,
|
||||
"company": filters.get("company"),
|
||||
"supplier": filters.get("supplier"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_inter_company_reference()
|
||||
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
@@ -395,6 +396,20 @@ class AccountsController(TransactionBase):
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def disable_tax_included_prices_for_internal_transfer(self):
|
||||
if self.is_internal_transfer():
|
||||
tax_updated = False
|
||||
for tax in self.get("taxes"):
|
||||
if tax.get("included_in_print_rate"):
|
||||
tax.included_in_print_rate = 0
|
||||
tax_updated = True
|
||||
|
||||
if tax_updated:
|
||||
frappe.msgprint(
|
||||
_("Disabled tax included prices since this {} is an internal transfer").format(self.doctype),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def validate_due_date(self):
|
||||
if self.get("is_pos"):
|
||||
return
|
||||
|
||||
@@ -578,6 +578,7 @@ class SellingController(StockController):
|
||||
"customer_address": "address_display",
|
||||
"shipping_address_name": "shipping_address",
|
||||
"company_address": "company_address_display",
|
||||
"dispatch_address_name": "dispatch_address",
|
||||
}
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
|
||||
@@ -890,24 +890,33 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||
|
||||
def set_total_amount_to_default_mop(self, total_amount_to_pay):
|
||||
default_mode_of_payment = frappe.db.get_value(
|
||||
"POS Payment Method",
|
||||
{"parent": self.doc.pos_profile, "default": 1},
|
||||
["mode_of_payment"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if default_mode_of_payment:
|
||||
self.doc.payments = []
|
||||
self.doc.append(
|
||||
"payments",
|
||||
{
|
||||
"mode_of_payment": default_mode_of_payment.mode_of_payment,
|
||||
"amount": total_amount_to_pay,
|
||||
"default": 1,
|
||||
},
|
||||
total_paid_amount = 0
|
||||
for payment in self.doc.get("payments"):
|
||||
total_paid_amount += (
|
||||
payment.amount if self.doc.party_account_currency == self.doc.currency else payment.base_amount
|
||||
)
|
||||
|
||||
pending_amount = total_amount_to_pay - total_paid_amount
|
||||
|
||||
if pending_amount > 0:
|
||||
default_mode_of_payment = frappe.db.get_value(
|
||||
"POS Payment Method",
|
||||
{"parent": self.doc.pos_profile, "default": 1},
|
||||
["mode_of_payment"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if default_mode_of_payment:
|
||||
self.doc.payments = []
|
||||
self.doc.append(
|
||||
"payments",
|
||||
{
|
||||
"mode_of_payment": default_mode_of_payment.mode_of_payment,
|
||||
"amount": pending_amount,
|
||||
"default": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
|
||||
@@ -1345,7 +1345,7 @@ class QuickBooksMigrator(Document):
|
||||
)[0]["name"]
|
||||
|
||||
def _publish(self, *args, **kwargs):
|
||||
frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs)
|
||||
frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs, user=self.modified_by)
|
||||
|
||||
def _get_unique_account_name(self, quickbooks_name, number=0):
|
||||
if number:
|
||||
|
||||
@@ -302,6 +302,7 @@ class TallyMigration(Document):
|
||||
frappe.publish_realtime(
|
||||
"tally_migration_progress_update",
|
||||
{"title": title, "message": message, "count": count, "total": total},
|
||||
user=self.modified_by,
|
||||
)
|
||||
|
||||
def _import_master_data(self):
|
||||
|
||||
@@ -302,7 +302,7 @@ def check_for_nexus(doc, tax_dict):
|
||||
item.tax_collectable = flt(0)
|
||||
item.taxable_amount = flt(0)
|
||||
|
||||
for tax in doc.taxes:
|
||||
for tax in list(doc.taxes):
|
||||
if tax.account_head == TAX_ACCOUNT_HEAD:
|
||||
doc.taxes.remove(tax)
|
||||
return
|
||||
|
||||
@@ -70,11 +70,11 @@ def get_children(doctype, parent=None, company=None, is_root=False):
|
||||
select
|
||||
name as value,
|
||||
is_group as expandable
|
||||
from `tab{doctype}`
|
||||
from `tabDepartment`
|
||||
where
|
||||
{condition}
|
||||
order by name""".format(
|
||||
doctype=doctype, condition=condition
|
||||
condition=condition
|
||||
),
|
||||
var_dict,
|
||||
as_dict=1,
|
||||
|
||||
@@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
|
||||
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
|
||||
frm.add_custom_button(__('Request Loan Closure'), function() {
|
||||
@@ -103,6 +107,14 @@ frappe.ui.form.on('Loan', {
|
||||
frm.trigger("toggle_fields");
|
||||
},
|
||||
|
||||
repayment_schedule_type: function(frm) {
|
||||
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
|
||||
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
|
||||
} else {
|
||||
frm.set_df_property("repayment_start_date", "label", "Repayment Start Date");
|
||||
}
|
||||
},
|
||||
|
||||
loan_type: function(frm) {
|
||||
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
|
||||
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"manually_update_paid_amount_in_salary_slip",
|
||||
"section_break_8",
|
||||
"loan_type",
|
||||
"repayment_schedule_type",
|
||||
"loan_amount",
|
||||
"rate_of_interest",
|
||||
"is_secured_loan",
|
||||
@@ -167,7 +168,8 @@
|
||||
"depends_on": "is_term_loan",
|
||||
"fieldname": "repayment_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Repayment Start Date"
|
||||
"label": "Repayment Start Date",
|
||||
"mandatory_depends_on": "is_term_loan"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@@ -419,12 +421,20 @@
|
||||
"fieldname": "manually_update_paid_amount_in_salary_slip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manually Update Paid Amount in Salary Slip"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"fetch_from": "loan_type.repayment_schedule_type",
|
||||
"fieldname": "repayment_schedule_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Repayment Schedule Type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 02:05:25.017190",
|
||||
"modified": "2022-11-01 10:36:47.902903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
|
||||
@@ -7,7 +7,16 @@ import math
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
date_diff,
|
||||
flt,
|
||||
get_last_day,
|
||||
getdate,
|
||||
now_datetime,
|
||||
nowdate,
|
||||
)
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
@@ -115,30 +124,81 @@ class Loan(AccountsController):
|
||||
if not self.repayment_start_date:
|
||||
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
|
||||
|
||||
schedule_type_details = frappe.db.get_value(
|
||||
"Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1
|
||||
)
|
||||
|
||||
self.repayment_schedule = []
|
||||
payment_date = self.repayment_start_date
|
||||
balance_amount = self.loan_amount
|
||||
while balance_amount > 0:
|
||||
interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100))
|
||||
principal_amount = self.monthly_repayment_amount - interest_amount
|
||||
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
|
||||
if balance_amount < 0:
|
||||
principal_amount += balance_amount
|
||||
balance_amount = 0.0
|
||||
|
||||
total_payment = principal_amount + interest_amount
|
||||
self.append(
|
||||
"repayment_schedule",
|
||||
{
|
||||
"payment_date": payment_date,
|
||||
"principal_amount": principal_amount,
|
||||
"interest_amount": interest_amount,
|
||||
"total_payment": total_payment,
|
||||
"balance_loan_amount": balance_amount,
|
||||
},
|
||||
while balance_amount > 0:
|
||||
interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts(
|
||||
payment_date,
|
||||
balance_amount,
|
||||
schedule_type_details.repayment_schedule_type,
|
||||
schedule_type_details.repayment_date_on,
|
||||
)
|
||||
next_payment_date = add_single_month(payment_date)
|
||||
payment_date = next_payment_date
|
||||
|
||||
if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months":
|
||||
next_payment_date = get_last_day(payment_date)
|
||||
if schedule_type_details.repayment_date_on == "Start of the next month":
|
||||
next_payment_date = add_days(next_payment_date, 1)
|
||||
|
||||
payment_date = next_payment_date
|
||||
|
||||
self.add_repayment_schedule_row(
|
||||
payment_date, principal_amount, interest_amount, total_payment, balance_amount
|
||||
)
|
||||
|
||||
if (
|
||||
schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date"
|
||||
or schedule_type_details.repayment_date_on == "End of the current month"
|
||||
):
|
||||
next_payment_date = add_single_month(payment_date)
|
||||
payment_date = next_payment_date
|
||||
|
||||
def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on):
|
||||
if schedule_type == "Monthly as per repayment start date":
|
||||
days = 1
|
||||
months = 12
|
||||
else:
|
||||
expected_payment_date = get_last_day(payment_date)
|
||||
if repayment_date_on == "Start of the next month":
|
||||
expected_payment_date = add_days(expected_payment_date, 1)
|
||||
|
||||
if expected_payment_date == payment_date:
|
||||
# using 30 days for calculating interest for all full months
|
||||
days = 30
|
||||
months = 365
|
||||
else:
|
||||
days = date_diff(get_last_day(payment_date), payment_date)
|
||||
months = 365
|
||||
|
||||
interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100))
|
||||
principal_amount = self.monthly_repayment_amount - interest_amount
|
||||
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
|
||||
if balance_amount < 0:
|
||||
principal_amount += balance_amount
|
||||
balance_amount = 0.0
|
||||
|
||||
total_payment = principal_amount + interest_amount
|
||||
|
||||
return interest_amount, principal_amount, balance_amount, total_payment
|
||||
|
||||
def add_repayment_schedule_row(
|
||||
self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount
|
||||
):
|
||||
self.append(
|
||||
"repayment_schedule",
|
||||
{
|
||||
"payment_date": payment_date,
|
||||
"principal_amount": principal_amount,
|
||||
"interest_amount": interest_amount,
|
||||
"total_payment": total_payment,
|
||||
"balance_loan_amount": balance_loan_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def set_repayment_period(self):
|
||||
if self.repayment_method == "Repay Fixed Amount per Period":
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
date_diff,
|
||||
flt,
|
||||
format_date,
|
||||
get_datetime,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.loan_management.doctype.loan.loan import (
|
||||
make_loan_write_off,
|
||||
@@ -50,6 +59,51 @@ class TestLoan(unittest.TestCase):
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 1",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 2",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Pro-rated calendar months",
|
||||
repayment_date_on="Start of the next month",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
"Term Loan Type 3",
|
||||
12000,
|
||||
7.5,
|
||||
is_term_loan=1,
|
||||
mode_of_payment="Cash",
|
||||
disbursement_account="Disbursement Account - _TC",
|
||||
payment_account="Payment Account - _TC",
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Pro-rated calendar months",
|
||||
repayment_date_on="End of the current month",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
@@ -65,6 +119,7 @@ class TestLoan(unittest.TestCase):
|
||||
"Loan Account - _TC",
|
||||
"Interest Income Account - _TC",
|
||||
"Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
create_loan_type(
|
||||
@@ -912,6 +967,69 @@ class TestLoan(unittest.TestCase):
|
||||
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
|
||||
self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0)
|
||||
|
||||
def test_term_loan_schedule_types(self):
|
||||
loan = create_loan(
|
||||
self.applicant1,
|
||||
"Term Loan Type 1",
|
||||
12000,
|
||||
"Repay Over Number of Periods",
|
||||
12,
|
||||
repayment_start_date="2022-10-17",
|
||||
)
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023"
|
||||
)
|
||||
|
||||
loan.loan_type = "Term Loan Type 2"
|
||||
loan.save()
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023"
|
||||
)
|
||||
|
||||
loan.loan_type = "Term Loan Type 3"
|
||||
loan.save()
|
||||
|
||||
# Check for first, second and last installment date
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
|
||||
)
|
||||
|
||||
loan.repayment_method = "Repay Fixed Amount per Period"
|
||||
loan.monthly_repayment_amount = 1042
|
||||
loan.save()
|
||||
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022"
|
||||
)
|
||||
self.assertEqual(
|
||||
format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023"
|
||||
)
|
||||
|
||||
|
||||
def create_loan_scenario_for_penalty(doc):
|
||||
pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}]
|
||||
@@ -1043,6 +1161,8 @@ def create_loan_type(
|
||||
penalty_income_account=None,
|
||||
repayment_method=None,
|
||||
repayment_periods=None,
|
||||
repayment_schedule_type=None,
|
||||
repayment_date_on=None,
|
||||
):
|
||||
|
||||
if not frappe.db.exists("Loan Type", loan_name):
|
||||
@@ -1052,6 +1172,7 @@ def create_loan_type(
|
||||
"company": "_Test Company",
|
||||
"loan_name": loan_name,
|
||||
"is_term_loan": is_term_loan,
|
||||
"repayment_schedule_type": "Monthly as per repayment start date",
|
||||
"maximum_loan_amount": maximum_loan_amount,
|
||||
"rate_of_interest": rate_of_interest,
|
||||
"penalty_interest_rate": penalty_interest_rate,
|
||||
@@ -1066,8 +1187,14 @@ def create_loan_type(
|
||||
"repayment_periods": repayment_periods,
|
||||
"write_off_amount": 100,
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
|
||||
if loan_type.is_term_loan:
|
||||
loan_type.repayment_schedule_type = repayment_schedule_type
|
||||
if loan_type.repayment_schedule_type != "Monthly as per repayment start date":
|
||||
loan_type.repayment_date_on = repayment_date_on
|
||||
|
||||
loan_type.insert()
|
||||
loan_type.submit()
|
||||
|
||||
|
||||
|
||||
@@ -191,7 +191,9 @@ def get_total_pledged_security_value(loan):
|
||||
|
||||
for security, qty in pledged_securities.items():
|
||||
after_haircut_percentage = 100 - hair_cut_map.get(security)
|
||||
security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage) / 100
|
||||
security_value += (
|
||||
loan_security_price_map.get(security, 0) * qty * after_haircut_percentage
|
||||
) / 100
|
||||
|
||||
return security_value
|
||||
|
||||
|
||||
@@ -577,8 +577,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
|
||||
loan_doc = frappe.get_doc("Loan", loan)
|
||||
next_accrual_date = None
|
||||
accrued_entries = 0
|
||||
last_repayment_amount = 0
|
||||
last_balance_amount = 0
|
||||
last_repayment_amount = None
|
||||
last_balance_amount = None
|
||||
|
||||
for term in reversed(loan_doc.get("repayment_schedule")):
|
||||
if not term.is_accrued:
|
||||
@@ -586,9 +586,9 @@ def regenerate_repayment_schedule(loan, cancel=0):
|
||||
loan_doc.remove(term)
|
||||
else:
|
||||
accrued_entries += 1
|
||||
if not last_repayment_amount:
|
||||
if last_repayment_amount is None:
|
||||
last_repayment_amount = term.total_payment
|
||||
if not last_balance_amount:
|
||||
if last_balance_amount is None:
|
||||
last_balance_amount = term.balance_loan_amount
|
||||
|
||||
loan_doc.save()
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"company",
|
||||
"is_term_loan",
|
||||
"disabled",
|
||||
"repayment_schedule_type",
|
||||
"repayment_date_on",
|
||||
"description",
|
||||
"account_details_section",
|
||||
"mode_of_payment",
|
||||
@@ -157,12 +159,30 @@
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_term_loan",
|
||||
"description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)",
|
||||
"fieldname": "repayment_schedule_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Repayment Schedule Type",
|
||||
"mandatory_depends_on": "is_term_loan",
|
||||
"options": "\nMonthly as per repayment start date\nPro-rated calendar months"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
|
||||
"description": "Select whether the repayment date should be the end of the current month or start of the upcoming month",
|
||||
"fieldname": "repayment_date_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Repayment Date On",
|
||||
"mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
|
||||
"options": "\nStart of the next month\nEnd of the current month"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-25 16:23:57.009349",
|
||||
"modified": "2022-11-01 17:43:03.954201",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
|
||||
@@ -384,6 +384,7 @@ class BOM(WebsiteGenerator):
|
||||
if self.docstatus == 2:
|
||||
return
|
||||
|
||||
self.flags.cost_updated = False
|
||||
existing_bom_cost = self.total_cost
|
||||
|
||||
if self.docstatus == 1:
|
||||
@@ -406,7 +407,11 @@ class BOM(WebsiteGenerator):
|
||||
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
|
||||
|
||||
if not from_child_bom:
|
||||
frappe.msgprint(_("Cost Updated"), alert=True)
|
||||
msg = "Cost Updated"
|
||||
if not self.flags.cost_updated:
|
||||
msg = "No changes in cost found"
|
||||
|
||||
frappe.msgprint(_(msg), alert=True)
|
||||
|
||||
def update_parent_cost(self):
|
||||
if self.total_cost:
|
||||
@@ -592,11 +597,16 @@ class BOM(WebsiteGenerator):
|
||||
# not via doc event, table is not regenerated and needs updation
|
||||
self.calculate_exploded_cost()
|
||||
|
||||
old_cost = self.total_cost
|
||||
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.base_total_cost = (
|
||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
)
|
||||
|
||||
if self.total_cost != old_cost:
|
||||
self.flags.cost_updated = True
|
||||
|
||||
def calculate_op_cost(self, update_hour_rate=False):
|
||||
"""Update workstation rate and calculates totals"""
|
||||
self.operating_cost = 0
|
||||
|
||||
@@ -583,6 +583,28 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
def test_bom_cost_update_flag(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||
).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item])
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code=rm_item, warehouse="_Test Warehouse - _TC", qty=100, rate=600
|
||||
)
|
||||
|
||||
bom.load_from_db()
|
||||
bom.update_cost()
|
||||
self.assertTrue(bom.flags.cost_updated)
|
||||
|
||||
bom.load_from_db()
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -7,6 +7,8 @@ import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull, Max, Min
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_to_date,
|
||||
@@ -54,6 +56,9 @@ class JobCard(Document):
|
||||
self.set_onload("job_card_excess_transfer", excess_transfer)
|
||||
self.set_onload("work_order_stopped", self.is_work_order_stopped())
|
||||
|
||||
def before_validate(self):
|
||||
self.set_wip_warehouse()
|
||||
|
||||
def validate(self):
|
||||
self.validate_time_logs()
|
||||
self.set_status()
|
||||
@@ -109,43 +114,44 @@ class JobCard(Document):
|
||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||
production_capacity = 1
|
||||
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
time_conditions = [
|
||||
((jctl.from_time < args.from_time) & (jctl.to_time > args.from_time)),
|
||||
((jctl.from_time < args.to_time) & (jctl.to_time > args.to_time)),
|
||||
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
|
||||
]
|
||||
|
||||
if check_next_available_slot:
|
||||
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jctl)
|
||||
.from_(jc)
|
||||
.select(jc.name.as_("name"), jctl.to_time)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (Criterion.any(time_conditions))
|
||||
& (jctl.name != f"{args.name or 'No Name'}")
|
||||
& (jc.name != f"{args.parent or 'No Name'}")
|
||||
& (jc.docstatus < 2)
|
||||
)
|
||||
.orderby(jctl.to_time, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if self.workstation:
|
||||
production_capacity = (
|
||||
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
|
||||
)
|
||||
validate_overlap_for = " and jc.workstation = %(workstation)s "
|
||||
query = query.where(jc.workstation == self.workstation)
|
||||
|
||||
if args.get("employee"):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
validate_overlap_for = " and jctl.employee = %(employee)s "
|
||||
query = query.where(jctl.employee == args.get("employee"))
|
||||
|
||||
extra_cond = ""
|
||||
if check_next_available_slot:
|
||||
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
|
||||
|
||||
existing = frappe.db.sql(
|
||||
"""select jc.name as name, jctl.to_time from
|
||||
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
|
||||
(
|
||||
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
|
||||
(%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or
|
||||
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
|
||||
)
|
||||
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
|
||||
order by jctl.to_time desc limit 1""".format(
|
||||
extra_cond, validate_overlap_for
|
||||
),
|
||||
{
|
||||
"from_time": args.from_time,
|
||||
"to_time": args.to_time,
|
||||
"name": args.name or "No Name",
|
||||
"parent": args.parent or "No Name",
|
||||
"employee": args.get("employee"),
|
||||
"workstation": self.workstation,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
existing = query.run(as_dict=True)
|
||||
|
||||
if existing and production_capacity > len(existing):
|
||||
return
|
||||
@@ -485,18 +491,21 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
def update_work_order_data(self, for_quantity, time_in_mins, wo):
|
||||
time_data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
min(from_time) as start_time, max(to_time) as end_time
|
||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||
WHERE
|
||||
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
|
||||
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
|
||||
""",
|
||||
(self.work_order, self.operation_id),
|
||||
as_dict=1,
|
||||
)
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
time_data = (
|
||||
frappe.qb.from_(jc)
|
||||
.from_(jctl)
|
||||
.select(Min(jctl.from_time).as_("start_time"), Max(jctl.to_time).as_("end_time"))
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (jc.work_order == self.work_order)
|
||||
& (jc.operation_id == self.operation_id)
|
||||
& (jc.docstatus == 1)
|
||||
& (IfNull(jc.is_corrective_job_card, 0) == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
for data in wo.operations:
|
||||
if data.get("name") == self.operation_id:
|
||||
@@ -639,6 +648,12 @@ class JobCard(Document):
|
||||
if update_status:
|
||||
self.db_set("status", self.status)
|
||||
|
||||
def set_wip_warehouse(self):
|
||||
if not self.wip_warehouse:
|
||||
self.wip_warehouse = frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "default_wip_warehouse"
|
||||
)
|
||||
|
||||
def validate_operation_id(self):
|
||||
if (
|
||||
self.get("operation_id")
|
||||
|
||||
@@ -133,6 +133,45 @@ class TestJobCard(FrappeTestCase):
|
||||
)
|
||||
self.assertRaises(OverlapError, jc2.save)
|
||||
|
||||
def test_job_card_overlap_with_capacity(self):
|
||||
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
|
||||
workstation = make_workstation(workstation_name=random_string(5)).name
|
||||
frappe.db.set_value("Workstation", workstation, "production_capacity", 1)
|
||||
|
||||
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
|
||||
|
||||
jc1.workstation = workstation
|
||||
jc1.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:00:00", "to_time": "2021-01-01 08:00:00", "completed_qty": 1},
|
||||
)
|
||||
jc1.save()
|
||||
|
||||
jc2.workstation = workstation
|
||||
|
||||
# add a new entry in same time slice
|
||||
jc2.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
|
||||
)
|
||||
self.assertRaises(OverlapError, jc2.save)
|
||||
|
||||
frappe.db.set_value("Workstation", workstation, "production_capacity", 2)
|
||||
jc2.load_from_db()
|
||||
|
||||
jc2.workstation = workstation
|
||||
|
||||
# add a new entry in same time slice
|
||||
jc2.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 1},
|
||||
)
|
||||
|
||||
jc2.save()
|
||||
self.assertTrue(jc2.name)
|
||||
|
||||
def test_job_card_multiple_materials_transfer(self):
|
||||
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||
self.transfer_material_against = "Job Card"
|
||||
|
||||
@@ -27,6 +27,7 @@ from six import iteritems
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
|
||||
|
||||
@@ -977,11 +978,25 @@ def get_material_request_items(
|
||||
if include_safety_stock:
|
||||
required_qty += flt(row["safety_stock"])
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1
|
||||
)
|
||||
|
||||
conversion_factor = 1.0
|
||||
if (
|
||||
row.get("default_material_request_type") == "Purchase"
|
||||
and item_details.purchase_uom
|
||||
and item_details.purchase_uom != item_details.stock_uom
|
||||
):
|
||||
conversion_factor = (
|
||||
get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
if required_qty > 0:
|
||||
return {
|
||||
"item_code": row.item_code,
|
||||
"item_name": row.item_name,
|
||||
"quantity": required_qty,
|
||||
"quantity": required_qty / conversion_factor,
|
||||
"required_bom_qty": total_qty,
|
||||
"stock_uom": row.get("stock_uom"),
|
||||
"warehouse": warehouse
|
||||
|
||||
@@ -732,6 +732,40 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertEqual(pln.status, "Completed")
|
||||
self.assertEqual(pln.po_items[0].produced_qty, 5)
|
||||
|
||||
def test_material_request_item_for_purchase_uom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
bom_item = make_item(
|
||||
properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
|
||||
).name
|
||||
|
||||
if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
|
||||
doc = frappe.get_doc("Item", bom_item)
|
||||
doc.append("uoms", {"uom": "Nos", "conversion_factor": 10})
|
||||
doc.save()
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
|
||||
)
|
||||
|
||||
pln.make_material_request()
|
||||
|
||||
for row in pln.mr_items:
|
||||
self.assertEqual(row.uom, "Nos")
|
||||
self.assertEqual(row.quantity, 1)
|
||||
|
||||
for row in frappe.get_all(
|
||||
"Material Request Item",
|
||||
filters={"production_plan": pln.name},
|
||||
fields=["item_code", "uom", "qty"],
|
||||
):
|
||||
self.assertEqual(row.item_code, bom_item)
|
||||
self.assertEqual(row.uom, "Nos")
|
||||
self.assertEqual(row.qty, 1)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -625,6 +625,10 @@ class TestWorkOrder(FrappeTestCase):
|
||||
bom.submit()
|
||||
bom_name = bom.name
|
||||
|
||||
ste1 = test_stock_entry.make_stock_entry(
|
||||
item_code=rm1, target="_Test Warehouse - _TC", qty=32, basic_rate=5000.0
|
||||
)
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1
|
||||
)
|
||||
@@ -649,11 +653,29 @@ class TestWorkOrder(FrappeTestCase):
|
||||
work_order.insert()
|
||||
work_order.submit()
|
||||
self.assertEqual(work_order.has_batch_no, 1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
|
||||
batches = frappe.get_all("Batch", filters={"reference_name": work_order.name})
|
||||
self.assertEqual(len(batches), 3)
|
||||
batches = [batch.name for batch in batches]
|
||||
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 10))
|
||||
for row in ste1.get("items"):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
self.assertEqual(row.qty, 10)
|
||||
self.assertTrue(row.batch_no in batches)
|
||||
batches.remove(row.batch_no)
|
||||
|
||||
ste1.submit()
|
||||
|
||||
remaining_batches = []
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 20))
|
||||
for row in ste1.get("items"):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
self.assertEqual(row.qty, 10)
|
||||
remaining_batches.append(row.batch_no)
|
||||
|
||||
self.assertEqual(sorted(remaining_batches), sorted(batches))
|
||||
|
||||
frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0)
|
||||
|
||||
|
||||
@@ -555,66 +555,69 @@ erpnext.work_order = {
|
||||
}
|
||||
}
|
||||
|
||||
if(!frm.doc.skip_transfer){
|
||||
if (frm.doc.status != 'Stopped') {
|
||||
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') {
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
|
||||
frm.has_finish_btn = true;
|
||||
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
// Only show "Material Consumption" when required_qty > consumed_qty
|
||||
var counter = 0;
|
||||
var tbl = frm.doc.required_items || [];
|
||||
var tbl_lenght = tbl.length;
|
||||
for (var i = 0, len = tbl_lenght; i < len; i++) {
|
||||
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
|
||||
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
if (counter > 0) {
|
||||
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
|
||||
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
|
||||
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
|
||||
});
|
||||
consumption_btn.addClass('btn-primary');
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0 || frm.doc.skip_transfer) {
|
||||
// Only show "Material Consumption" when required_qty > consumed_qty
|
||||
var counter = 0;
|
||||
var tbl = frm.doc.required_items || [];
|
||||
var tbl_lenght = tbl.length;
|
||||
for (var i = 0, len = tbl_lenght; i < len; i++) {
|
||||
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
|
||||
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
if (counter > 0) {
|
||||
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
|
||||
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
|
||||
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
|
||||
});
|
||||
consumption_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!frm.doc.skip_transfer){
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0) {
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
|
||||
frm.has_finish_btn = true;
|
||||
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((flt(doc.produced_qty) < flt(doc.qty))) {
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
calculate_cost: function(doc) {
|
||||
if (doc.operations){
|
||||
|
||||
@@ -145,7 +145,7 @@ class WorkOrder(Document):
|
||||
frappe.throw(_("Sales Order {0} is {1}").format(self.sales_order, status))
|
||||
|
||||
def set_default_warehouse(self):
|
||||
if not self.wip_warehouse:
|
||||
if not self.wip_warehouse and not self.skip_transfer:
|
||||
self.wip_warehouse = frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "default_wip_warehouse"
|
||||
)
|
||||
|
||||
@@ -54,11 +54,11 @@ frappe.query_reports["Job Card Summary"] = {
|
||||
options: ["", "Open", "Work In Progress", "Completed", "On Hold"]
|
||||
},
|
||||
{
|
||||
label: __("Sales Orders"),
|
||||
fieldname: "sales_order",
|
||||
label: __("Work Orders"),
|
||||
fieldname: "work_order",
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Sales Order', txt);
|
||||
return frappe.db.get_link_options('Work Order', txt);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,10 +36,14 @@ def get_data(filters):
|
||||
"total_time_in_mins",
|
||||
]
|
||||
|
||||
for field in ["work_order", "workstation", "operation", "company"]:
|
||||
for field in ["work_order", "production_item"]:
|
||||
if filters.get(field):
|
||||
query_filters[field] = ("in", filters.get(field))
|
||||
|
||||
for field in ["workstation", "operation", "status", "company"]:
|
||||
if filters.get(field):
|
||||
query_filters[field] = filters.get(field)
|
||||
|
||||
data = frappe.get_all("Job Card", fields=fields, filters=query_filters)
|
||||
|
||||
if not data:
|
||||
|
||||
@@ -39,10 +39,14 @@ def get_data(filters):
|
||||
"lead_time",
|
||||
]
|
||||
|
||||
for field in ["sales_order", "production_item", "status", "company"]:
|
||||
for field in ["sales_order", "production_item"]:
|
||||
if filters.get(field):
|
||||
query_filters[field] = ("in", filters.get(field))
|
||||
|
||||
for field in ["status", "company"]:
|
||||
if filters.get(field):
|
||||
query_filters[field] = filters.get(field)
|
||||
|
||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
|
||||
|
||||
|
||||
@@ -374,3 +374,4 @@ erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
17
erpnext/patches/v13_0/update_schedule_type_in_loans.py
Normal file
17
erpnext/patches/v13_0/update_schedule_type_in_loans.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("loan_management", "doctype", "loan")
|
||||
frappe.reload_doc("loan_management", "doctype", "loan_type")
|
||||
|
||||
loan = frappe.qb.DocType("Loan")
|
||||
loan_type = frappe.qb.DocType("Loan Type")
|
||||
|
||||
frappe.qb.update(loan_type).set(
|
||||
loan_type.repayment_schedule_type, "Monthly as per repayment start date"
|
||||
).where(loan_type.is_term_loan == 1).run()
|
||||
|
||||
frappe.qb.update(loan).set(
|
||||
loan.repayment_schedule_type, "Monthly as per repayment start date"
|
||||
).where(loan.is_term_loan == 1).run()
|
||||
@@ -346,6 +346,8 @@ class PayrollEntry(Document):
|
||||
"credit_in_account_currency": flt(payable_amt, precision),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"cost_center": self.cost_center,
|
||||
"reference_type": self.doctype,
|
||||
"reference_name": self.name,
|
||||
},
|
||||
accounting_dimensions,
|
||||
)
|
||||
@@ -720,12 +722,21 @@ def get_month_details(year, month):
|
||||
|
||||
def get_payroll_entry_bank_entries(payroll_entry_name):
|
||||
journal_entries = frappe.db.sql(
|
||||
"select name from `tabJournal Entry Account` "
|
||||
'where reference_type="Payroll Entry" '
|
||||
"and reference_name=%s and docstatus=1",
|
||||
"""
|
||||
select
|
||||
je.name
|
||||
from
|
||||
`tabJournal Entry` je,
|
||||
`tabJournal Entry Account` jea
|
||||
where
|
||||
je.name = jea.parent
|
||||
and je.voucher_type = 'Bank Entry'
|
||||
and jea.reference_type = 'Payroll Entry'
|
||||
and jea.reference_name = %s
|
||||
""",
|
||||
payroll_entry_name,
|
||||
as_dict=1,
|
||||
)
|
||||
as_dict=True,
|
||||
) # nosemgrep
|
||||
|
||||
return journal_entries
|
||||
|
||||
|
||||
@@ -133,9 +133,17 @@ class TestPayrollEntry(FrappeTestCase):
|
||||
|
||||
payment_entry = frappe.db.sql(
|
||||
"""
|
||||
Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea
|
||||
Where je.name = jea.parent
|
||||
And jea.reference_name = %s
|
||||
select
|
||||
ifnull(sum(je.total_debit),0) as total_debit,
|
||||
ifnull(sum(je.total_credit),0) as total_credit
|
||||
from
|
||||
`tabJournal Entry` je,
|
||||
`tabJournal Entry Account` jea
|
||||
Where
|
||||
je.name = jea.parent
|
||||
and je.voucher_type = 'Bank Entry'
|
||||
and jea.reference_type = 'Payroll Entry'
|
||||
and jea.reference_name = %s
|
||||
""",
|
||||
(payroll_entry.name),
|
||||
as_dict=1,
|
||||
@@ -295,6 +303,7 @@ class TestPayrollEntry(FrappeTestCase):
|
||||
loan_account="Loan Account - _TC",
|
||||
interest_income_account="Interest Income Account - _TC",
|
||||
penalty_income_account="Penalty Income Account - _TC",
|
||||
repayment_schedule_type="Monthly as per repayment start date",
|
||||
)
|
||||
|
||||
loan = create_loan(
|
||||
|
||||
@@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
|
||||
});
|
||||
};
|
||||
|
||||
erpnext.utils.get_contact_details = function(frm) {
|
||||
erpnext.utils.get_contact_details = function (frm) {
|
||||
if (frm.updating_party_details) return;
|
||||
|
||||
if (frm.doc["contact_person"]) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
|
||||
args: {contact: frm.doc.contact_person },
|
||||
callback: function(r) {
|
||||
if (r.message)
|
||||
frm.set_value(r.message);
|
||||
}
|
||||
})
|
||||
args: { contact: frm.doc.contact_person },
|
||||
callback: function (r) {
|
||||
if (r.message) frm.set_value(r.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
frm.set_value({
|
||||
contact_person: "",
|
||||
contact_display: "",
|
||||
contact_email: "",
|
||||
contact_mobile: "",
|
||||
contact_phone: "",
|
||||
contact_designation: "",
|
||||
contact_department: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
|
||||
if (!value) {
|
||||
|
||||
@@ -159,6 +159,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.item-img {
|
||||
@extend .image;
|
||||
border-radius: 8px 8px 0 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
> .item-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -79,7 +79,7 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa
|
||||
]
|
||||
else:
|
||||
return frappe.get_all(
|
||||
doctype,
|
||||
"Quality Procedure",
|
||||
fields=["name as value", "is_group as expandable"],
|
||||
filters=dict(parent_quality_procedure=parent),
|
||||
order_by="name asc",
|
||||
|
||||
@@ -10,3 +10,8 @@ class EInvoiceSettings(Document):
|
||||
def validate(self):
|
||||
if self.enable and not self.credentials:
|
||||
frappe.throw(_("You must add atleast one credentials to be able to use E Invoicing."))
|
||||
|
||||
prev_doc = self.get_doc_before_save()
|
||||
if prev_doc.client_secret != self.client_secret or prev_doc.client_id != self.client_id:
|
||||
self.auth_token = None
|
||||
self.token_expiry = None
|
||||
|
||||
@@ -146,7 +146,9 @@ class ImportSupplierInvoice(Document):
|
||||
|
||||
def publish(self, title, message, count, total):
|
||||
frappe.publish_realtime(
|
||||
"import_invoice_update", {"title": title, "message": message, "count": count, "total": total}
|
||||
"import_invoice_update",
|
||||
{"title": title, "message": message, "count": count, "total": total},
|
||||
user=self.modified_by,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,12 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reverse_charge: function(frm) {
|
||||
if (frm.doc.reverse_charge == "Y") {
|
||||
frm.set_value('eligibility_for_itc', 'ITC on Reverse Charge');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -983,8 +983,6 @@ def validate_reverse_charge_transaction(doc, method):
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
doc.eligibility_for_itc = "ITC on Reverse Charge"
|
||||
|
||||
|
||||
def update_itc_availed_fields(doc, method):
|
||||
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||
|
||||
@@ -35,7 +35,7 @@ def _execute(filters=None):
|
||||
data = []
|
||||
added_item = []
|
||||
for d in item_list:
|
||||
if (d.parent, d.item_code) not in added_item:
|
||||
if (d.parent, d.gst_hsn_code, d.item_code) not in added_item:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
|
||||
total_tax = 0
|
||||
tax_rate = 0
|
||||
@@ -52,7 +52,7 @@ def _execute(filters=None):
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
row += [item_tax.get("tax_amount", 0)]
|
||||
data.append(row)
|
||||
added_item.append((d.parent, d.item_code))
|
||||
added_item.append((d.parent, d.gst_hsn_code, d.item_code))
|
||||
if data:
|
||||
data = get_merged_data(columns, data) # merge same hsn code data
|
||||
return columns, data
|
||||
@@ -161,11 +161,9 @@ def get_items(filters):
|
||||
GROUP BY
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
`tabSales Invoice Item`.gst_hsn_code
|
||||
ORDER BY
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
`tabSales Invoice Item`.gst_hsn_code
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
|
||||
@@ -83,11 +83,12 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) {
|
||||
if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||
cur_frm.add_custom_button(__('Sales Order'),
|
||||
cur_frm.cscript['Make Sales Order'], __('Create'));
|
||||
}
|
||||
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
this.frm.cscript["Make Sales Order"],
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if(doc.status!=="Ordered") {
|
||||
this.frm.add_custom_button(__('Set as Lost'), () => {
|
||||
|
||||
@@ -95,6 +95,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
return query;
|
||||
});
|
||||
|
||||
// On cancel and amending a sales order with advance payment, reset advance paid amount
|
||||
if (frm.is_new()) {
|
||||
frm.set_value("advance_paid", 0)
|
||||
}
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
|
||||
},
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
|
||||
@@ -105,11 +106,11 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
|
||||
ORDER BY
|
||||
item.name asc
|
||||
LIMIT
|
||||
{start}, {page_length}""".format(
|
||||
start=start,
|
||||
page_length=page_length,
|
||||
lft=lft,
|
||||
rgt=rgt,
|
||||
{page_length} offset {start}""".format(
|
||||
start=cint(start),
|
||||
page_length=cint(page_length),
|
||||
lft=cint(lft),
|
||||
rgt=cint(rgt),
|
||||
condition=condition,
|
||||
bin_join_selection=bin_join_selection,
|
||||
bin_join_condition=bin_join_condition,
|
||||
|
||||
@@ -67,7 +67,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
{
|
||||
fieldtype: 'Link', label: __('POS Profile'),
|
||||
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
|
||||
get_query: () => pos_profile_query,
|
||||
get_query: () => pos_profile_query(),
|
||||
onchange: () => fetch_pos_payment_methods()
|
||||
},
|
||||
{
|
||||
@@ -101,9 +101,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
dialog.show();
|
||||
const pos_profile_query = {
|
||||
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
|
||||
filters: { company: dialog.fields_dict.company.get_value() }
|
||||
const pos_profile_query = () => {
|
||||
return {
|
||||
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
|
||||
filters: { company: dialog.fields_dict.company.get_value() }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
|
||||
<img
|
||||
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
||||
class="h-full" src="${item_image}"
|
||||
class="h-full item-img" src="${item_image}"
|
||||
alt="${frappe.get_abbr(item.item_name)}"
|
||||
style="object-fit: cover;">
|
||||
>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="item-qty-pill">
|
||||
|
||||
@@ -611,11 +611,11 @@ def get_children(doctype, parent=None, company=None, is_root=False):
|
||||
name as value,
|
||||
is_group as expandable
|
||||
from
|
||||
`tab{doctype}` comp
|
||||
`tabCompany` comp
|
||||
where
|
||||
ifnull(parent_company, "")={parent}
|
||||
""".format(
|
||||
doctype=doctype, parent=frappe.db.escape(parent)
|
||||
parent=frappe.db.escape(parent)
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -285,7 +285,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
|
||||
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
|
||||
|
||||
for batch in batches:
|
||||
if cint(qty) <= cint(batch.qty):
|
||||
if flt(qty) <= flt(batch.qty):
|
||||
batch_no = batch.batch_id
|
||||
break
|
||||
|
||||
|
||||
@@ -832,6 +832,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
)
|
||||
|
||||
update_taxes(
|
||||
target_doc,
|
||||
|
||||
@@ -1050,9 +1050,22 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": "Tax 1",
|
||||
"rate": 14,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(dn.items[0].rate, 500) # haven't saved yet
|
||||
dn.save()
|
||||
self.assertEqual(dn.ignore_pricing_rule, 1)
|
||||
self.assertEqual(dn.taxes[0].included_in_print_rate, 0)
|
||||
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
@@ -1063,6 +1076,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.save()
|
||||
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
self.assertEqual(dn.items[0].net_rate, rate)
|
||||
|
||||
def test_internal_transfer_precision_gle(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
@@ -370,9 +370,6 @@ frappe.ui.form.on("Material Request Item", {
|
||||
if (flt(d.qty) < flt(d.min_order_qty)) {
|
||||
frappe.msgprint(__("Warning: Material Requested Qty is less than Minimum Order Qty"));
|
||||
}
|
||||
|
||||
const item = locals[doctype][name];
|
||||
frm.events.get_item_data(frm, item, false);
|
||||
},
|
||||
|
||||
from_warehouse: function(frm, doctype, name) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||
from six import string_types
|
||||
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
@@ -504,13 +504,13 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
|
||||
and mr.per_ordered < 99.99
|
||||
and mr.docstatus = 1
|
||||
and mr.status != 'Stopped'
|
||||
and mr.company = '{1}'
|
||||
{2}
|
||||
and mr.company = %s
|
||||
{1}
|
||||
order by mr_item.item_code ASC
|
||||
limit {3} offset {4} """.format(
|
||||
", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start
|
||||
limit {2} offset {3} """.format(
|
||||
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
|
||||
),
|
||||
tuple(supplier_items),
|
||||
tuple(supplier_items) + (filters.get("company"),),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "material_request",
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"],
|
||||
"items": ["Sales Order", "Request for Quotation", "Supplier Quotation", "Purchase Order"],
|
||||
},
|
||||
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order"]},
|
||||
|
||||
@@ -12,13 +12,17 @@ def get_data():
|
||||
"Purchase Receipt": "return_against",
|
||||
},
|
||||
"internal_links": {
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
"Project": ["items", "project"],
|
||||
"Quality Inspection": ["items", "quality_inspection"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
|
||||
{"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]},
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
|
||||
},
|
||||
{"label": _("Returns"), "items": ["Purchase Receipt"]},
|
||||
{"label": _("Subscription"), "items": ["Auto Repeat"]},
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, cstr, flt
|
||||
|
||||
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
|
||||
get_template_details,
|
||||
@@ -219,68 +219,71 @@ class QualityInspection(Document):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("from"):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
mcond = get_match_cond(filters["from"])
|
||||
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
|
||||
from_doctype = cstr(filters.get("doctype"))
|
||||
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
|
||||
return []
|
||||
|
||||
if filters.get("parent"):
|
||||
if (
|
||||
filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"]
|
||||
and filters.get("inspection_type") != "In Process"
|
||||
):
|
||||
cond = """and item_code in (select name from `tabItem` where
|
||||
inspection_required_before_purchase = 1)"""
|
||||
elif (
|
||||
filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"]
|
||||
and filters.get("inspection_type") != "In Process"
|
||||
):
|
||||
cond = """and item_code in (select name from `tabItem` where
|
||||
inspection_required_before_delivery = 1)"""
|
||||
elif filters.get("from") == "Stock Entry Detail":
|
||||
cond = """and s_warehouse is null"""
|
||||
mcond = get_match_cond(from_doctype)
|
||||
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
|
||||
|
||||
if filters.get("from") in ["Supplier Quotation Item"]:
|
||||
qi_condition = ""
|
||||
if filters.get("parent"):
|
||||
if (
|
||||
from_doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]
|
||||
and filters.get("inspection_type") != "In Process"
|
||||
):
|
||||
cond = """and item_code in (select name from `tabItem` where
|
||||
inspection_required_before_purchase = 1)"""
|
||||
elif (
|
||||
from_doctype in ["Sales Invoice Item", "Delivery Note Item"]
|
||||
and filters.get("inspection_type") != "In Process"
|
||||
):
|
||||
cond = """and item_code in (select name from `tabItem` where
|
||||
inspection_required_before_delivery = 1)"""
|
||||
elif from_doctype == "Stock Entry Detail":
|
||||
cond = """and s_warehouse is null"""
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT item_code
|
||||
FROM `tab{doc}`
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {start}, {page_len}
|
||||
""".format(
|
||||
doc=filters.get("from"),
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=start,
|
||||
page_len=page_len,
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
{"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
if from_doctype in ["Supplier Quotation Item"]:
|
||||
qi_condition = ""
|
||||
|
||||
elif filters.get("reference_name"):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT production_item
|
||||
FROM `tab{doc}`
|
||||
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY production_item
|
||||
LIMIT {start}, {page_len}
|
||||
""".format(
|
||||
doc=filters.get("from"),
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=start,
|
||||
page_len=page_len,
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
{"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT item_code
|
||||
FROM `tab{doc}`
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {page_len} offset {start}
|
||||
""".format(
|
||||
doc=from_doctype,
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=cint(start),
|
||||
page_len=cint(page_len),
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
{"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
elif filters.get("reference_name"):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT production_item
|
||||
FROM `tab{doc}`
|
||||
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY production_item
|
||||
limit {page_len} offset {start}
|
||||
""".format(
|
||||
doc=from_doctype,
|
||||
cond=cond,
|
||||
mcond=mcond,
|
||||
start=cint(start),
|
||||
page_len=cint(page_len),
|
||||
qi_condition=qi_condition,
|
||||
),
|
||||
{"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -34,6 +34,22 @@ frappe.ui.form.on('Repost Item Valuation', {
|
||||
frm.trigger('setup_realtime_progress');
|
||||
},
|
||||
|
||||
based_on: function(frm) {
|
||||
var fields_to_reset = [];
|
||||
|
||||
if (frm.doc.based_on == 'Transaction') {
|
||||
fields_to_reset = ['item_code', 'warehouse'];
|
||||
} else if (frm.doc.based_on == 'Item and Warehouse') {
|
||||
fields_to_reset = ['voucher_type', 'voucher_no'];
|
||||
}
|
||||
|
||||
if (fields_to_reset) {
|
||||
fields_to_reset.forEach(field => {
|
||||
frm.set_value(field, undefined);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setup_realtime_progress: function(frm) {
|
||||
frappe.realtime.on('item_reposting_progress', data => {
|
||||
if (frm.doc.name !== data.name) {
|
||||
|
||||
@@ -50,13 +50,15 @@
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"read_only_depends_on": "eval: doc.based_on == \"Transaction\"",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "voucher_no.posting_time",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time"
|
||||
"label": "Posting Time",
|
||||
"read_only_depends_on": "eval: doc.based_on == \"Transaction\""
|
||||
},
|
||||
{
|
||||
"default": "Queued",
|
||||
@@ -195,7 +197,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-13 12:20:22.182322",
|
||||
"modified": "2022-11-28 16:00:05.637440",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Repost Item Valuation",
|
||||
|
||||
@@ -1050,7 +1050,8 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
||||
if (frm.doc.purpose === 'Material Receipt') return;
|
||||
|
||||
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
|
||||
new erpnext.SerialNoBatchSelector({
|
||||
if (frm.batch_selector?.dialog?.display) return;
|
||||
frm.batch_selector = new erpnext.SerialNoBatchSelector({
|
||||
frm: frm,
|
||||
item: item,
|
||||
warehouse_details: get_warehouse_type_and_name(item),
|
||||
|
||||
@@ -1474,6 +1474,7 @@ class StockEntry(StockController):
|
||||
"reference_name": self.pro_doc.name,
|
||||
"reference_doctype": self.pro_doc.doctype,
|
||||
"qty_to_produce": (">", 0),
|
||||
"batch_qty": ("=", 0),
|
||||
}
|
||||
|
||||
fields = ["qty_to_produce as qty", "produced_qty", "name"]
|
||||
@@ -2191,14 +2192,14 @@ class StockEntry(StockController):
|
||||
d.qty -= process_loss_dict[d.item_code][1]
|
||||
|
||||
def set_serial_no_batch_for_finished_good(self):
|
||||
args = {}
|
||||
serial_nos = ""
|
||||
if self.pro_doc.serial_no:
|
||||
self.get_serial_nos_for_fg(args)
|
||||
serial_nos = self.get_serial_nos_for_fg()
|
||||
|
||||
for row in self.items:
|
||||
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
|
||||
if args.get("serial_no"):
|
||||
row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)])
|
||||
if serial_nos:
|
||||
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
|
||||
|
||||
def get_serial_nos_for_fg(self, args):
|
||||
fields = [
|
||||
@@ -2211,14 +2212,14 @@ class StockEntry(StockController):
|
||||
filters = [
|
||||
["Stock Entry", "work_order", "=", self.work_order],
|
||||
["Stock Entry", "purpose", "=", "Manufacture"],
|
||||
["Stock Entry", "docstatus", "=", 1],
|
||||
["Stock Entry", "docstatus", "<", 2],
|
||||
["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item],
|
||||
]
|
||||
|
||||
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
|
||||
|
||||
if self.pro_doc.serial_no:
|
||||
args["serial_no"] = self.get_available_serial_nos(stock_entries)
|
||||
return self.get_available_serial_nos(stock_entries)
|
||||
|
||||
def get_available_serial_nos(self, stock_entries):
|
||||
used_serial_nos = []
|
||||
|
||||
@@ -229,7 +229,7 @@ class StockReconciliation(StockController):
|
||||
|
||||
if item.has_serial_no or item.has_batch_no:
|
||||
has_serial_no = True
|
||||
self.get_sle_for_serialized_items(row, sl_entries)
|
||||
self.get_sle_for_serialized_items(row, sl_entries, item)
|
||||
else:
|
||||
if row.serial_no or row.batch_no:
|
||||
frappe.throw(
|
||||
@@ -281,7 +281,7 @@ class StockReconciliation(StockController):
|
||||
if has_serial_no and sl_entries:
|
||||
self.update_valuation_rate_for_serial_no()
|
||||
|
||||
def get_sle_for_serialized_items(self, row, sl_entries):
|
||||
def get_sle_for_serialized_items(self, row, sl_entries, item):
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
serial_nos = get_serial_nos(row.serial_no)
|
||||
@@ -347,6 +347,9 @@ class StockReconciliation(StockController):
|
||||
if row.qty:
|
||||
args = self.get_sle_for_items(row)
|
||||
|
||||
if item.has_serial_no and item.has_batch_no:
|
||||
args["qty_after_transaction"] = row.qty
|
||||
|
||||
args.update(
|
||||
{
|
||||
"actual_qty": row.qty,
|
||||
|
||||
@@ -643,6 +643,38 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
)
|
||||
self.assertEqual(len(active_sr_no), 0)
|
||||
|
||||
def test_serial_no_batch_no_item(self):
|
||||
item = self.make_item(
|
||||
"Test Serial No Batch No Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"has_batch_no": 1,
|
||||
"serial_no_series": "SRS9.####",
|
||||
"batch_number_series": "BNS9.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item.name,
|
||||
warehouse=warehouse,
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
sl_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Stock Reconciliation", "voucher_no": sr.name},
|
||||
["actual_qty", "qty_after_transaction"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sl_entry.actual_qty), 1.0)
|
||||
self.assertEqual(flt(sl_entry.qty_after_transaction), 1.0)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -315,6 +315,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
else:
|
||||
args.uom = item.stock_uom
|
||||
|
||||
# Set stock UOM in args, so that it can be used while fetching item price
|
||||
args.stock_uom = item.stock_uom
|
||||
|
||||
if args.get("batch_no") and item.name != frappe.get_cached_value(
|
||||
"Batch", args.get("batch_no"), "item"
|
||||
):
|
||||
|
||||
@@ -74,10 +74,21 @@ def get_conditions(filters):
|
||||
else:
|
||||
frappe.throw(_("'To Date' is required"))
|
||||
|
||||
for field in ["item_code", "warehouse", "batch_no", "company"]:
|
||||
for field in ["item_code", "batch_no", "company"]:
|
||||
if filters.get(field):
|
||||
conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field)))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
if warehouse_details:
|
||||
conditions += (
|
||||
" and exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"
|
||||
% (warehouse_details.lft, warehouse_details.rgt)
|
||||
)
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
@@ -87,7 +98,7 @@ def get_stock_ledger_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty
|
||||
from `tabStock Ledger Entry`
|
||||
from `tabStock Ledger Entry` as sle
|
||||
where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s
|
||||
group by voucher_no, batch_no, item_code, warehouse
|
||||
order by item_code, warehouse"""
|
||||
|
||||
@@ -355,7 +355,7 @@ def get_opening_balance(filters, columns, sl_entries):
|
||||
)
|
||||
|
||||
# check if any SLEs are actually Opening Stock Reconciliation
|
||||
for sle in sl_entries:
|
||||
for sle in list(sl_entries):
|
||||
if (
|
||||
sle.get("voucher_type") == "Stock Reconciliation"
|
||||
and sle.get("date").split()[0] == filters.from_date
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<div class="product-container mt-4 {{ padding_top }} {{ info_col }}">
|
||||
<div class="item-content {{ 'mt-minus-2' if (show_tabs and tabs) else '' }}">
|
||||
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
|
||||
<div class="product-page-content">
|
||||
<!-- Product Specifications Table Section -->
|
||||
{% if show_tabs and tabs %}
|
||||
<div class="category-tabs">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user