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

chore: release v14
This commit is contained in:
Deepesh Garg
2023-05-16 22:28:33 +05:30
committed by GitHub
34 changed files with 610 additions and 100 deletions

View File

@@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
added = False
for voucher in vouchers:
@@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry)
frappe.throw(
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
)
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
self.reload()
@@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
)
unmatched_gles -= 1

View File

@@ -303,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
apply_tds(frm) {
var me = this;
me.frm.set_value("tax_withheld_vouchers", []);
if (!me.frm.doc.apply_tds) {
me.frm.set_value("tax_withholding_category", '');
me.frm.set_df_property("tax_withholding_category", "hidden", 1);

View File

@@ -19,14 +19,19 @@ def execute(filters=None):
return _execute(filters)
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
def _execute(
filters=None,
additional_table_columns=None,
additional_query_columns=None,
additional_conditions=None,
):
if not filters:
filters = {}
columns = get_columns(additional_table_columns, filters)
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
item_list = get_items(filters, additional_query_columns)
item_list = get_items(filters, additional_query_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
return columns
def get_conditions(filters):
def get_conditions(filters, additional_conditions=None):
conditions = ""
for opts in (
@@ -341,6 +346,9 @@ def get_conditions(filters):
if filters.get(opts[0]):
conditions += opts[1]
if additional_conditions:
conditions += additional_conditions
if filters.get("mode_of_payment"):
conditions += """ and exists(select name from `tabSales Invoice Payment`
where parent=`tabSales Invoice`.name
@@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
def get_items(filters, additional_query_columns):
conditions = get_conditions(filters)
def get_items(filters, additional_query_columns, additional_conditions=None):
conditions = get_conditions(filters, additional_conditions)
if additional_query_columns:
additional_query_columns = ", " + ", ".join(additional_query_columns)

View File

@@ -392,6 +392,9 @@ class AccountsController(TransactionBase):
)
def validate_inter_company_reference(self):
if self.get("is_return"):
return
if self.doctype not in ("Purchase Invoice", "Purchase Receipt"):
return
@@ -1679,6 +1682,9 @@ class AccountsController(TransactionBase):
d.base_payment_amount = flt(
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
)
else:
self.fetch_payment_terms_from_order(po_or_so, doctype)
self.ignore_default_payment_terms_template = 1
def get_order_details(self):
if self.doctype == "Sales Invoice":

View File

@@ -171,7 +171,7 @@ class SellingController(StockController):
self.round_floats_in(sales_person)
sales_person.allocated_amount = flt(
self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
flt(self.amount_eligible_for_commission) * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person),
)

View File

@@ -442,7 +442,43 @@ class StockController(AccountsController):
if not dimension:
continue
if row.get(dimension.source_fieldname):
if self.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Sales Invoice",
"Delivery Note",
"Stock Entry",
]:
if (
(
sl_dict.actual_qty > 0
and not self.get("is_return")
or sl_dict.actual_qty < 0
and self.get("is_return")
)
and self.doctype in ["Purchase Invoice", "Purchase Receipt"]
) or (
(
sl_dict.actual_qty < 0
and not self.get("is_return")
or sl_dict.actual_qty > 0
and self.get("is_return")
)
and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"]
):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
else:
fieldname_start_with = "to"
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
fieldname_start_with = "from"
fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}"
sl_dict[dimension.target_fieldname] = row.get(fieldname)
if not sl_dict.get(dimension.target_fieldname):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
elif row.get(dimension.source_fieldname):
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent:
@@ -734,6 +770,9 @@ class StockController(AccountsController):
}
)
if self.docstatus == 2:
force = True
if force or future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")

View File

@@ -741,7 +741,7 @@ class SubcontractingController(StockController):
sco_doc = frappe.get_doc("Subcontracting Order", sco)
sco_doc.update_status()
def set_missing_values_in_additional_costs(self):
def calculate_additional_costs(self):
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
if self.total_additional_costs:

View File

@@ -36,7 +36,7 @@ class TestSubcontractingController(FrappeTestCase):
sco.remove_empty_rows()
self.assertEqual((len_before - 1), len(sco.service_items))
def test_set_missing_values_in_additional_costs(self):
def test_calculate_additional_costs(self):
sco = get_subcontracting_order(do_not_submit=1)
rate_without_additional_cost = sco.items[0].rate

View File

@@ -165,6 +165,7 @@
"fieldname": "slide_3_content_align",
"fieldtype": "Select",
"label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0
},
{
@@ -214,6 +215,7 @@
"fieldname": "slide_4_content_align",
"fieldtype": "Select",
"label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0
},
{
@@ -263,6 +265,7 @@
"fieldname": "slide_5_content_align",
"fieldtype": "Select",
"label": "Content Align",
"options": "Left\nCentre\nRight",
"reqd": 0
},
{
@@ -274,7 +277,7 @@
}
],
"idx": 2,
"modified": "2021-02-24 15:57:05.889709",
"modified": "2023-05-12 15:03:57.604060",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Hero Slider",

View File

@@ -562,6 +562,7 @@ class JobCard(Document):
)
def update_work_order_data(self, for_quantity, time_in_mins, wo):
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
@@ -587,6 +588,7 @@ class JobCard(Document):
if data.get("workstation") != self.workstation:
# workstations can change in a job card
data.workstation = self.workstation
data.hour_rate = flt(workstation_hour_rate)
wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status()

View File

@@ -264,7 +264,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
// payment request
if(flt(doc.per_billed)<100) {
if(flt(doc.per_billed, precision('per_billed', doc)) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create'));
this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create'));
}

View File

@@ -299,7 +299,8 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
}
batch_no(doc, cdt, cdn) {
var me = this;
super.batch_no(doc, cdt, cdn);
var item = frappe.get_doc(cdt, cdn);
if (item.serial_no) {
@@ -378,10 +379,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
}
}
batch_no(doc, cdt, cdn) {
super.batch_no(doc, cdt, cdn);
}
qty(doc, cdt, cdn) {
super.qty(doc, cdt, cdn);

View File

@@ -281,7 +281,9 @@ def get_employee_email(employee_doc):
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
holiday_list, company = frappe.get_cached_value(
"Employee", employee, ["holiday_list", "company"]
)
else:
holiday_list = ""
company = frappe.db.get_single_value("Global Defaults", "default_company")

View File

@@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
if date is None:
date = today()
if holiday_list:
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
return bool(
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
)
else:
return False

View File

@@ -22,24 +22,18 @@
"creation": "2021-11-22 12:19:15.888642",
"docstatus": 0,
"doctype": "Module Onboarding",
"documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
"documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup",
"idx": 0,
"is_complete": 0,
"modified": "2022-06-07 14:31:00.575193",
"modified": "2023-05-16 13:13:24.043792",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
"owner": "Administrator",
"steps": [
{
"step": "Company Set Up"
},
{
"step": "Navigation Help"
},
{
"step": "Data import"
},
{
"step": "Create an Item"
},
@@ -51,12 +45,9 @@
},
{
"step": "Create a Quotation"
},
{
"step": "Letterhead"
}
],
"subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation",
"success_message": "Masters are all set up!",
"title": "Let's Set Up Some Masters"
"subtitle": "Item, Customer, Supplier, Navigation Help and Quotation",
"success_message": "You're ready to start your journey with ERPNext",
"title": "Let's begin your journey with ERPNext"
}

View File

@@ -5,11 +5,11 @@
"description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:22:18.317423",
"modified": "2023-05-15 09:18:42.895537",
"modified_by": "Administrator",
"name": "Company Set Up",
"owner": "Administrator",

View File

@@ -5,17 +5,17 @@
"description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:20:31.197564",
"modified": "2023-05-16 12:54:54.112364",
"modified_by": "Administrator",
"name": "Create a Customer",
"owner": "Administrator",
"reference_document": "Customer",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Manage Customers",
"title": "Create a Customer",
"validate_action": 1
}

View File

@@ -5,11 +5,11 @@
"description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:21:31.675330",
"modified": "2023-05-15 09:18:42.984170",
"modified_by": "Administrator",
"name": "Create a Quotation",
"owner": "Administrator",

View File

@@ -5,17 +5,17 @@
"description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:21:23.518301",
"modified": "2023-05-16 12:55:08.610113",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",
"reference_document": "Supplier",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Manage Suppliers",
"title": "Create a Supplier",
"validate_action": 1
}

View File

@@ -6,18 +6,18 @@
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "Item General",
"idx": 0,
"idx": 1,
"intro_video_url": "",
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:19:56.297772",
"modified": "2023-05-16 12:56:40.355878",
"modified_by": "Administrator",
"name": "Create an Item",
"owner": "Administrator",
"reference_document": "Item",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Manage Items",
"show_full_form": 0,
"title": "Create an Item",
"validate_action": 1
}

View File

@@ -5,11 +5,11 @@
"description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2022-06-07 14:28:51.390813",
"modified": "2023-05-15 09:18:42.962231",
"modified_by": "Administrator",
"name": "Data import",
"owner": "Administrator",

View File

@@ -5,11 +5,11 @@
"description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-12-15 14:21:39.037742",
"modified": "2023-05-15 09:18:42.995184",
"modified_by": "Administrator",
"name": "Letterhead",
"owner": "Administrator",

View File

@@ -2,14 +2,14 @@
"action": "Watch Video",
"action_label": "Learn about Navigation options",
"creation": "2021-11-22 12:09:52.233872",
"description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n",
"description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or AwesomeBar.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2022-06-07 14:28:00.901082",
"modified": "2023-05-16 12:53:25.939908",
"modified_by": "Administrator",
"name": "Navigation Help",
"owner": "Administrator",

View File

@@ -21,6 +21,10 @@ def boot_session(bootinfo):
bootinfo.sysdefaults.allow_stale = cint(
frappe.db.get_single_value("Accounts Settings", "allow_stale")
)
bootinfo.sysdefaults.over_billing_allowance = frappe.db.get_single_value(
"Accounts Settings", "over_billing_allowance"
)
bootinfo.sysdefaults.quotation_valid_till = cint(
frappe.db.get_single_value("CRM Settings", "default_valid_till")
)

View File

@@ -75,7 +75,16 @@ class InventoryDimension(Document):
self.delete_custom_fields()
def delete_custom_fields(self):
filters = {"fieldname": self.source_fieldname}
filters = {
"fieldname": (
"in",
[
self.source_fieldname,
f"to_{self.source_fieldname}",
f"from_{self.source_fieldname}",
],
)
}
if self.document_type:
filters["dt"] = self.document_type
@@ -88,6 +97,8 @@ class InventoryDimension(Document):
def reset_value(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = ""
self.istable = 0
for field in ["document_type", "condition"]:
self.set(field, None)
@@ -111,12 +122,35 @@ class InventoryDimension(Document):
def on_update(self):
self.add_custom_fields()
def add_custom_fields(self):
dimension_fields = [
@staticmethod
def get_insert_after_fieldname(doctype):
return frappe.get_all(
"DocField",
fields=["fieldname"],
filters={"parent": doctype},
order_by="idx desc",
limit=1,
)[0].fieldname
def get_dimension_fields(self, doctype=None):
if not doctype:
doctype = self.document_type
label_start_with = ""
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
label_start_with = "Target"
elif doctype in ["Sales Invoice Item", "Delivery Note Item", "Stock Entry Detail"]:
label_start_with = "Source"
label = self.dimension_name
if label_start_with:
label = f"{label_start_with} {self.dimension_name}"
return [
dict(
fieldname="inventory_dimension",
fieldtype="Section Break",
insert_after="warehouse",
insert_after=self.get_insert_after_fieldname(doctype),
label="Inventory Dimension",
collapsible=1,
),
@@ -125,24 +159,37 @@ class InventoryDimension(Document):
fieldtype="Link",
insert_after="inventory_dimension",
options=self.reference_document,
label=self.dimension_name,
label=label,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
),
]
def add_custom_fields(self):
custom_fields = {}
dimension_fields = []
if self.apply_to_all_doctypes:
for doctype in get_inventory_documents():
if not field_exists(doctype[0], self.source_fieldname):
custom_fields.setdefault(doctype[0], dimension_fields)
if field_exists(doctype[0], self.source_fieldname):
continue
dimension_fields = self.get_dimension_fields(doctype[0])
self.add_transfer_field(doctype[0], dimension_fields)
custom_fields.setdefault(doctype[0], dimension_fields)
elif not field_exists(self.document_type, self.source_fieldname):
dimension_fields = self.get_dimension_fields()
self.add_transfer_field(self.document_type, dimension_fields)
custom_fields.setdefault(self.document_type, dimension_fields)
if not frappe.db.get_value(
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
) and not field_exists("Stock Ledger Entry", self.target_fieldname):
if (
dimension_fields
and not frappe.db.get_value(
"Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname}
)
and not field_exists("Stock Ledger Entry", self.target_fieldname)
):
dimension_field = dimension_fields[1]
dimension_field["mandatory_depends_on"] = ""
dimension_field["reqd"] = 0
@@ -152,6 +199,53 @@ class InventoryDimension(Document):
if custom_fields:
create_custom_fields(custom_fields)
def add_transfer_field(self, doctype, dimension_fields):
if doctype not in [
"Stock Entry Detail",
"Sales Invoice Item",
"Delivery Note Item",
"Purchase Invoice Item",
"Purchase Receipt Item",
]:
return
fieldname_start_with = "to"
label_start_with = "Target"
display_depends_on = ""
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
fieldname_start_with = "from"
label_start_with = "Source"
display_depends_on = "eval:parent.is_internal_supplier == 1"
elif doctype != "Stock Entry Detail":
display_depends_on = "eval:parent.is_internal_customer == 1"
elif doctype == "Stock Entry Detail":
display_depends_on = "eval:parent.purpose != 'Material Issue'"
fieldname = f"{fieldname_start_with}_{self.source_fieldname}"
label = f"{label_start_with} {self.dimension_name}"
if field_exists(doctype, fieldname):
return
dimension_fields.extend(
[
dict(
fieldname="inventory_dimension_col_break",
fieldtype="Column Break",
insert_after=self.source_fieldname,
),
dict(
fieldname=fieldname,
fieldtype="Link",
insert_after="inventory_dimension_col_break",
options=self.reference_document,
label=label,
depends_on=display_depends_on,
),
]
)
def field_exists(doctype, fieldname) -> str or None:
return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name")
@@ -185,18 +279,19 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None):
dimensions = get_document_wise_inventory_dimensions(doc.doctype)
filter_dimensions = []
for row in dimensions:
if (
row.type_of_transaction == "Inward"
if doc.docstatus == 1
else row.type_of_transaction != "Inward"
) and sl_dict.actual_qty < 0:
continue
elif (
row.type_of_transaction == "Outward"
if doc.docstatus == 1
else row.type_of_transaction != "Outward"
) and sl_dict.actual_qty > 0:
continue
if row.type_of_transaction:
if (
row.type_of_transaction == "Inward"
if doc.docstatus == 1
else row.type_of_transaction != "Inward"
) and sl_dict.actual_qty < 0:
continue
elif (
row.type_of_transaction == "Outward"
if doc.docstatus == 1
else row.type_of_transaction != "Outward"
) and sl_dict.actual_qty > 0:
continue
evals = {"doc": doc}
if parent_doc:

View File

@@ -4,6 +4,7 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate, nowtime
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
@@ -12,6 +13,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
DoNotChangeError,
delete_dimension,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -20,6 +22,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestInventoryDimension(FrappeTestCase):
def setUp(self):
prepare_test_data()
create_store_dimension()
def test_validate_inventory_dimension(self):
# Can not be child doc
@@ -73,6 +76,8 @@ class TestInventoryDimension(FrappeTestCase):
self.assertFalse(custom_field)
def test_inventory_dimension(self):
frappe.local.document_wise_inventory_dimensions = {}
warehouse = "Shelf Warehouse - _TC"
item_code = "_Test Item"
@@ -143,6 +148,8 @@ class TestInventoryDimension(FrappeTestCase):
self.assertRaises(DoNotChangeError, inv_dim1.save)
def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self):
frappe.local.document_wise_inventory_dimensions = {}
inv_dimension = create_inventory_dimension(
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
)
@@ -250,6 +257,191 @@ class TestInventoryDimension(FrappeTestCase):
)
)
def test_for_purchase_sales_and_stock_transaction(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
create_inventory_dimension(
reference_document="Store",
type_of_transaction="Outward",
dimension_name="Store",
apply_to_all_doctypes=1,
)
item_code = "Test Inventory Dimension Item"
create_item(item_code)
warehouse = create_warehouse("Store Warehouse")
# Purchase Receipt -> Inward in Store 1
pr_doc = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True
)
pr_doc.items[0].store = "Store 1"
pr_doc.save()
pr_doc.submit()
entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"])
self.assertEqual(entries[0].warehouse, warehouse)
self.assertEqual(entries[0].store, "Store 1")
# Stock Entry -> Transfer from Store 1 to Store 2
se_doc = make_stock_entry(
item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True
)
se_doc.items[0].store = "Store 1"
se_doc.items[0].to_store = "Store 2"
se_doc.save()
se_doc.submit()
entries = get_voucher_sl_entries(
se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"]
)
for entry in entries:
self.assertEqual(entry.warehouse, warehouse)
if entry.actual_qty > 0:
self.assertEqual(entry.store, "Store 2")
self.assertEqual(entry.incoming_rate, 100.0)
else:
self.assertEqual(entry.store, "Store 1")
# Delivery Note -> Outward from Store 2
dn_doc = create_delivery_note(item_code=item_code, qty=10, warehouse=warehouse, do_not_save=True)
dn_doc.items[0].store = "Store 2"
dn_doc.save()
dn_doc.submit()
entries = get_voucher_sl_entries(dn_doc.name, ["warehouse", "store", "actual_qty"])
self.assertEqual(entries[0].warehouse, warehouse)
self.assertEqual(entries[0].store, "Store 2")
self.assertEqual(entries[0].actual_qty, -10.0)
return_dn = make_return_doc("Delivery Note", dn_doc.name)
return_dn.submit()
entries = get_voucher_sl_entries(return_dn.name, ["warehouse", "store", "actual_qty"])
self.assertEqual(entries[0].warehouse, warehouse)
self.assertEqual(entries[0].store, "Store 2")
self.assertEqual(entries[0].actual_qty, 10.0)
se_doc = make_stock_entry(
item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True
)
se_doc.items[0].store = "Store 2"
se_doc.items[0].to_store = "Store 1"
se_doc.save()
se_doc.submit()
return_pr = make_return_doc("Purchase Receipt", pr_doc.name)
return_pr.submit()
entries = get_voucher_sl_entries(return_pr.name, ["warehouse", "store", "actual_qty"])
self.assertEqual(entries[0].warehouse, warehouse)
self.assertEqual(entries[0].store, "Store 1")
self.assertEqual(entries[0].actual_qty, -10.0)
def test_inter_transfer_return_against_inventory_dimension(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
data = prepare_data_for_internal_transfer()
dn_doc = create_delivery_note(
customer=data.customer,
company=data.company,
warehouse=data.from_warehouse,
target_warehouse=data.to_warehouse,
qty=5,
cost_center=data.cost_center,
expense_account=data.expense_account,
do_not_submit=True,
)
dn_doc.items[0].store = "Inter Transfer Store 1"
dn_doc.items[0].to_store = "Inter Transfer Store 2"
dn_doc.save()
dn_doc.submit()
for d in get_voucher_sl_entries(dn_doc.name, ["store", "actual_qty"]):
if d.actual_qty > 0:
self.assertEqual(d.store, "Inter Transfer Store 2")
else:
self.assertEqual(d.store, "Inter Transfer Store 1")
pr_doc = make_inter_company_purchase_receipt(dn_doc.name)
pr_doc.items[0].warehouse = data.store_warehouse
pr_doc.items[0].from_store = "Inter Transfer Store 2"
pr_doc.items[0].store = "Inter Transfer Store 3"
pr_doc.save()
pr_doc.submit()
for d in get_voucher_sl_entries(pr_doc.name, ["store", "actual_qty"]):
if d.actual_qty > 0:
self.assertEqual(d.store, "Inter Transfer Store 3")
else:
self.assertEqual(d.store, "Inter Transfer Store 2")
return_doc = make_return_doc("Purchase Receipt", pr_doc.name)
return_doc.submit()
for d in get_voucher_sl_entries(return_doc.name, ["store", "actual_qty"]):
if d.actual_qty > 0:
self.assertEqual(d.store, "Inter Transfer Store 2")
else:
self.assertEqual(d.store, "Inter Transfer Store 3")
dn_doc.load_from_db()
return_doc1 = make_return_doc("Delivery Note", dn_doc.name)
return_doc1.posting_date = nowdate()
return_doc1.posting_time = nowtime()
return_doc1.items[0].target_warehouse = dn_doc.items[0].target_warehouse
return_doc1.items[0].warehouse = dn_doc.items[0].warehouse
return_doc1.save()
return_doc1.submit()
for d in get_voucher_sl_entries(return_doc1.name, ["store", "actual_qty"]):
if d.actual_qty > 0:
self.assertEqual(d.store, "Inter Transfer Store 1")
else:
self.assertEqual(d.store, "Inter Transfer Store 2")
def get_voucher_sl_entries(voucher_no, fields):
return frappe.get_all(
"Stock Ledger Entry", filters={"voucher_no": voucher_no}, fields=fields, order_by="creation"
)
def create_store_dimension():
if not frappe.db.exists("DocType", "Store"):
frappe.get_doc(
{
"doctype": "DocType",
"name": "Store",
"module": "Stock",
"custom": 1,
"naming_rule": "By fieldname",
"autoname": "field:store_name",
"fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
],
}
).insert(ignore_permissions=True)
for store in ["Store 1", "Store 2"]:
if not frappe.db.exists("Store", store):
frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True)
def prepare_test_data():
if not frappe.db.exists("DocType", "Shelf"):
@@ -326,3 +518,79 @@ def create_inventory_dimension(**args):
doc.insert(ignore_permissions=True)
return doc
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
company = "_Test Company with perpetual inventory"
customer = create_internal_customer(
"_Test Internal Customer 2",
company,
company,
)
supplier = create_internal_supplier(
"_Test Internal Supplier 2",
company,
company,
)
for store in ["Inter Transfer Store 1", "Inter Transfer Store 2", "Inter Transfer Store 3"]:
if not frappe.db.exists("Store", store):
frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True)
warehouse = create_warehouse("_Test Internal Warehouse New A", company=company)
to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company)
pr_doc = make_purchase_receipt(
company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True
)
pr_doc.items[0].store = "Inter Transfer Store 1"
pr_doc.submit()
if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"):
account = "Unrealized Profit and Loss - TCP1"
if not frappe.db.exists("Account", account):
frappe.get_doc(
{
"doctype": "Account",
"account_name": "Unrealized Profit and Loss",
"parent_account": "Direct Income - TCP1",
"company": company,
"is_group": 0,
"account_type": "Income Account",
}
).insert()
frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account)
cost_center = frappe.db.get_value("Company", company, "cost_center") or frappe.db.get_value(
"Cost Center", {"company": company}, "name"
)
expense_account = frappe.db.get_value(
"Company", company, "stock_adjustment_account"
) or frappe.db.get_value(
"Account", {"company": company, "account_type": "Expense Account"}, "name"
)
return frappe._dict(
{
"from_warehouse": warehouse,
"to_warehouse": to_warehouse,
"customer": customer,
"supplier": supplier,
"company": company,
"cost_center": cost_center,
"expense_account": expense_account,
"store_warehouse": frappe.db.get_value(
"Warehouse", {"name": ("like", "Store%"), "company": company}, "name"
),
}
)

View File

@@ -1,5 +1,5 @@
frappe.listview_settings['Item'] = {
add_fields: ["item_name", "stock_uom", "item_group", "image", "variant_of",
add_fields: ["item_name", "stock_uom", "item_group", "image",
"has_variants", "end_of_life", "disabled"],
filters: [["disabled", "=", "0"]],

View File

@@ -172,8 +172,8 @@ class PickList(Document):
if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
frappe.throw(
_(
f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
)
"You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}."
).format(row.item_code, row.sales_order)
)
@frappe.whitelist()

View File

@@ -376,3 +376,19 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
accounts_settings.acc_frozen_upto = ""
accounts_settings.save()
def test_create_repost_entry_for_cancelled_document(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
get_multiple_items=True,
)
self.assertTrue(pr.docstatus == 1)
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))
pr.load_from_db()
pr.cancel()
self.assertTrue(pr.docstatus == 2)
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))

View File

@@ -781,13 +781,21 @@ class update_entries_after(object):
d.db_update()
def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
else:
frappe.db.set_value(
"Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
"Subcontracting Receipt Supplied Item",
sle.voucher_detail_no,
{"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
)
scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
scr.calculate_items_qty_and_amount()
scr.db_update()
for d in scr.items:
d.db_update()
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)

View File

@@ -77,22 +77,22 @@ class SubcontractingOrder(SubcontractingController):
frappe.throw(_(msg))
def set_missing_values(self):
self.set_missing_values_in_additional_costs()
self.set_missing_values_in_service_items()
self.set_missing_values_in_supplied_items()
self.set_missing_values_in_items()
self.calculate_additional_costs()
self.calculate_service_costs()
self.calculate_supplied_items_qty_and_amount()
self.calculate_items_qty_and_amount()
def set_missing_values_in_service_items(self):
def calculate_service_costs(self):
for idx, item in enumerate(self.get("service_items")):
self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty
def set_missing_values_in_supplied_items(self):
def calculate_supplied_items_qty_and_amount(self):
for item in self.get("items"):
bom = frappe.get_doc("BOM", item.bom)
rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items)
item.rm_cost_per_qty = rm_cost / flt(bom.quantity)
def set_missing_values_in_items(self):
def calculate_items_qty_and_amount(self):
total_qty = total = 0
for item in self.items:
item.rate = item.rm_cost_per_qty + item.service_cost_per_qty + flt(item.additional_cost_per_qty)

View File

@@ -113,9 +113,9 @@ class SubcontractingReceipt(SubcontractingController):
@frappe.whitelist()
def set_missing_values(self):
self.set_missing_values_in_additional_costs()
self.set_missing_values_in_supplied_items()
self.set_missing_values_in_items()
self.calculate_additional_costs()
self.calculate_supplied_items_qty_and_amount()
self.calculate_items_qty_and_amount()
def set_available_qty_for_consumption(self):
supplied_items_details = {}
@@ -147,13 +147,13 @@ class SubcontractingReceipt(SubcontractingController):
item.rm_item_code, 0
)
def set_missing_values_in_supplied_items(self):
def calculate_supplied_items_qty_and_amount(self):
for item in self.get("supplied_items") or []:
item.amount = item.rate * item.consumed_qty
self.set_available_qty_for_consumption()
def set_missing_values_in_items(self):
def calculate_items_qty_and_amount(self):
rm_supp_cost = {}
for item in self.get("supplied_items") or []:
if item.reference_name in rm_supp_cost:

View File

@@ -6,7 +6,7 @@ import copy
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint, flt
from frappe.utils import add_days, cint, cstr, flt, today
import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -26,6 +26,9 @@ from erpnext.controllers.tests.test_subcontracting_controller import (
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
@@ -528,6 +531,69 @@ class TestSubcontractingReceipt(FrappeTestCase):
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
def test_supplied_items_cost_after_reposting(self):
# Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Create Material Receipt for RM's
make_stock_entry(
item_code="_Test Item",
qty=100,
target="_Test Warehouse 1 - _TC",
basic_rate=100,
posting_date=add_days(today(), -2),
)
make_stock_entry(
item_code="_Test Item Home Desktop 100",
qty=100,
target="_Test Warehouse 1 - _TC",
basic_rate=100,
)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": "_Test FG Item",
"fg_item_qty": 10,
},
]
# Create Subcontracting Order
sco = get_subcontracting_order(service_items=service_items)
# Transfer RM's
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
# Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
# Create Backdated Stock Reconciliation
sr = create_stock_reconciliation(
item_code=rm_items[0].get("item_code"),
warehouse="_Test Warehouse 1 - _TC",
qty=100,
rate=50,
posting_date=add_days(today(), -1),
)
# Cost should be updated in Subcontracting Receipt after reposting
prev_cost = scr.supplied_items[0].rate
scr.load_from_db()
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -776,6 +776,9 @@ def on_communication_update(doc, status):
if not parent.meta.has_field("service_level_agreement"):
return
if not parent.get("service_level_agreement"):
return
if (
doc.sent_or_received == "Received" # a reply is received
and parent.get("status") == "Open" # issue status is set as open from communication.py