Merge pull request #38953 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
rohitwaghchaure
2023-12-27 14:09:17 +05:30
committed by GitHub
35 changed files with 280 additions and 513 deletions

View File

@@ -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

View File

@@ -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"
}
},

View File

@@ -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"
},

View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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):

View File

@@ -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",

View File

@@ -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)

View File

@@ -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",
}
},
],

View File

@@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60,
"range3": 90,
"range4": 120,
"in_party_currency": 1,
}
data = execute(filters)

View File

@@ -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) {

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -123,8 +123,7 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"fieldname": "supplier_part_no",

View File

@@ -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(

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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,

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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();
}
});
}
}

View File

@@ -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"),
}
)

View File

@@ -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(

View File

@@ -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) {

View File

@@ -51,8 +51,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"default": "0.00",

View File

@@ -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")

View File

@@ -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);
},

View File

@@ -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",

View File

@@ -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);

View File

@@ -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."))

View 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"""
)
)

View File

@@ -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.

View File

@@ -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(