mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 11:55:11 +00:00
Merge pull request #35324 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"]],
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user