Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29865

This commit is contained in:
Deepesh Garg
2022-02-28 11:43:01 +05:30
committed by GitHub
23 changed files with 191 additions and 97 deletions

View File

@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0, "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number, "invoice_number": row.invoice_number,
"disable_rounded_total": 1 "disable_rounded_total": 1
}) })

View File

@@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension, create_dimension,
@@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account, get_temporary_opening_account,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(ERPNextTestCase):
def setUp(self): @classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
create_dimension() create_dimension()
return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") invoices = self.make_invoices(company="_Test Opening Invoice Company")
try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status"], "keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"], 0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"], 1: ["_Test Customer 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value) self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0]) si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled # Check if update stock is not enabled
self.assertEqual(si.update_stock, 0) self.assertEqual(si.update_stock, 0)
finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"

View File

@@ -1078,7 +1078,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"): if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get all SO / PO which are not fully billed or aginst which full advance not paid # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = [] orders_to_be_billed = []
if (args.get("party_type") != "Student"): if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),

View File

@@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice):
self.paid_amount = 0 self.paid_amount = 0
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments: for pay in self.payments:
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")

View File

@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_account_head(tax, doc) validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc) validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)

View File

@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = [] companies = []
for account in doc.get("accounts"): for account in doc.get("accounts"):
@@ -331,6 +331,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
# validate if account is mapped for same company
validate_account_head(account.idx, account.account, account.company)
@frappe.whitelist() @frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None): def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

@@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', {
}); });
frappe.ui.form.on('Asset Repair Consumed Item', { frappe.ui.form.on('Asset Repair Consumed Item', {
item_code: function(frm, cdt, cdn) {
var item = locals[cdt][cdn];
let item_args = {
'item_code': item.item_code,
'warehouse': frm.doc.warehouse,
'qty': item.consumed_quantity,
'serial_no': item.serial_no,
'company': frm.doc.company
};
frappe.call({
method: 'erpnext.stock.utils.get_incoming_rate',
args: {
args: item_args
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
}
});
},
consumed_quantity: function(frm, cdt, cdn) { consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);

View File

@@ -13,12 +13,10 @@
], ],
"fields": [ "fields": [
{ {
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate", "fieldname": "valuation_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Valuation Rate", "label": "Valuation Rate"
"read_only": 1
}, },
{ {
"fieldname": "consumed_quantity", "fieldname": "consumed_quantity",
@@ -49,7 +47,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-11-11 18:23:00.492483", "modified": "2022-02-08 17:37:20.028290",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair Consumed Item", "name": "Asset Repair Consumed Item",

View File

@@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None tax.rate = None
def validate_account_head(tax, doc): def validate_account_head(idx, account, company):
company = frappe.get_cached_value('Account', account_company = frappe.get_cached_value('Account', account, 'company')
tax.account_head, 'company')
if company != doc.company: if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc): def validate_cost_center(tax, doc):

View File

@@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_sales_orders, get_sales_orders,
get_warehouse_list, get_warehouse_list,
) )
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -466,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase):
bom = make_bom(item=item, raw_materials=raw_materials) bom = make_bom(item=item, raw_materials=raw_materials)
# Create Production Plan # Create Production Plan
pln = create_production_plan(item_code=bom.item, planned_qty=10) pln = create_production_plan(item_code=bom.item, planned_qty=5)
# All the created Work Orders # All the created Work Orders
wo_list = [] wo_list = []
# Create and Submit 1st Work Order for 5 qty # Create and Submit 1st Work Order for 3 qty
create_work_order(item, pln, 5) create_work_order(item, pln, 3)
pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 3)
# Create and Submit 2nd Work Order for 2 qty
create_work_order(item, pln, 2)
pln.reload() pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 5) self.assertEqual(pln.po_items[0].ordered_qty, 5)
# Create and Submit 2nd Work Order for 3 qty # Overproduction
create_work_order(item, pln, 3) self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2)
pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 8)
# Cancel 1st Work Order # Cancel 1st Work Order
wo1 = frappe.get_doc("Work Order", wo_list[0]) wo1 = frappe.get_doc("Work Order", wo_list[0])
wo1.cancel() wo1.cancel()
pln.reload() pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 3) self.assertEqual(pln.po_items[0].ordered_qty, 2)
# Cancel 2nd Work Order # Cancel 2nd Work Order
wo2 = frappe.get_doc("Work Order", wo_list[1]) wo2 = frappe.get_doc("Work Order", wo_list[1])

View File

@@ -632,6 +632,21 @@ class WorkOrder(Document):
if not self.qty > 0: if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0.")) frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
if max_qty < 1:
frappe.throw(_("Cannot produce more item for {0}")
.format(self.production_item), OverProductionError)
elif self.qty > max_qty:
frappe.throw(_("Cannot produce more than {0} items for {1}")
.format(max_qty, self.production_item), OverProductionError)
def validate_transfer_against(self): def validate_transfer_against(self):
if not self.docstatus == 1: if not self.docstatus == 1:
# let user configure operations until they're ready to submit # let user configure operations until they're ready to submit

View File

@@ -2265,13 +2265,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}, },
coupon_code: function() { coupon_code: function() {
var me = this; if (this.frm.doc.coupon_code || this.frm._last_coupon_code) {
frappe.run_serially([ // reset pricing rules if coupon code is set or is unset
() => this.frm.doc.ignore_pricing_rule=1, const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule;
() => me.ignore_pricing_rule(), return frappe.run_serially([
() => this.frm.doc.ignore_pricing_rule=0, () => this.frm.doc.ignore_pricing_rule=1,
() => me.apply_pricing_rule() () => this.frm.trigger('ignore_pricing_rule'),
]); () => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule,
() => this.frm.trigger('apply_pricing_rule'),
() => this.frm._last_coupon_code = this.frm.doc.coupon_code
]);
}
} }
}); });

View File

@@ -304,12 +304,13 @@ erpnext.HierarchyChart = class {
} }
get_child_nodes(node_id) { get_child_nodes(node_id) {
let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: this.method, method: me.method,
args: { args: {
parent: node_id, parent: node_id,
company: this.company company: me.company
} }
}).then(r => resolve(r.message)); }).then(r => resolve(r.message));
}); });
@@ -350,12 +351,13 @@ erpnext.HierarchyChart = class {
} }
get_all_nodes() { get_all_nodes() {
let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', method: 'erpnext.utilities.hierarchy_chart.get_all_nodes',
args: { args: {
method: this.method, method: me.method,
company: this.company company: me.company
}, },
callback: (r) => { callback: (r) => {
resolve(r.message); resolve(r.message);
@@ -427,8 +429,8 @@ erpnext.HierarchyChart = class {
add_connector(parent_id, child_id) { add_connector(parent_id, child_id) {
// using pure javascript for better performance // using pure javascript for better performance
const parent_node = document.querySelector(`#${parent_id}`); const parent_node = document.getElementById(`${parent_id}`);
const child_node = document.querySelector(`#${child_id}`); const child_node = document.getElementById(`${child_id}`);
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');

View File

@@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class {
let me = this; let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: this.method, method: me.method,
args: { args: {
parent: node_id, parent: node_id,
company: me.company, company: me.company,
@@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class {
} }
add_connector(parent_id, child_id) { add_connector(parent_id, child_id) {
const parent_node = document.querySelector(`#${parent_id}`); const parent_node = document.getElementById(`${parent_id}`);
const child_node = document.querySelector(`#${child_id}`); const child_node = document.getElementById(`${child_id}`);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
@@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class {
level.nextAll('li').remove(); level.nextAll('li').remove();
let node_object = this.nodes[node.id]; let node_object = this.nodes[node.id];
let current_node = level.find(`#${node.id}`).detach(); let current_node = level.find(`[id="${node.id}"]`).detach();
current_node.removeClass('active-child active-path'); current_node.removeClass('active-child active-path');
node_object.expanded = 0; node_object.expanded = 0;

View File

@@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class {
}); });
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
if (!frm.doc.ignore_pricing_rule) { if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
if (frm.doc.coupon_code) { frappe.run_serially([
frappe.run_serially([ () => frm.doc.ignore_pricing_rule=1,
() => frm.doc.ignore_pricing_rule=1, () => frm.trigger('ignore_pricing_rule'),
() => frm.trigger('ignore_pricing_rule'), () => frm.doc.ignore_pricing_rule=0,
() => frm.doc.ignore_pricing_rule=0, () => frm.trigger('apply_pricing_rule'),
() => frm.trigger('apply_pricing_rule'), () => frm.save(),
() => frm.save(), () => this.update_totals_section(frm.doc)
() => this.update_totals_section(frm.doc) ]);
]); } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
} frappe.show_alert({
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
indicator: "orange"
});
} }
}); });

View File

@@ -399,6 +399,7 @@ class Item(Document):
if merge: if merge:
self.validate_properties_before_merge(new_name) self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge): def after_rename(self, old_name, new_name, merge):
@@ -463,6 +464,20 @@ class Item(Document):
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
if old_bundle and new_bundle:
bundle_link = get_link_to_form("Product Bundle", old_bundle)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
bundle_link, old_name, new_name
)
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_website_item_before_merge(self, old_name, new_name): def validate_duplicate_website_item_before_merge(self, old_name, new_name):
""" """
Block merge if both old and new items have website items against them. Block merge if both old and new items have website items against them.
@@ -480,8 +495,9 @@ class Item(Document):
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
web_item_link = get_link_to_form("Website Item", old_web_item) web_item_link = get_link_to_form("Website Item", old_web_item)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}" msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name): def set_last_purchase_rate(self, new_name):

View File

@@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import (
get_variant, get_variant,
) )
from erpnext.stock.doctype.item.item import ( from erpnext.stock.doctype.item.item import (
DataValidationError,
InvalidBarcode, InvalidBarcode,
StockExistsForTemplate, StockExistsForTemplate,
get_item_attribute, get_item_attribute,
@@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase):
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
create_item("Test Item Bundle Item 1", is_stock_item=False)
create_item("Test Item Bundle Item 2", is_stock_item=False)
create_item("Test Item inside Bundle")
bundle_items = ["Test Item inside Bundle"]
# make bundles for both items
bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
with self.assertRaises(DataValidationError):
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
bundle1.delete()
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
def test_uom_conversion_factor(self): def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'): if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM') frappe.delete_doc('Item', 'Test Item UOM')

View File

@@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', {
frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.set_serial_no(frm, cdt, cdn, () => {
frm.events.get_warehouse_details(frm, cdt, cdn); frm.events.get_warehouse_details(frm, cdt, cdn);
}); });
// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
let item = frappe.get_doc(cdt, cdn);
if (item.s_warehouse) {
item.allow_zero_valuation_rate = 0;
}
}, },
t_warehouse: function(frm, cdt, cdn) { t_warehouse: function(frm, cdt, cdn) {

View File

@@ -45,6 +45,7 @@ def get_sle(**args):
class TestStockEntry(ERPNextTestCase): class TestStockEntry(ERPNextTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase):
st1.set_stock_entry_type() st1.set_stock_entry_type()
st1.insert() st1.insert()
st1.submit() st1.submit()
st1.cancel()
frappe.set_user("Administrator") frappe.set_user("Administrator")
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
@@ -690,6 +692,8 @@ class TestStockEntry(ERPNextTestCase):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
"is_default": 1, "docstatus": 1}) "is_default": 1, "docstatus": 1})
make_item_variant() # make variant of _Test Variant Item if absent
work_order = frappe.new_doc("Work Order") work_order = frappe.new_doc("Work Order")
work_order.update({ work_order.update({
"company": "_Test Company", "company": "_Test Company",
@@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase):
# Check if FG cost is calculated based on RM total cost # Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50 # RM total cost = 200, FG rate = 200/4(FG qty) = 50
self.assertEqual(se.items[1].basic_rate, 50) self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value) self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
# teardown
se.delete()
def make_serialized_item(**args): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)
se = frappe.copy_doc(test_records[0]) se = frappe.copy_doc(test_records[0])

View File

@@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-03-29 18:22:12", "creation": "2022-02-05 00:17:49.860824",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
@@ -340,13 +340,13 @@
"label": "More Information" "label": "More Information"
}, },
{ {
"allow_on_submit": 1,
"default": "0", "default": "0",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"read_only_depends_on": "eval:doc.s_warehouse"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -556,12 +556,14 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-22 16:47:11.268975", "modified": "2022-02-26 00:51:24.963653",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -60,6 +60,9 @@ def add_invariant_check_fields(sles):
fifo_qty += qty fifo_qty += qty
fifo_value += qty * rate fifo_value += qty * rate
if sle.actual_qty < 0:
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
balance_qty += sle.actual_qty balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
@@ -145,9 +148,9 @@ def get_columns():
"label": "Incoming Rate", "label": "Incoming Rate",
}, },
{ {
"fieldname": "outgoing_rate", "fieldname": "consumption_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Outgoing Rate", "label": "Consumption Rate",
}, },
{ {
"fieldname": "qty_after_transaction", "fieldname": "qty_after_transaction",

View File

@@ -23,7 +23,6 @@ class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError): class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass pass
_exceptions = frappe.local('stockledger_exceptions')
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists from erpnext.controllers.stock_controller import future_sle_exists
@@ -459,6 +458,8 @@ class update_entries_after(object):
# rounding as per precision # rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
if not self.wh_data.qty_after_transaction:
self.wh_data.stock_value = 0.0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value self.wh_data.prev_stock_value = self.wh_data.stock_value
@@ -622,9 +623,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no: if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate: if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_incoming_value_for_serial_nos(self, sle, serial_nos): def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company # get rate from serial nos within same company
@@ -690,9 +689,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no: if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_fifo_values(self, sle): def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate) incoming_rate = flt(sle.incoming_rate)
@@ -723,9 +720,7 @@ class update_entries_after(object):
# Get valuation rate from last sle if exists or from valuation rate field in item master # Get valuation rate from last sle if exists or from valuation rate field in item master
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
_rate = get_valuation_rate(sle.item_code, sle.warehouse, _rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
else: else:
_rate = 0 _rate = 0
@@ -788,6 +783,13 @@ class update_entries_after(object):
else: else:
return 0 return 0
def get_fallback_rate(self, sle) -> float:
"""When exact incoming rate isn't available use any of other "average" rates as fallback.
This should only get used for negative stock."""
return get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_sle_before_datetime(self, args): def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket""" """get previous stock ledger entry before current time-bucket"""
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)

View File

@@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter,
Edit Details,Details bearbeiten, Edit Details,Details bearbeiten,
Edit Profile,Profil bearbeiten, Edit Profile,Profil bearbeiten,
Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich, Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
Email,Email, Email,E-Mail,
Email Campaigns,E-Mail-Kampagnen, Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft, Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen, Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6481,7 +6481,7 @@ Select Users,Wählen Sie Benutzer aus,
Send Emails At,Die E-Mails senden um, Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung, Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer, Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
email,Email, email,E-Mail,
Parent Department,Elternabteilung, Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste, Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.", Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
Can't render this file because it is too large.