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

chore: release v14
This commit is contained in:
Deepesh Garg
2023-12-05 22:45:16 +05:30
committed by GitHub
56 changed files with 1258 additions and 394 deletions

View File

@@ -192,7 +192,7 @@ class ExchangeRateRevaluation(Document):
# round off balance based on currency precision
# and consider debit-credit difference allowance
currency_precision = get_currency_precision()
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
rounding_loss_allowance = float(rounding_loss_allowance)
for acc in account_details:
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
},
refresh: function(frm) {

View File

@@ -98,6 +98,8 @@ class JournalEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
)
self.make_gl_entries(1)
self.update_advance_paid()

View File

@@ -9,6 +9,8 @@ import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money
from pypika import Case
from pypika.functions import Coalesce, Sum
import erpnext
from erpnext.accounts.doctype.bank_account.bank_account import (
@@ -1566,12 +1568,13 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
if not split_rows:
continue
frappe.msgprint(
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
),
alert=True,
)
if len(split_rows) > 1:
frappe.msgprint(
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
),
alert=True,
)
outstanding_invoices_after_split += split_rows
continue
@@ -1853,18 +1856,24 @@ def get_company_defaults(company):
def get_outstanding_on_journal_entry(name):
res = frappe.db.sql(
"SELECT "
'CASE WHEN party_type IN ("Customer") '
"THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
"ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
"END as outstanding_amount "
"FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) "
"AND party_type IS NOT NULL "
'AND party_type != ""',
(name, name),
as_dict=1,
)
gl = frappe.qb.DocType("GL Entry")
res = (
frappe.qb.from_(gl)
.select(
Case()
.when(
gl.party_type == "Customer",
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
)
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
.as_("outstanding_amount")
)
.where(
(Coalesce(gl.party_type, "") != "")
& (gl.is_cancelled == 0)
& ((gl.voucher_no == name) | (gl.against_voucher == name))
)
).run(as_dict=True)
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0

View File

@@ -203,9 +203,10 @@
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"is_virtual": 1,
"issingle": 1,
"links": [],
"modified": "2023-08-15 05:35:50.109290",
"modified": "2023-11-17 17:33:55.701726",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -230,6 +231,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"states": []
}

View File

@@ -29,6 +29,58 @@ class PaymentReconciliation(Document):
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
doc_dict = frappe._dict(
{
"modified": None,
"company": None,
"party": None,
"party_type": None,
"receivable_payable_account": None,
"default_advance_account": None,
"from_invoice_date": None,
"to_invoice_date": None,
"invoice_limit": 50,
"from_payment_date": None,
"to_payment_date": None,
"payment_limit": 50,
"minimum_invoice_amount": None,
"minimum_payment_amount": None,
"maximum_invoice_amount": None,
"maximum_payment_amount": None,
"bank_cash_account": None,
"cost_center": None,
"payment_name": None,
"invoice_name": None,
}
)
super(Document, self).__init__(doc_dict)
def save(self):
return
@staticmethod
def get_list(args):
pass
@staticmethod
def get_count(args):
pass
@staticmethod
def get_stats(args):
pass
def db_insert(self, *args, **kwargs):
pass
def db_update(self, *args, **kwargs):
pass
def delete(self):
pass
@frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()

View File

@@ -159,9 +159,10 @@
"label": "Difference Posting Date"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2023-10-23 10:44:56.066303",
"modified": "2023-11-17 17:33:38.612615",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -71,9 +71,10 @@
"label": "Exchange Rate"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2022-11-08 18:18:02.502149",
"modified": "2023-11-17 17:33:45.455166",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",

View File

@@ -107,9 +107,10 @@
"options": "Cost Center"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2023-09-03 07:43:29.965353",
"modified": "2023-11-17 17:33:34.818530",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format

View File

@@ -1290,6 +1290,8 @@ class PurchaseInvoice(BuyingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payment",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)
@@ -1703,6 +1705,4 @@ def make_purchase_receipt(source_name, target_doc=None):
target_doc,
)
doc.set_onload("ignore_price_list", True)
return doc

View File

@@ -286,6 +286,7 @@
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
"reqd": 1
},
{
@@ -893,7 +894,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:33:48.547297",
"modified": "2023-11-30 16:26:05.629780",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -1934,7 +1934,6 @@ def make_delivery_note(source_name, target_doc=None):
set_missing_values,
)
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -195,7 +195,7 @@ def set_address_details(
company_address=None,
shipping_address=None,
*,
ignore_permissions=False
ignore_permissions=False,
):
billing_address_field = (
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
@@ -239,7 +239,7 @@ def set_address_details(
shipping_address_display=render_address(
shipping_address, check_permissions=not ignore_permissions
),
**get_fetch_values(doctype, "shipping_address", shipping_address)
**get_fetch_values(doctype, "shipping_address", shipping_address),
)
if party_details.company_address:
@@ -250,7 +250,7 @@ def set_address_details(
party_details.company_address_display
or render_address(party_details.company_address, check_permissions=False)
),
**get_fetch_values(doctype, "billing_address", party_details.company_address)
**get_fetch_values(doctype, "billing_address", party_details.company_address),
)
# shipping address - if not already set
@@ -258,7 +258,7 @@ def set_address_details(
party_details.update(
shipping_address=party_details.billing_address,
shipping_address_display=party_details.billing_address_display,
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
)
party_address, shipping_address = (
@@ -956,6 +956,9 @@ def get_partywise_advanced_payment_amount(
if party:
query = query.where(ple.party == party)
if invoice_doctypes := frappe.get_hooks("invoice_doctypes"):
query = query.where(ple.voucher_type.notin(invoice_doctypes))
data = query.run()
if data:
return frappe._dict(data)

View File

@@ -1085,7 +1085,7 @@ class ReceivablePayableReport(object):
)
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200)
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
if not fieldname:

View File

@@ -282,7 +282,8 @@ def get_conditions(filters):
if accounting_dimensions:
for dimension in accounting_dimensions:
if not dimension.disabled:
# Ignore 'Finance Book' set up as dimension in below logic, as it is already handled in above section
if not dimension.disabled and dimension.document_type != "Finance Book":
if filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
filters[dimension.fieldname] = get_dimension_with_children(

View File

@@ -1809,6 +1809,8 @@ class QueryPaymentLedger(object):
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_against_voucher_no))
.where(Criterion.all(self.common_filter))
.where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date))
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
.orderby(ple.posting_date, ple.voucher_no)
.having(qb.Field("amount_in_account_currency") > 0)

View File

@@ -322,16 +322,16 @@ frappe.ui.form.on('Asset', {
},
make_schedules_editable: function(frm) {
if (frm.doc.finance_books) {
if (frm.doc.finance_books.length) {
var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
? true : false;
var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0
? true : false;
frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable);
frm.toggle_enable("schedules", is_manual_hence_editable || is_shift_hence_editable);
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_manual_hence_editable);
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable);
frm.fields_dict["schedules"].grid.toggle_enable("shift", is_shift_hence_editable);
}
},

View File

@@ -15,6 +15,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
class TestAssetValueAdjustment(unittest.TestCase):
def setUp(self):
create_asset_data()
frappe.db.set_value(
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
)
def test_current_asset_value(self):
pr = make_purchase_receipt(

View File

@@ -86,6 +86,10 @@ class PurchaseOrder(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
mri_compare_fields = [["project", "="], ["item_code", "="]]
if self.is_subcontracted:
mri_compare_fields = [["project", "="]]
super(PurchaseOrder, self).validate_with_previous_doc(
{
"Supplier Quotation": {
@@ -108,7 +112,7 @@ class PurchaseOrder(BuyingController):
},
"Material Request Item": {
"ref_dn_field": "material_request_item",
"compare_fields": [["project", "="], ["item_code", "="]],
"compare_fields": mri_compare_fields,
"is_child_table": True,
},
}
@@ -282,23 +286,6 @@ class PurchaseOrder(BuyingController):
check_list.append(d.material_request)
check_on_hold_or_closed_status("Material Request", d.material_request)
def update_requested_qty(self):
material_request_map = {}
for d in self.get("items"):
if d.material_request_item:
material_request_map.setdefault(d.material_request, []).append(d.material_request_item)
for mr, mr_item_rows in material_request_map.items():
if mr and mr_item_rows:
mr_obj = frappe.get_doc("Material Request", mr)
if mr_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(
_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError
)
mr_obj.update_requested_qty(mr_item_rows)
def update_ordered_qty(self, po_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
item_wh_list = []
@@ -340,7 +327,9 @@ class PurchaseOrder(BuyingController):
self.update_status_updater()
self.update_prevdoc_status()
self.update_requested_qty()
if not self.is_subcontracted or self.is_old_subcontracting_flow:
self.update_requested_qty()
self.update_ordered_qty()
self.validate_budget()
self.update_reserved_qty_for_subcontract()
@@ -372,7 +361,9 @@ class PurchaseOrder(BuyingController):
# Must be called after updating ordered qty in Material Request
# bin uses Material Request Items to recalculate & update
self.update_requested_qty()
if not self.is_subcontracted or self.is_old_subcontracting_flow:
self.update_requested_qty()
self.update_ordered_qty()
self.update_blanket_order()
@@ -450,6 +441,20 @@ class PurchaseOrder(BuyingController):
else:
self.db_set("per_received", 0, update_modified=False)
def update_ordered_qty_in_so_for_removed_items(self, removed_items):
"""
Updates ordered_qty in linked SO when item rows are removed using Update Items
"""
if not self.is_against_so():
return
for item in removed_items:
prev_ordered_qty = frappe.get_cached_value(
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
)
frappe.db.set_value(
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
)
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""
@@ -536,8 +541,6 @@ def make_purchase_receipt(source_name, target_doc=None):
set_missing_values,
)
doc.set_onload("ignore_price_list", True)
return doc
@@ -617,7 +620,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
postprocess,
ignore_permissions=ignore_permissions,
)
doc.set_onload("ignore_price_list", True)
return doc
@@ -679,7 +681,10 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
},
"Purchase Order Item": {
"doctype": "Subcontracting Order Service Item",
"field_map": {},
"field_map": {
"material_request": "material_request",
"material_request_item": "material_request_item",
},
"field_no_map": [],
},
},
@@ -705,8 +710,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
@frappe.whitelist()
def is_subcontracting_order_created(po_name) -> bool:
count = frappe.db.count(
"Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]}
return (
True
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
else False
)
return True if count else False

View File

@@ -168,7 +168,6 @@ def make_purchase_order(source_name, target_doc=None):
set_missing_values,
)
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -2874,6 +2874,9 @@ def validate_and_delete_children(parent, data) -> bool:
d.cancel()
d.delete()
if parent.doctype == "Purchase Order":
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
# need to update ordered qty in Material Request first
# bin uses Material Request Items to recalculate & update
parent.update_prevdoc_status()
@@ -3129,7 +3132,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if parent_doctype == "Purchase Order":
update_last_purchase_rate(parent, is_submit=1)
parent.update_prevdoc_status()
if any_qty_changed or items_added_or_removed or any_conversion_factor_changed:
parent.update_prevdoc_status()
parent.update_requested_qty()
parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty()

View File

@@ -530,8 +530,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
set_missing_values,
)
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -789,6 +789,23 @@ class SubcontractingController(StockController):
return self._sub_contracted_items
def update_requested_qty(self):
material_request_map = {}
for d in self.get("items"):
if d.material_request_item:
material_request_map.setdefault(d.material_request, []).append(d.material_request_item)
for mr, mr_item_rows in material_request_map.items():
if mr and mr_item_rows:
mr_obj = frappe.get_doc("Material Request", mr)
if mr_obj.status in ["Stopped", "Cancelled"]:
frappe.throw(
_("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError
)
mr_obj.update_requested_qty(mr_item_rows)
def get_item_details(items):
item = frappe.qb.DocType("Item")

View File

@@ -938,6 +938,7 @@ def make_subcontracted_items():
"Subcontracted Item SA5": {},
"Subcontracted Item SA6": {},
"Subcontracted Item SA7": {},
"Subcontracted Item SA8": {},
}
for item, properties in sub_contracted_items.items():
@@ -957,6 +958,7 @@ def make_raw_materials():
},
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
"Subcontracted SRM Item 8": {},
}
for item, properties in raw_materials.items():
@@ -980,6 +982,7 @@ def make_service_items():
"Subcontracted Service Item 5": {},
"Subcontracted Service Item 6": {},
"Subcontracted Service Item 7": {},
"Subcontracted Service Item 8": {},
}
for item, properties in service_items.items():
@@ -1003,6 +1006,7 @@ def make_bom_for_subcontracted_items():
"Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
}
for item_code, raw_materials in boms.items():

View File

@@ -40,7 +40,7 @@ class Lead(SellingController, CRMNote):
if self.source == "Existing Customer" and self.customer:
contact = frappe.db.get_value(
"Dynamic Link",
{"link_doctype": "Customer", "link_name": self.customer},
{"link_doctype": "Customer", "parenttype": "Contact", "link_name": self.customer},
"parent",
)
if contact:

View File

@@ -1306,7 +1306,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
order_by = "idx desc, name, item_name"
fields = ["name", "item_group", "item_name", "description"]
fields = ["name", "item_name", "item_group", "description"]
fields.extend(
[field for field in searchfields if not field in ["name", "item_group", "description"]]
)

View File

@@ -213,29 +213,27 @@ class JobCard(Document):
production_capacity = 1
query = query.where(jctl.employee == args.get("employee"))
existing = query.run(as_dict=True)
existing_time_logs = query.run(as_dict=True)
overlap_count = self.get_overlap_count(existing)
if existing and production_capacity > overlap_count:
return
if not self.has_overlap(production_capacity, existing_time_logs):
return {}
if self.workstation_type:
if workstation := self.get_workstation_based_on_available_slot(existing):
if workstation := self.get_workstation_based_on_available_slot(existing_time_logs):
self.workstation = workstation
return None
return existing[0] if existing else None
return existing_time_logs[0] if existing_time_logs else None
@staticmethod
def get_overlap_count(time_logs):
count = 1
def has_overlap(self, production_capacity, time_logs):
overlap = False
if production_capacity == 1 and len(time_logs) > 0:
return True
# Check overlap exists or not between the overlapping time logs with the current Job Card
for idx, row in enumerate(time_logs):
next_idx = idx
if idx + 1 < len(time_logs):
next_idx = idx + 1
next_row = time_logs[next_idx]
for row in time_logs:
count = 1
for next_row in time_logs:
if row.name == next_row.name:
continue
@@ -255,7 +253,10 @@ class JobCard(Document):
):
count += 1
return count
if count > production_capacity:
return True
return overlap
def get_workstation_based_on_available_slot(self, existing) -> Optional[str]:
workstations = get_workstations(self.workstation_type)

View File

@@ -1521,19 +1521,23 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
)
locations = get_available_item_locations(
item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True
item.get("item_code"),
warehouses,
item.get("quantity") * item.get("conversion_factor"),
company,
ignore_validation=True,
)
required_qty = item.get("quantity")
if item.get("conversion_factor") and item.get("purchase_uom") != item.get("stock_uom"):
# Convert qty to stock UOM
required_qty = required_qty * item.get("conversion_factor")
# get available material by transferring to production warehouse
for d in locations:
if required_qty <= 0:
return
conversion_factor = 1.0
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
@@ -1543,10 +1547,11 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
"material_request_type": "Material Transfer",
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse"),
"conversion_factor": 1.0,
}
)
required_qty -= quantity / conversion_factor
required_qty -= quantity
new_mr_items.append(new_dict)
# raise purchase request for remaining qty
@@ -1558,7 +1563,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
required_qty = ceil(required_qty)
item["quantity"] = required_qty
item["quantity"] = required_qty / item.get("conversion_factor")
new_mr_items.append(item)

View File

@@ -1272,12 +1272,14 @@ class TestProductionPlan(FrappeTestCase):
for row in items:
row = frappe._dict(row)
if row.material_request_type == "Material Transfer":
self.assertTrue(row.uom == row.stock_uom)
self.assertTrue(row.from_warehouse in [wh1, wh2])
self.assertEqual(row.quantity, 2)
if row.material_request_type == "Purchase":
self.assertTrue(row.uom != row.stock_uom)
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12)
self.assertEqual(row.quantity, 12.0)
def test_mr_qty_for_same_rm_with_different_sub_assemblies(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
@@ -1393,6 +1395,58 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty)
def test_material_request_qty_purchase_and_material_transfer(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
bom_item = make_item(
properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
).name
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
make_stock_entry(
item_code=bom_item,
qty=60,
target=store_warehouse,
rate=99,
)
if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
doc = frappe.get_doc("Item", bom_item)
doc.append("uoms", {"uom": "Nos", "conversion_factor": 10})
doc.save()
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
)
pln.for_warehouse = rm_warehouse
items = get_items_for_material_requests(
pln.as_dict(), warehouses=[{"warehouse": store_warehouse}]
)
for row in items:
self.assertEqual(row.get("quantity"), 10.0)
self.assertEqual(row.get("material_request_type"), "Material Transfer")
self.assertEqual(row.get("uom"), "_Test UOM 1")
self.assertEqual(row.get("from_warehouse"), store_warehouse)
self.assertEqual(row.get("conversion_factor"), 1.0)
items = get_items_for_material_requests(
pln.as_dict(), warehouses=[{"warehouse": pln.for_warehouse}]
)
for row in items:
self.assertEqual(row.get("quantity"), 1.0)
self.assertEqual(row.get("material_request_type"), "Purchase")
self.assertEqual(row.get("uom"), "Nos")
self.assertEqual(row.get("conversion_factor"), 10.0)
def create_production_plan(**args):
"""

View File

@@ -270,6 +270,7 @@ erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v14_0.france_depreciation_warning
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -344,13 +345,12 @@ erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
erpnext.patches.v14_0.rename_over_order_allowance_field
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
erpnext.patches.v14_0.add_default_for_repost_settings
erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@@ -0,0 +1,17 @@
from frappe import qb
def execute():
"""
Clear `tabSingles` and Payment Reconciliation tables of values
"""
singles = qb.DocType("Singles")
qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run()
doctypes = [
"Payment Reconciliation Invoice",
"Payment Reconciliation Payment",
"Payment Reconciliation Allocation",
]
for x in doctypes:
dt = qb.DocType(x)
qb.from_(dt).delete().run()

View File

@@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", {
frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
frm.trigger('make_issue_from_communication');
})
}, "Create");
}, __("Create"));
}
if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {

View File

@@ -328,7 +328,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
onload_post_render() {
if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length
&& !(this.frm.doc.__onload ? this.frm.doc.__onload.load_after_mapping : false)) {
&& !this.frm.doc.__onload?.load_after_mapping) {
frappe.after_ajax(() => this.apply_default_taxes());
} else if(this.frm.doc.__islocal && this.frm.doc.company && this.frm.doc["items"]
&& !this.frm.doc.is_pos) {
@@ -918,9 +918,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let me = this;
this.set_dynamic_labels();
let company_currency = this.get_company_currency();
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
// Added `load_after_mapping` to determine if document is loading after mapping from another doc
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
&& !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
&& !this.frm.doc.__onload?.load_after_mapping) {
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
function(exchange_rate) {
@@ -952,7 +952,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if(flt(this.frm.doc.conversion_rate)>0.0) {
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
if(this.frm.doc.__onload?.load_after_mapping) {
this.calculate_taxes_and_totals();
} else if (!this.in_apply_price_list){
this.apply_price_list();
@@ -1039,9 +1039,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.set_dynamic_labels();
var company_currency = this.get_company_currency();
// Added `ignore_price_list` to determine if document is loading after mapping from another doc
// Added `load_after_mapping` to determine if document is loading after mapping from another doc
if(this.frm.doc.price_list_currency !== company_currency &&
!(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) {
!this.frm.doc.__onload?.load_after_mapping) {
this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency,
function(exchange_rate) {
me.frm.set_value("plc_conversion_rate", exchange_rate);
@@ -1420,7 +1420,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
// Target doc created from a mapped doc
if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
if (this.frm.doc.__onload?.load_after_mapping) {
// Calculate totals even though pricing rule is not applied.
// `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total.
if (calculate_taxes_and_totals) me.calculate_taxes_and_totals();

View File

@@ -0,0 +1,431 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.selling");
erpnext.sales_common = {
setup_selling_controller:function() {
erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
setup() {
super.setup();
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales");
this.frm.email_field = "contact_email";
}
onload() {
super.onload();
this.setup_queries();
this.frm.set_query('shipping_rule', function() {
return {
filters: {
"shipping_rule_type": "Selling"
}
};
});
}
setup_queries() {
var me = this;
$.each([["customer", "customer"],
["lead", "lead"]],
function(i, opts) {
if(me.frm.fields_dict[opts[0]])
me.frm.set_query(opts[0], erpnext.queries[opts[1]]);
});
me.frm.set_query('contact_person', erpnext.queries.contact_query);
me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
if(this.frm.fields_dict.selling_price_list) {
this.frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
}
if(this.frm.fields_dict.tc_name) {
this.frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
}
if(!this.frm.fields_dict["items"]) {
return;
}
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0}
}
});
}
if(this.frm.fields_dict["packed_items"] &&
this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) {
this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) {
return me.set_query_for_batch(doc, cdt, cdn)
});
}
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
return me.set_query_for_item_tax_template(doc, cdt, cdn)
});
}
}
refresh() {
super.refresh();
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
this.frm.toggle_display("customer_name",
(this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer));
this.toggle_editable_price_list_rate();
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function() {
me.apply_price_list();
});
}
customer_address() {
erpnext.utils.get_address_display(this.frm, "customer_address");
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
}
shipping_address_name() {
erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address");
erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name");
}
dispatch_address_name() {
erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address");
}
sales_partner() {
this.apply_pricing_rule();
}
campaign() {
this.apply_pricing_rule();
}
selling_price_list() {
this.apply_price_list();
this.set_dynamic_labels();
}
discount_percentage(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage');
}
discount_amount(doc, cdt, cdn) {
if(doc.name === cdn) {
return;
}
var item = frappe.get_doc(cdt, cdn);
item.discount_percentage = 0.0;
this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount');
}
commission_rate() {
this.calculate_commission();
}
total_commission() {
frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
const { amount_eligible_for_commission } = this.frm.doc;
if(!amount_eligible_for_commission) return;
this.frm.set_value(
"commission_rate", flt(
this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
)
);
}
allocated_percentage(doc, cdt, cdn) {
var sales_person = frappe.get_doc(cdt, cdn);
if(sales_person.allocated_percentage) {
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person));
sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person);
this.calculate_incentive(sales_person);
refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name,
sales_person.parentfield);
}
}
sales_person(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.calculate_incentive(row);
refresh_field("incentives",row.name,row.parentfield);
}
toggle_editable_price_list_rate() {
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
if(df && editable_price_list_rate) {
const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item");
if (!this.frm.fields_dict[parent_field]) return;
this.frm.fields_dict[parent_field].grid.update_docfield_property(
'price_list_rate', 'read_only', 0
);
}
}
calculate_commission() {
if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100);
frappe.throw(`${__(frappe.meta.get_label(
this.frm.doc.doctype, "commission_rate", this.frm.doc.name
))} ${__("cannot be greater than 100")}`);
}
this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
(sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
)
this.frm.doc.total_commission = flt(
this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
precision("total_commission")
);
refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_contribution() {
var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person);
if (!sales_person.allocated_percentage) return;
sales_person.allocated_amount = flt(
me.frm.doc.amount_eligible_for_commission
* sales_person.allocated_percentage
/ 100.0,
precision("allocated_amount", sales_person)
);
});
}
calculate_incentive(row) {
if(row.allocated_amount)
{
row.incentives = flt(
row.allocated_amount * row.commission_rate / 100.0,
precision("incentives", row));
}
}
set_dynamic_labels() {
super.set_dynamic_labels();
this.set_product_bundle_help(this.frm.doc);
}
set_product_bundle_help(doc) {
if(!this.frm.fields_dict.packing_list) return;
if ((doc.packed_items || []).length) {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(true);
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
var help_msg = "<div class='alert alert-warning'>" +
__("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+
"</div>";
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg;
}
} else {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(false);
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = '';
}
}
refresh_field('product_bundle_help');
}
company_address() {
var me = this;
if(this.frm.doc.company_address) {
frappe.call({
method: "frappe.contacts.doctype.address.address.get_address_display",
args: {"address_dict": this.frm.doc.company_address },
callback: function(r) {
if(r.message) {
me.frm.set_value("company_address_display", r.message)
}
}
})
} else {
this.frm.set_value("company_address_display", "");
}
}
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
}
qty(doc, cdt, cdn) {
super.qty(doc, cdt, cdn);
}
pick_serial_and_batch(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
item.title = item.has_serial_no ?
__("Select Serial No") : __("Select Batch No");
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
}
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
});
}
}
);
});
}
});
}
update_auto_repeat_reference(doc) {
if (doc.auto_repeat) {
frappe.call({
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
args:{
docname: doc.auto_repeat,
reference:doc.name
},
callback: function(r){
if (r.message=="success") {
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'});
} else {
frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'});
}
}
})
}
}
project() {
let me = this;
if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) {
if(this.frm.doc.project) {
frappe.call({
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
args: {project: this.frm.doc.project},
callback: function(r, rt) {
if(!r.exc) {
$.each(me.frm.doc["items"] || [], function(i, row) {
if(r.message) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
}
})
}
}
})
}
}
}
coupon_code() {
this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 0);
}
};
}
}
erpnext.pre_sales = {
set_as_lost: function(doctype) {
frappe.ui.form.on(doctype, {
set_as_lost_dialog: function(frm) {
var dialog = new frappe.ui.Dialog({
title: __("Set as Lost"),
fields: [
{
"fieldtype": "Table MultiSelect",
"label": __("Lost Reasons"),
"fieldname": "lost_reason",
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
"reqd": 1
},
{
"fieldtype": "Table MultiSelect",
"label": __("Competitors"),
"fieldname": "competitors",
"options": "Competitor Detail"
},
{
"fieldtype": "Small Text",
"label": __("Detailed Reason"),
"fieldname": "detailed_reason"
},
],
primary_action: function() {
let values = dialog.get_values();
frm.call({
doc: frm.doc,
method: 'declare_enquiry_lost',
args: {
'lost_reasons_list': values.lost_reason,
'competitors': values.competitors ? values.competitors : [],
'detailed_reason': values.detailed_reason
},
callback: function(r) {
dialog.hide();
frm.reload_doc();
},
});
},
primary_action_label: __('Declare Lost')
});
dialog.show();
}
});
}
}

View File

@@ -651,7 +651,7 @@
"search_fields": "customer_name,customer_group,territory, mobile_no,primary_address",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"sort_order": "DESC",
"states": [],
"title_field": "customer_name",
"track_changes": 1

View File

@@ -582,7 +582,8 @@ def get_customer_outstanding(
"""
select sum(debit) - sum(credit)
from `tabGL Entry` where party_type = 'Customer'
and party = %s and company=%s {0}""".format(
and is_cancelled = 0 and party = %s
and company=%s {0}""".format(
cond
),
(customer, company),

View File

@@ -352,9 +352,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
ignore_permissions=ignore_permissions,
)
# postprocess: fetch shipping address, set missing values
doclist.set_onload("ignore_price_list", True)
return doclist
@@ -423,8 +420,6 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
ignore_permissions=ignore_permissions,
)
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -716,8 +716,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
target_doc.set_onload("ignore_price_list", True)
return target_doc
@@ -806,8 +804,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
if automatically_fetch_payment_terms:
doclist.set_payment_schedule()
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import _, msgprint
from frappe import _, msgprint, qb
from frappe.query_builder import Criterion
from erpnext import get_company_currency
@@ -214,24 +215,33 @@ def get_conditions(filters, date_field):
if items:
conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items)))
values += items
else:
# return empty result, if no items are fetched after filtering on 'item group' and 'brand'
conditions.append("dt_item.item_code = Null")
return " and ".join(conditions), values
def get_items(filters):
item = qb.DocType("Item")
item_query_conditions = []
if filters.get("item_group"):
key = "item_group"
elif filters.get("brand"):
key = "brand"
else:
key = ""
items = []
if key:
items = frappe.db.sql_list(
"""select name from tabItem where %s = %s""" % (key, "%s"), (filters[key])
# Handle 'Parent' nodes as well.
item_group = qb.DocType("Item Group")
lft, rgt = frappe.db.get_all(
"Item Group", filters={"name": filters.get("item_group")}, fields=["lft", "rgt"], as_list=True
)[0]
item_group_query = (
qb.from_(item_group)
.select(item_group.name)
.where((item_group.lft >= lft) & (item_group.rgt <= rgt))
)
item_query_conditions.append(item.item_group.isin(item_group_query))
if filters.get("brand"):
item_query_conditions.append(item.brand == filters.get("brand"))
items = qb.from_(item).select(item.name).where(Criterion.all(item_query_conditions)).run()
return items

View File

@@ -665,8 +665,6 @@ def make_sales_invoice(source_name, target_doc=None):
if automatically_fetch_payment_terms:
doc.set_payment_schedule()
doc.set_onload("ignore_price_list", True)
return doc

View File

@@ -1057,7 +1057,6 @@ def make_purchase_invoice(source_name, target_doc=None):
set_missing_values,
)
doclist.set_onload("ignore_price_list", True)
return doclist

View File

@@ -352,6 +352,7 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_width": "100px",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
"width": "100px"
},
{
@@ -1054,7 +1055,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:38:15.251994",
"modified": "2023-11-30 16:12:02.364608",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -241,7 +241,7 @@ frappe.ui.form.on('Stock Entry', {
}
}
if (frm.doc.docstatus===0) {
if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Purchase Invoice'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_stock_entry",
@@ -294,7 +294,8 @@ frappe.ui.form.on('Stock Entry', {
})
}, __("Get Items From"));
}
if (frm.doc.docstatus===0 && frm.doc.purpose == "Material Issue") {
if (frm.doc.docstatus === 0 && frm.doc.purpose == "Material Issue") {
frm.add_custom_button(__('Expired Batches'), function() {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items",
@@ -379,6 +380,10 @@ frappe.ui.form.on('Stock Entry', {
frm.remove_custom_button('Bill of Materials', "Get Items From");
frm.events.show_bom_custom_button(frm);
frm.trigger('add_to_transit');
frm.fields_dict.items.grid.update_docfield_property(
'basic_rate', 'read_only', frm.doc.purpose == "Material Receipt" ? 0 : 1
);
},
purpose: function(frm) {

View File

@@ -17,6 +17,7 @@ frappe.query_reports["Stock Analytics"] = {
fieldtype: "Link",
options:"Item",
default: "",
get_query: () => ({filters: { 'is_stock_item': 1 }}),
},
{
fieldname: "value_quantity",

View File

@@ -270,7 +270,7 @@ def get_items(filters):
if item_code := filters.get("item_code"):
return [item_code]
else:
item_filters = {}
item_filters = {"is_stock_item": 1}
if item_group := filters.get("item_group"):
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
item_filters["item_group"] = ("in", children + [item_group])

View File

@@ -13,10 +13,18 @@ const DIFFERENCE_FIELD_NAMES = [
frappe.query_reports["Stock Ledger Variance"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item",
"label": __("Item"),
"options": "Item",
get_query: function() {
return {
@@ -27,7 +35,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"label": __("Warehouse"),
"options": "Warehouse",
get_query: function() {
return {
@@ -38,7 +46,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
{
"fieldname": "difference_in",
"fieldtype": "Select",
"label": "Difference In",
"label": __("Difference In"),
"options": [
"",
"Qty",
@@ -49,7 +57,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
{
"fieldname": "include_disabled",
"fieldtype": "Check",
"label": "Include Disabled",
"label": __("Include Disabled"),
}
],

View File

@@ -55,6 +55,11 @@ def get_columns():
"label": _("Warehouse"),
"options": "Warehouse",
},
{
"fieldname": "valuation_method",
"fieldtype": "Data",
"label": _("Valuation Method"),
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
@@ -194,6 +199,7 @@ def get_columns():
def get_data(filters=None):
filters = frappe._dict(filters or {})
item_warehouse_map = get_item_warehouse_combinations(filters)
valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
data = []
if item_warehouse_map:
@@ -206,8 +212,17 @@ def get_data(filters=None):
continue
for row in report_data:
if has_difference(row, precision, filters.difference_in):
data.append(add_item_warehouse_details(row, item_warehouse))
if has_difference(
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
):
row.update(
{
"item_code": item_warehouse.item_code,
"warehouse": item_warehouse.warehouse,
"valuation_method": item_warehouse.valuation_method or valuation_method,
}
)
data.append(row)
break
return data
@@ -229,8 +244,14 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
.select(
bin.item_code,
bin.warehouse,
item.valuation_method,
)
.where(
(item.is_stock_item == 1)
& (item.has_serial_no == 0)
& (warehouse.is_group == 0)
& (warehouse.company == filters.company)
)
.where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0))
)
if filters.item_code:
@@ -243,37 +264,27 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
return query.run(as_dict=1)
def has_difference(row, precision, difference_in):
has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
has_value_difference = (
flt(row.diff_value_diff, precision)
or flt(row.fifo_value_diff, precision)
or flt(row.fifo_difference_diff, precision)
)
has_valuation_difference = flt(row.valuation_diff, precision) or flt(
row.fifo_valuation_diff, precision
)
def has_difference(row, precision, difference_in, valuation_method):
if valuation_method == "Moving Average":
qty_diff = flt(row.difference_in_qty, precision)
value_diff = flt(row.diff_value_diff, precision)
valuation_diff = flt(row.valuation_diff, precision)
else:
qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
value_diff = (
flt(row.diff_value_diff, precision)
or flt(row.fifo_value_diff, precision)
or flt(row.fifo_difference_diff, precision)
)
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
if difference_in == "Qty" and has_qty_difference:
if difference_in == "Qty" and qty_diff:
return True
elif difference_in == "Value" and has_value_difference:
elif difference_in == "Value" and value_diff:
return True
elif difference_in == "Valuation" and has_valuation_difference:
elif difference_in == "Valuation" and valuation_diff:
return True
elif difference_in not in ["Qty", "Value", "Valuation"] and (
has_qty_difference or has_value_difference or has_valuation_difference
qty_diff or value_diff or valuation_diff
):
return True
return False
def add_item_warehouse_details(row, item_warehouse):
row.update(
{
"item_code": item_warehouse.item_code,
"warehouse": item_warehouse.warehouse,
}
)
return row

View File

@@ -8,11 +8,28 @@ from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin
class SubcontractingOrder(SubcontractingController):
def __init__(self, *args, **kwargs):
super(SubcontractingOrder, self).__init__(*args, **kwargs)
self.status_updater = [
{
"source_dt": "Subcontracting Order Item",
"target_dt": "Material Request Item",
"join_field": "material_request_item",
"target_field": "ordered_qty",
"target_parent_dt": "Material Request",
"target_parent_field": "per_ordered",
"target_ref_field": "stock_qty",
"source_field": "qty",
"percent_join_field": "material_request",
}
]
def before_validate(self):
super(SubcontractingOrder, self).before_validate()
@@ -26,11 +43,15 @@ class SubcontractingOrder(SubcontractingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def on_submit(self):
self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
def on_cancel(self):
self.update_prevdoc_status()
self.update_requested_qty()
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
@@ -114,7 +135,32 @@ class SubcontractingOrder(SubcontractingController):
):
item_wh_list.append([item.item_code, item.warehouse])
for item_code, warehouse in item_wh_list:
update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
update_bin_qty(
item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)}
)
@staticmethod
def get_ordered_qty(item_code, warehouse):
table = frappe.qb.DocType("Subcontracting Order")
child = frappe.qb.DocType("Subcontracting Order Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select((child.qty - child.received_qty) * child.conversion_factor)
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
& (child.warehouse == warehouse)
& (child.qty > child.received_qty)
& (table.status != "Completed")
)
)
query = query.run()
return flt(query[0][0]) if query else 0
def update_reserved_qty_for_subcontracting(self):
for item in self.supplied_items:
@@ -139,7 +185,9 @@ class SubcontractingOrder(SubcontractingController):
"qty": si.fg_item_qty,
"stock_uom": item.stock_uom,
"bom": bom,
},
"material_request": si.material_request,
"material_request_item": si.material_request_item,
}
)
else:
frappe.throw(

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
from erpnext.controllers.subcontracting_controller import (
@@ -566,6 +567,123 @@ class TestSubcontractingOrder(FrappeTestCase):
self.assertEqual(sco.status, "Closed")
self.assertEqual(sco.supplied_items[0].returned_qty, 5)
def test_ordered_qty_for_subcontracting_order(self):
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 8",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA8",
"fg_item_qty": 10,
},
]
ordered_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="ordered_qty",
)
ordered_qty = flt(ordered_qty)
sco = get_subcontracting_order(service_items=service_items)
sco.reload()
new_ordered_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="ordered_qty",
)
new_ordered_qty = flt(new_ordered_qty)
self.assertEqual(ordered_qty + 10, new_ordered_qty)
for row in sco.supplied_items:
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code=row.rm_item_code,
qty=row.required_qty,
basic_rate=100,
)
scr = make_subcontracting_receipt(sco.name)
scr.submit()
new_ordered_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="ordered_qty",
)
self.assertEqual(ordered_qty, new_ordered_qty)
scr.reload()
scr.cancel()
new_ordered_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="ordered_qty",
)
self.assertEqual(ordered_qty + 10, new_ordered_qty)
def test_requested_qty_for_subcontracting_order(self):
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
requested_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="indented_qty",
)
requested_qty = flt(requested_qty)
mr = make_material_request(
item_code="Subcontracted Item SA8",
material_request_type="Purchase",
qty=10,
)
self.assertTrue(mr.docstatus == 1)
new_requested_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="indented_qty",
)
new_requested_qty = flt(new_requested_qty)
self.assertEqual(requested_qty + 10, new_requested_qty)
po = make_purchase_order(mr.name)
po.is_subcontracted = 1
po.supplier = "_Test Supplier"
po.items[0].fg_item = "Subcontracted Item SA8"
po.items[0].fg_item_qty = 10
po.items[0].item_code = "Subcontracted Service Item 8"
po.items[0].item_name = "Subcontracted Service Item 8"
po.items[0].qty = 10
po.supplier_warehouse = "_Test Warehouse 1 - _TC"
po.save()
po.submit()
self.assertTrue(po.items[0].material_request)
self.assertTrue(po.items[0].material_request_item)
sco = create_subcontracting_order(po_name=po.name)
self.assertTrue(sco.items[0].material_request)
self.assertTrue(sco.items[0].material_request_item)
new_requested_qty = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
fieldname="indented_qty",
)
new_requested_qty = flt(new_requested_qty)
self.assertEqual(requested_qty, new_requested_qty)
def create_subcontracting_order(**args):
args = frappe._dict(args)

View File

@@ -40,6 +40,11 @@
"manufacture_section",
"manufacturer",
"manufacturer_part_no",
"column_break_impp",
"reference_section",
"material_request",
"column_break_fpyl",
"material_request_item",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -332,13 +337,44 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "column_break_impp",
"fieldtype": "Column Break"
},
{
"fieldname": "material_request",
"fieldtype": "Link",
"label": "Material Request",
"no_copy": 1,
"options": "Material Request",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "material_request_item",
"fieldtype": "Data",
"label": "Material Request Item",
"no_copy": 1,
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
"fieldname": "reference_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "column_break_fpyl",
"fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:38:37.640677",
"modified": "2023-11-30 15:29:43.744618",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Item",
@@ -351,4 +387,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -1,131 +1,160 @@
{
"actions": [],
"autoname": "hash",
"creation": "2022-04-01 19:23:05.728354",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"column_break_2",
"item_name",
"section_break_4",
"qty",
"column_break_6",
"rate",
"column_break_8",
"amount",
"section_break_10",
"fg_item",
"column_break_12",
"fg_item_qty"
],
"fields": [
{
"bold": 1,
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Name",
"print_hide": 1,
"reqd": 1
},
{
"bold": 1,
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"print_width": "60px",
"reqd": 1,
"width": "60px"
},
{
"bold": 1,
"columns": 2,
"fetch_from": "item_code.standard_rate",
"fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
"columns": 2,
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good Item",
"options": "Item",
"reqd": 1
},
{
"default": "1",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Item Quantity",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2022-04-07 11:43:43.094867",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Service Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"actions": [],
"autoname": "hash",
"creation": "2022-04-01 19:23:05.728354",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"column_break_2",
"item_name",
"section_break_4",
"qty",
"column_break_6",
"rate",
"column_break_8",
"amount",
"section_break_10",
"fg_item",
"column_break_12",
"fg_item_qty",
"section_break_kphn",
"material_request",
"column_break_piqi",
"material_request_item"
],
"fields": [
{
"bold": 1,
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Name",
"print_hide": 1,
"reqd": 1
},
{
"bold": 1,
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"print_width": "60px",
"reqd": 1,
"width": "60px"
},
{
"bold": 1,
"columns": 2,
"fetch_from": "item_code.standard_rate",
"fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
"columns": 2,
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good Item",
"options": "Item",
"reqd": 1
},
{
"default": "1",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Item Quantity",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "section_break_kphn",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "material_request",
"fieldtype": "Link",
"label": "Material Request",
"no_copy": 1,
"options": "Material Request",
"read_only": 1
},
{
"fieldname": "column_break_piqi",
"fieldtype": "Column Break"
},
{
"fieldname": "material_request_item",
"fieldtype": "Data",
"label": "Material Request Item",
"no_copy": 1,
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2023-11-30 13:29:31.017440",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Service Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -111,12 +111,12 @@ class SubcontractingReceipt(SubcontractingController):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.update_status()
@frappe.whitelist()

View File

@@ -45,6 +45,7 @@
"subcontracting_receipt_item",
"section_break_45",
"bom",
"include_exploded_items",
"serial_no",
"col_break5",
"batch_no",
@@ -465,12 +466,19 @@
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"default": "0",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
"label": "Include Exploded Items",
"print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:38:26.459669",
"modified": "2023-11-30 12:05:51.920705",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",

View File

@@ -4,93 +4,67 @@
frappe.provide("erpnext.support");
frappe.ui.form.on("Warranty Claim", {
setup: function(frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
setup: (frm) => {
frm.set_query("contact_person", erpnext.queries.contact_query);
frm.set_query("customer_address", erpnext.queries.address_query);
frm.set_query("customer", erpnext.queries.customer);
frm.add_fetch('serial_no', 'item_code', 'item_code');
frm.add_fetch('serial_no', 'item_name', 'item_name');
frm.add_fetch('serial_no', 'description', 'description');
frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status');
frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date');
frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date');
frm.add_fetch('serial_no', 'customer', 'customer');
frm.add_fetch('serial_no', 'customer_name', 'customer_name');
frm.add_fetch('item_code', 'item_name', 'item_name');
frm.add_fetch('item_code', 'description', 'description');
frm.set_query("serial_no", () => {
let filters = {
company: frm.doc.company,
};
if (frm.doc.item_code) {
filters["item_code"] = frm.doc.item_code;
}
return { filters: filters };
});
frm.set_query("item_code", () => {
return {
filters: {
disabled: 0,
},
};
});
},
onload: function(frm) {
if(!frm.doc.status) {
frm.set_value('status', 'Open');
onload: (frm) => {
if (!frm.doc.status) {
frm.set_value("status", "Open");
}
},
customer: function(frm) {
refresh: (frm) => {
frappe.dynamic_link = {
doc: frm.doc,
fieldname: "customer",
doctype: "Customer",
};
if (
!frm.doc.__islocal &&
["Open", "Work In Progress"].includes(frm.doc.status)
) {
frm.add_custom_button(__("Maintenance Visit"), () => {
frappe.model.open_mapped_doc({
method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
frm: frm,
});
});
}
},
customer: (frm) => {
erpnext.utils.get_party_details(frm);
},
customer_address: function(frm) {
customer_address: (frm) => {
erpnext.utils.get_address_display(frm);
},
contact_person: function(frm) {
contact_person: (frm) => {
erpnext.utils.get_contact_details(frm);
}
},
});
erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller {
refresh() {
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
if(!cur_frm.doc.__islocal &&
(cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) {
cur_frm.add_custom_button(__('Maintenance Visit'),
this.make_maintenance_visit);
}
}
make_maintenance_visit() {
frappe.model.open_mapped_doc({
method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
frm: cur_frm
})
}
};
extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm}));
cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) {
var cond = [];
var filter = [
['Serial No', 'docstatus', '!=', 2]
];
if(doc.item_code) {
cond = ['Serial No', 'item_code', '=', doc.item_code];
filter.push(cond);
}
if(doc.customer) {
cond = ['Serial No', 'customer', '=', doc.customer];
filter.push(cond);
}
return{
filters:filter
}
}
cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
if(doc.serial_no) {
return{
doctype: "Serial No",
fields: "item_code",
filters:{
name: doc.serial_no
}
}
}
else{
return{
filters:[
['Item', 'docstatus', '!=', 2],
['Item', 'disabled', '=', 0]
]
}
}
};

View File

@@ -90,7 +90,8 @@
"fieldname": "serial_no",
"fieldtype": "Link",
"label": "Serial No",
"options": "Serial No"
"options": "Serial No",
"search_index": 1
},
{
"fieldname": "customer",
@@ -126,6 +127,8 @@
"options": "fa fa-ticket"
},
{
"fetch_from": "serial_no.item_code",
"fetch_if_empty": 1,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
@@ -138,6 +141,7 @@
},
{
"depends_on": "eval:doc.item_code",
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
@@ -147,6 +151,7 @@
},
{
"depends_on": "eval:doc.item_code",
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
@@ -162,17 +167,24 @@
"width": "50%"
},
{
"fetch_from": "serial_no.maintenance_status",
"fetch_if_empty": 1,
"fieldname": "warranty_amc_status",
"fieldtype": "Select",
"label": "Warranty / AMC Status",
"options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC"
"options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC",
"search_index": 1
},
{
"fetch_from": "serial_no.warranty_expiry_date",
"fetch_if_empty": 1,
"fieldname": "warranty_expiry_date",
"fieldtype": "Date",
"label": "Warranty Expiry Date"
},
{
"fetch_from": "serial_no.amc_expiry_date",
"fetch_if_empty": 1,
"fieldname": "amc_expiry_date",
"fieldtype": "Date",
"label": "AMC Expiry Date"
@@ -223,6 +235,7 @@
{
"bold": 1,
"depends_on": "customer",
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -362,7 +375,8 @@
],
"icon": "fa fa-bug",
"idx": 1,
"modified": "2021-11-09 17:26:09.703215",
"links": [],
"modified": "2023-11-28 17:30:35.676410",
"modified_by": "Administrator",
"module": "Support",
"name": "Warranty Claim",