mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 15:48:29 +00:00
Merge pull request #38953 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -5,7 +5,7 @@ fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "erpnext.*"
|
||||
@@ -15,6 +15,10 @@ repos:
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
|
||||
@@ -53,8 +53,13 @@
|
||||
},
|
||||
"II. Forderungen und sonstige Vermögensgegenstände": {
|
||||
"is_group": 1,
|
||||
"Ford. a. Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1400",
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1410",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Durchlaufende Posten": {
|
||||
@@ -180,8 +185,13 @@
|
||||
},
|
||||
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1600",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -407,13 +407,10 @@
|
||||
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "9960"
|
||||
},
|
||||
"Debitoren": {
|
||||
"is_group": 1,
|
||||
"account_number": "10000"
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1200",
|
||||
"account_type": "Receivable"
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1210"
|
||||
@@ -1138,18 +1135,15 @@
|
||||
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "9964"
|
||||
},
|
||||
"Kreditoren": {
|
||||
"account_number": "70000",
|
||||
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1,
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"account_number": "70001",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
}
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "3310"
|
||||
},
|
||||
|
||||
@@ -138,8 +138,7 @@
|
||||
"label": "Against Voucher Type",
|
||||
"oldfieldname": "against_voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher",
|
||||
@@ -158,8 +157,7 @@
|
||||
"label": "Voucher Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
@@ -291,4 +289,4 @@
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
this.frm.doc.items.forEach((item) => {
|
||||
this.frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
|
||||
|
||||
|
||||
@@ -1660,10 +1660,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
|
||||
@@ -286,7 +286,6 @@
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -894,7 +893,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:26:05.629780",
|
||||
"modified": "2023-12-25 22:00:28.043555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -2388,10 +2388,6 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -149,11 +149,16 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
|
||||
@@ -181,13 +181,17 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
22
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
22
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
@@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision
|
||||
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
|
||||
# 7. For overpayment against an invoice with payment terms, there will be an additional row
|
||||
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
|
||||
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
|
||||
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
|
||||
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
|
||||
# 10. This report is based on Payment Ledger Entries
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -84,6 +84,9 @@ class ReceivablePayableReport(object):
|
||||
self.total_row_map = {}
|
||||
self.skip_total_row = 1
|
||||
|
||||
if self.filters.get("in_party_currency"):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
@@ -145,7 +148,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, ple):
|
||||
@@ -224,8 +227,7 @@ class ReceivablePayableReport(object):
|
||||
if not row:
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
@@ -260,8 +262,10 @@ class ReceivablePayableReport(object):
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
if total_row:
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
total_row["currency"] = row.get("currency", "")
|
||||
|
||||
def append_subtotal_row(self, party):
|
||||
sub_total_row = self.total_row_map.get(party)
|
||||
@@ -322,7 +326,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.append_subtotal_row(self.previous_party)
|
||||
if self.data:
|
||||
self.data.append(self.total_row_map.get("Total"))
|
||||
self.data.append(self.total_row_map.get("Total", {}))
|
||||
|
||||
def append_row(self, row):
|
||||
self.allocate_future_payments(row)
|
||||
@@ -453,7 +457,7 @@ class ReceivablePayableReport(object):
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
||||
@@ -579,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
@@ -616,6 +616,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
|
||||
@@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
||||
)
|
||||
else:
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@
|
||||
"oldfieldname": "item_code",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_part_no",
|
||||
|
||||
@@ -433,8 +433,11 @@ class SubcontractingController(StockController):
|
||||
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
|
||||
self.available_materials[key]["batch_no"][batch_no] = 0
|
||||
|
||||
if abs(qty) > 0 and not new_rm_obj:
|
||||
if new_rm_obj:
|
||||
self.remove(rm_obj)
|
||||
elif abs(qty) > 0:
|
||||
self.__set_consumed_qty(rm_obj, qty)
|
||||
|
||||
else:
|
||||
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
@@ -525,6 +528,10 @@ class SubcontractingController(StockController):
|
||||
(row.item_code, row.get(self.subcontract_data.order_field))
|
||||
] -= row.qty
|
||||
|
||||
def __reset_idx(self):
|
||||
for idx, item in enumerate(self.get(self.raw_material_table)):
|
||||
item.idx = idx + 1
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_subcontract_orders()
|
||||
@@ -532,6 +539,7 @@ class SubcontractingController(StockController):
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
self.__reset_idx()
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
|
||||
|
||||
@@ -214,6 +214,7 @@
|
||||
"options": "\nWork Order\nJob Card"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Conversion Rate",
|
||||
@@ -606,7 +607,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 12:47:58.514795",
|
||||
"modified": "2023-12-26 19:34:08.159312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -645,4 +646,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', {
|
||||
frappe.throw(__("Select the Warehouse"));
|
||||
}
|
||||
|
||||
frm.set_value("consider_minimum_order_qty", 0);
|
||||
|
||||
if (frm.doc.ignore_existing_ordered_qty) {
|
||||
frm.events.get_items_for_material_requests(frm);
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"material_request_planning",
|
||||
"include_non_stock_items",
|
||||
"include_subcontracted_items",
|
||||
"consider_minimum_order_qty",
|
||||
"include_safety_stock",
|
||||
"ignore_existing_ordered_qty",
|
||||
"column_break_25",
|
||||
@@ -423,13 +424,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "consider_minimum_order_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Minimum Order Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 14:08:11.928027",
|
||||
"modified": "2023-12-26 16:31:13.740777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -1135,7 +1135,14 @@ def get_subitems(
|
||||
|
||||
|
||||
def get_material_request_items(
|
||||
row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict
|
||||
doc,
|
||||
row,
|
||||
sales_order,
|
||||
company,
|
||||
ignore_existing_ordered_qty,
|
||||
include_safety_stock,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
):
|
||||
total_qty = row["qty"]
|
||||
|
||||
@@ -1144,8 +1151,14 @@ def get_material_request_items(
|
||||
required_qty = total_qty
|
||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty - bin_dict.get("projected_qty", 0)
|
||||
if required_qty > 0 and required_qty < row["min_order_qty"]:
|
||||
|
||||
if (
|
||||
doc.get("consider_minimum_order_qty")
|
||||
and required_qty > 0
|
||||
and required_qty < row["min_order_qty"]
|
||||
):
|
||||
required_qty = row["min_order_qty"]
|
||||
|
||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||
|
||||
if not row["purchase_uom"]:
|
||||
@@ -1483,6 +1496,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
if details.qty > 0:
|
||||
items = get_material_request_items(
|
||||
doc,
|
||||
details,
|
||||
sales_order,
|
||||
company,
|
||||
|
||||
@@ -1488,6 +1488,29 @@ class TestProductionPlan(FrappeTestCase):
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty)
|
||||
|
||||
def test_min_order_qty_in_pp(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name
|
||||
|
||||
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 10.0)
|
||||
|
||||
pln.consider_minimum_order_qty = 1
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 1000.0)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -355,5 +355,5 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.selling");
|
||||
|
||||
erpnext.sales_common = {
|
||||
setup_selling_controller:function() {
|
||||
erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales");
|
||||
this.frm.email_field = "contact_email";
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
this.setup_queries();
|
||||
this.frm.set_query('shipping_rule', function() {
|
||||
return {
|
||||
filters: {
|
||||
"shipping_rule_type": "Selling"
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
$.each([["customer", "customer"],
|
||||
["lead", "lead"]],
|
||||
function(i, opts) {
|
||||
if(me.frm.fields_dict[opts[0]])
|
||||
me.frm.set_query(opts[0], erpnext.queries[opts[1]]);
|
||||
});
|
||||
|
||||
me.frm.set_query('contact_person', erpnext.queries.contact_query);
|
||||
me.frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
|
||||
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.selling_price_list) {
|
||||
this.frm.set_query("selling_price_list", function() {
|
||||
return { filters: { selling: 1 } };
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict.tc_name) {
|
||||
this.frm.set_query("tc_name", function() {
|
||||
return { filters: { selling: 1 } };
|
||||
});
|
||||
}
|
||||
|
||||
if(!this.frm.fields_dict["items"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["packed_items"] &&
|
||||
this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) {
|
||||
this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) {
|
||||
return me.set_query_for_batch(doc, cdt, cdn)
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
|
||||
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
|
||||
return me.set_query_for_item_tax_template(doc, cdt, cdn)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
|
||||
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
|
||||
|
||||
this.frm.toggle_display("customer_name",
|
||||
(this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer));
|
||||
|
||||
this.toggle_editable_price_list_rate();
|
||||
}
|
||||
|
||||
customer() {
|
||||
var me = this;
|
||||
erpnext.utils.get_party_details(this.frm, null, null, function() {
|
||||
me.apply_price_list();
|
||||
});
|
||||
}
|
||||
|
||||
customer_address() {
|
||||
erpnext.utils.get_address_display(this.frm, "customer_address");
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
|
||||
}
|
||||
|
||||
shipping_address_name() {
|
||||
erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address");
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name");
|
||||
}
|
||||
|
||||
dispatch_address_name() {
|
||||
erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address");
|
||||
}
|
||||
|
||||
sales_partner() {
|
||||
this.apply_pricing_rule();
|
||||
}
|
||||
|
||||
campaign() {
|
||||
this.apply_pricing_rule();
|
||||
}
|
||||
|
||||
selling_price_list() {
|
||||
this.apply_price_list();
|
||||
this.set_dynamic_labels();
|
||||
}
|
||||
|
||||
discount_percentage(doc, cdt, cdn) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
item.discount_amount = 0.0;
|
||||
this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage');
|
||||
}
|
||||
|
||||
discount_amount(doc, cdt, cdn) {
|
||||
|
||||
if(doc.name === cdn) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
item.discount_percentage = 0.0;
|
||||
this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount');
|
||||
}
|
||||
|
||||
commission_rate() {
|
||||
this.calculate_commission();
|
||||
}
|
||||
|
||||
total_commission() {
|
||||
frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
|
||||
|
||||
const { amount_eligible_for_commission } = this.frm.doc;
|
||||
if(!amount_eligible_for_commission) return;
|
||||
|
||||
this.frm.set_value(
|
||||
"commission_rate", flt(
|
||||
this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
allocated_percentage(doc, cdt, cdn) {
|
||||
var sales_person = frappe.get_doc(cdt, cdn);
|
||||
if(sales_person.allocated_percentage) {
|
||||
|
||||
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
|
||||
precision("allocated_percentage", sales_person));
|
||||
|
||||
sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
|
||||
sales_person.allocated_percentage / 100.0,
|
||||
precision("allocated_amount", sales_person));
|
||||
refresh_field(["allocated_amount"], sales_person);
|
||||
|
||||
this.calculate_incentive(sales_person);
|
||||
refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name,
|
||||
sales_person.parentfield);
|
||||
}
|
||||
}
|
||||
|
||||
sales_person(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.calculate_incentive(row);
|
||||
refresh_field("incentives",row.name,row.parentfield);
|
||||
}
|
||||
|
||||
toggle_editable_price_list_rate() {
|
||||
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
|
||||
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
|
||||
|
||||
if(df && editable_price_list_rate) {
|
||||
const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item");
|
||||
if (!this.frm.fields_dict[parent_field]) return;
|
||||
|
||||
this.frm.fields_dict[parent_field].grid.update_docfield_property(
|
||||
'price_list_rate', 'read_only', 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
calculate_commission() {
|
||||
if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
|
||||
|
||||
if(this.frm.doc.commission_rate > 100) {
|
||||
this.frm.set_value("commission_rate", 100);
|
||||
frappe.throw(`${__(frappe.meta.get_label(
|
||||
this.frm.doc.doctype, "commission_rate", this.frm.doc.name
|
||||
))} ${__("cannot be greater than 100")}`);
|
||||
}
|
||||
|
||||
this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
|
||||
(sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
|
||||
)
|
||||
|
||||
this.frm.doc.total_commission = flt(
|
||||
this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
|
||||
precision("total_commission")
|
||||
);
|
||||
|
||||
refresh_field(["amount_eligible_for_commission", "total_commission"]);
|
||||
}
|
||||
|
||||
calculate_contribution() {
|
||||
var me = this;
|
||||
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
|
||||
frappe.model.round_floats_in(sales_person);
|
||||
if (!sales_person.allocated_percentage) return;
|
||||
|
||||
sales_person.allocated_amount = flt(
|
||||
me.frm.doc.amount_eligible_for_commission
|
||||
* sales_person.allocated_percentage
|
||||
/ 100.0,
|
||||
precision("allocated_amount", sales_person)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
calculate_incentive(row) {
|
||||
if(row.allocated_amount)
|
||||
{
|
||||
row.incentives = flt(
|
||||
row.allocated_amount * row.commission_rate / 100.0,
|
||||
precision("incentives", row));
|
||||
}
|
||||
}
|
||||
|
||||
set_dynamic_labels() {
|
||||
super.set_dynamic_labels();
|
||||
this.set_product_bundle_help(this.frm.doc);
|
||||
}
|
||||
|
||||
set_product_bundle_help(doc) {
|
||||
if(!this.frm.fields_dict.packing_list) return;
|
||||
if ((doc.packed_items || []).length) {
|
||||
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(true);
|
||||
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
var help_msg = "<div class='alert alert-warning'>" +
|
||||
__("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+
|
||||
"</div>";
|
||||
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg;
|
||||
}
|
||||
} else {
|
||||
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(false);
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = '';
|
||||
}
|
||||
}
|
||||
refresh_field('product_bundle_help');
|
||||
}
|
||||
|
||||
company_address() {
|
||||
var me = this;
|
||||
if(this.frm.doc.company_address) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.address.address.get_address_display",
|
||||
args: {"address_dict": this.frm.doc.company_address },
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
me.frm.set_value("company_address_display", r.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.frm.set_value("company_address_display", "");
|
||||
}
|
||||
}
|
||||
|
||||
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
|
||||
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
super.qty(doc, cdt, cdn);
|
||||
}
|
||||
|
||||
pick_serial_and_batch(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let me = this;
|
||||
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
|
||||
|
||||
item.title = item.has_serial_no ?
|
||||
__("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
|
||||
frappe.require(path, function() {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": Math.abs(r.total_qty)
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update_auto_repeat_reference(doc) {
|
||||
if (doc.auto_repeat) {
|
||||
frappe.call({
|
||||
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
|
||||
args:{
|
||||
docname: doc.auto_repeat,
|
||||
reference:doc.name
|
||||
},
|
||||
callback: function(r){
|
||||
if (r.message=="success") {
|
||||
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'});
|
||||
} else {
|
||||
frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
project() {
|
||||
let me = this;
|
||||
if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) {
|
||||
if(this.frm.doc.project) {
|
||||
frappe.call({
|
||||
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
|
||||
args: {project: this.frm.doc.project},
|
||||
callback: function(r, rt) {
|
||||
if(!r.exc) {
|
||||
$.each(me.frm.doc["items"] || [], function(i, row) {
|
||||
if(r.message) {
|
||||
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
|
||||
frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coupon_code() {
|
||||
this.frm.set_value("discount_amount", 0);
|
||||
this.frm.set_value("additional_discount_percentage", 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.pre_sales = {
|
||||
set_as_lost: function(doctype) {
|
||||
frappe.ui.form.on(doctype, {
|
||||
set_as_lost_dialog: function(frm) {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Set as Lost"),
|
||||
fields: [
|
||||
{
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": __("Lost Reasons"),
|
||||
"fieldname": "lost_reason",
|
||||
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": __("Competitors"),
|
||||
"fieldname": "competitors",
|
||||
"options": "Competitor Detail"
|
||||
},
|
||||
{
|
||||
"fieldtype": "Small Text",
|
||||
"label": __("Detailed Reason"),
|
||||
"fieldname": "detailed_reason"
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
let values = dialog.get_values();
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'declare_enquiry_lost',
|
||||
args: {
|
||||
'lost_reasons_list': values.lost_reason,
|
||||
'competitors': values.competitors ? values.competitors : [],
|
||||
'detailed_reason': values.detailed_reason
|
||||
},
|
||||
callback: function(r) {
|
||||
dialog.hide();
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Declare Lost')
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -702,6 +702,7 @@ def make_contact(args, is_primary_contact=1):
|
||||
else:
|
||||
values.update(
|
||||
{
|
||||
"first_name": args.get("customer_name"),
|
||||
"company_name": args.get("customer_name"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_price_list_rate_for
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -50,6 +50,42 @@ def get_columns(filters=None):
|
||||
]
|
||||
|
||||
|
||||
def fetch_item_prices(
|
||||
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
|
||||
):
|
||||
price_list_map = frappe._dict()
|
||||
ip = qb.DocType("Item Price")
|
||||
and_conditions = []
|
||||
or_conditions = []
|
||||
if items:
|
||||
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
|
||||
and_conditions.append(ip.selling == True)
|
||||
|
||||
or_conditions.append(ip.customer == None)
|
||||
or_conditions.append(ip.price_list == None)
|
||||
|
||||
if customer:
|
||||
or_conditions.append(ip.customer == customer)
|
||||
|
||||
if price_list:
|
||||
or_conditions.append(ip.price_list == price_list)
|
||||
|
||||
if selling_price_list:
|
||||
or_conditions.append(ip.price_list == selling_price_list)
|
||||
|
||||
res = (
|
||||
qb.from_(ip)
|
||||
.select(ip.item_code, ip.price_list, ip.price_list_rate)
|
||||
.where(Criterion.all(and_conditions))
|
||||
.where(Criterion.any(or_conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for x in res:
|
||||
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
|
||||
|
||||
return price_list_map
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
data = []
|
||||
customer_details = get_customer_details(filters)
|
||||
@@ -59,9 +95,17 @@ def get_data(filters=None):
|
||||
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
|
||||
)
|
||||
item_stock_map = {item.item_code: item.available for item in item_stock_map}
|
||||
price_list_map = fetch_item_prices(
|
||||
customer_details.customer,
|
||||
customer_details.price_list,
|
||||
customer_details.selling_price_list,
|
||||
items,
|
||||
)
|
||||
|
||||
for item in items:
|
||||
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
|
||||
price_list_rate = price_list_map.get(
|
||||
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
|
||||
)
|
||||
available_stock = item_stock_map.get(item.item_code)
|
||||
|
||||
data.append(
|
||||
|
||||
@@ -200,6 +200,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
item.serial_no = null;
|
||||
}
|
||||
|
||||
if (doc.docstatus === 0 && doc.is_return && !doc.return_against) {
|
||||
item.incoming_rate = 0.0;
|
||||
}
|
||||
|
||||
var has_batch_no;
|
||||
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
|
||||
has_batch_no = r && r.has_batch_no;
|
||||
@@ -428,6 +432,11 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
coupon_code() {
|
||||
this.frm.set_value("discount_amount", 0);
|
||||
this.frm.set_value("additional_discount_percentage", 0);
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on(cur_frm.doctype,"project", function(frm) {
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0.00",
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
UNUSED_INDEXES = [
|
||||
("Delivery Note", ["customer", "is_return", "return_against"]),
|
||||
("Sales Invoice", ["customer", "is_return", "return_against"]),
|
||||
("Purchase Invoice", ["supplier", "is_return", "return_against"]),
|
||||
("Purchase Receipt", ["supplier", "is_return", "return_against"]),
|
||||
]
|
||||
|
||||
|
||||
def execute():
|
||||
"""Drop unused return_against index"""
|
||||
for doctype, index_fields in UNUSED_INDEXES:
|
||||
table = f"tab{doctype}"
|
||||
index_name = frappe.db.get_index_name(index_fields)
|
||||
drop_index_if_exists(table, index_name)
|
||||
|
||||
|
||||
def drop_index_if_exists(table: str, index: str):
|
||||
if not frappe.db.has_index(table, index):
|
||||
return
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop unused index")
|
||||
frappe.log_error("Failed to drop index")
|
||||
|
||||
@@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (!frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
frm.doc.items.forEach((item) => {
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_order && item.purchase_order_item)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frm.events.add_custom_buttons(frm);
|
||||
},
|
||||
|
||||
|
||||
@@ -352,7 +352,6 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -1055,7 +1054,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:12:02.364608",
|
||||
"modified": "2023-12-25 22:32:09.801965",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', {
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
["actual_qty", "basic_rate"].forEach((field) => {
|
||||
let fields = ["actual_qty", "basic_rate"];
|
||||
if (frm.doc.purpose == "Material Receipt") {
|
||||
fields = ["actual_qty"];
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
|
||||
});
|
||||
frm.events.calculate_basic_amount(frm, child);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils import cint, flt, get_table_name, getdate
|
||||
from pypika import functions as fn
|
||||
|
||||
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
@@ -12,11 +12,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
SLE_COUNT_LIMIT = 10_000
|
||||
|
||||
|
||||
def _estimate_table_row_count(doctype: str):
|
||||
table = get_table_name(doctype)
|
||||
return cint(
|
||||
frappe.db.sql(
|
||||
f"""select table_rows
|
||||
from information_schema.tables
|
||||
where table_name = '{table}' ;"""
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
sle_count = frappe.db.count("Stock Ledger Entry")
|
||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
|
||||
24
erpnext/tests/test_perf.py
Normal file
24
erpnext/tests/test_perf.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
INDEXED_FIELDS = {
|
||||
"Bin": ["item_code"],
|
||||
"GL Entry": ["voucher_type", "against_voucher_type"],
|
||||
"Purchase Order Item": ["item_code"],
|
||||
"Stock Ledger Entry": ["warehouse"],
|
||||
}
|
||||
|
||||
|
||||
class TestPerformance(FrappeTestCase):
|
||||
def test_ensure_indexes(self):
|
||||
# These fields are not explicitly indexed BUT they are prefix in some
|
||||
# other composite index. If those are removed this test should be
|
||||
# updated accordingly.
|
||||
for doctype, fields in INDEXED_FIELDS.items():
|
||||
for field in fields:
|
||||
self.assertTrue(
|
||||
frappe.db.sql(
|
||||
f"""SHOW INDEX FROM `tab{doctype}`
|
||||
WHERE Column_name = "{field}" AND Seq_in_index = 1"""
|
||||
)
|
||||
)
|
||||
@@ -9104,3 +9104,7 @@ Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den
|
||||
Is Alternative,Ist Alternative,
|
||||
Alternative Items,Alternativpositionen,
|
||||
Component Type,Komponententyp,
|
||||
Lost Quotations,Verlorene Angebote,
|
||||
Lost Quotations %,Verlorene Angebote %,
|
||||
Lost Value,Verlorener Wert,
|
||||
Lost Value %,Verlorener Wert %,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater):
|
||||
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||
)
|
||||
|
||||
stop_actions = []
|
||||
for ref_dt, ref_dn_field, ref_link_field in ref_details:
|
||||
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||
@@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater):
|
||||
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||
if action == "Stop":
|
||||
if role_allowed_to_override not in frappe.get_roles():
|
||||
frappe.throw(
|
||||
stop_actions.append(
|
||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
|
||||
)
|
||||
@@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater):
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
if stop_actions:
|
||||
frappe.throw(stop_actions, as_list=True)
|
||||
|
||||
def get_reference_details(self, reference_names, reference_doctype):
|
||||
return frappe._dict(
|
||||
|
||||
Reference in New Issue
Block a user