mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 21:05:10 +00:00
Merge branch 'version-13-hotfix' into fix/delivery-note/billed-amount
This commit is contained in:
@@ -630,6 +630,26 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_multiple_pricing_rules_with_min_qty(self):
|
||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
|
||||
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
||||
item = si.items[0]
|
||||
item.stock_qty = 1
|
||||
si.save()
|
||||
self.assertFalse(item.discount_percentage)
|
||||
item.qty = 5
|
||||
item.stock_qty = 5
|
||||
si.save()
|
||||
self.assertEqual(item.discount_percentage, 30)
|
||||
si.delete()
|
||||
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
|
||||
@@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
|
||||
for key in sorted(pricing_rule_dict):
|
||||
pricing_rules_list.extend(pricing_rule_dict.get(key))
|
||||
|
||||
return pricing_rules_list or pricing_rules
|
||||
return pricing_rules_list
|
||||
|
||||
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
|
||||
filtered_pricing_rules = []
|
||||
|
||||
@@ -71,7 +71,8 @@ class ShippingRule(Document):
|
||||
if doc.currency != doc.company_currency:
|
||||
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
||||
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
if shipping_amount:
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
|
||||
def get_shipping_amount_from_rules(self, value):
|
||||
for condition in self.get("conditions"):
|
||||
|
||||
@@ -77,17 +77,17 @@ class StockController(AccountsController):
|
||||
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
|
||||
|
||||
def clean_serial_nos(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
|
||||
|
||||
for row in self.get("items"):
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# replace commas by linefeed
|
||||
row.serial_no = row.serial_no.replace(",", "\n")
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
# strip preceeding and succeeding spaces for each SN
|
||||
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
|
||||
serial_no_list = row.serial_no.split("\n")
|
||||
serial_no_list = [sn.strip() for sn in serial_no_list]
|
||||
|
||||
row.serial_no = "\n".join(serial_no_list)
|
||||
for row in self.get('packed_items') or []:
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
||||
default_cost_center=None):
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"inspection_required",
|
||||
"quality_inspection_template",
|
||||
"column_break_31",
|
||||
"bom_level",
|
||||
"section_break_33",
|
||||
"items",
|
||||
"scrap_section",
|
||||
@@ -522,13 +521,6 @@
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bom_level",
|
||||
"fieldtype": "Int",
|
||||
"label": "BOM Level",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_33",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -540,7 +532,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-18 13:04:16.271975",
|
||||
"modified": "2022-01-30 21:27:54.727298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -577,5 +569,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -156,8 +156,6 @@ class BOM(WebsiteGenerator):
|
||||
self.update_stock_qty()
|
||||
self.validate_scrap_items()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||
self.set_bom_level()
|
||||
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||
@@ -704,7 +702,6 @@ class BOM(WebsiteGenerator):
|
||||
if not d.batch_size or d.batch_size <= 0:
|
||||
d.batch_size = 1
|
||||
|
||||
|
||||
def validate_scrap_items(self):
|
||||
for item in self.scrap_items:
|
||||
msg = ""
|
||||
@@ -735,20 +732,6 @@ class BOM(WebsiteGenerator):
|
||||
"""Get a complete tree representation preserving order of child items."""
|
||||
return BOMTree(self.name)
|
||||
|
||||
def set_bom_level(self, update=False):
|
||||
levels = []
|
||||
|
||||
self.bom_level = 0
|
||||
for row in self.items:
|
||||
if row.bom_no:
|
||||
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
|
||||
|
||||
if levels:
|
||||
self.bom_level = max(levels) + 1
|
||||
|
||||
if update:
|
||||
self.db_set("bom_level", self.bom_level)
|
||||
|
||||
def get_bom_item_rate(args, bom_doc):
|
||||
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
||||
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
||||
|
||||
@@ -560,9 +560,11 @@ class ProductionPlan(Document):
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
|
||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
||||
self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
|
||||
for idx, row in enumerate(self.sub_assembly_items, start=1):
|
||||
row.idx = idx
|
||||
|
||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||
for data in bom_data:
|
||||
data.qty = data.stock_qty
|
||||
data.production_plan_item = row.name
|
||||
@@ -1005,9 +1007,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
|
||||
if d.value else 0)
|
||||
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
bom_data.append(frappe._dict({
|
||||
'parent_item_code': parent_item_code,
|
||||
@@ -1018,7 +1017,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
'uom': d.stock_uom,
|
||||
'bom_no': d.value,
|
||||
'is_sub_contracted_item': d.is_sub_contracted_item,
|
||||
'bom_level': bom_level,
|
||||
'bom_level': indent,
|
||||
'indent': indent,
|
||||
'stock_qty': stock_qty
|
||||
}))
|
||||
|
||||
@@ -347,6 +347,45 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_subassmebly_sorting(self):
|
||||
""" Test subassembly sorting in case of multiple items with nested BOMs"""
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
prefix = "_TestLevel_"
|
||||
boms = {
|
||||
"Assembly": {
|
||||
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
|
||||
"SubAssembly2": {"ChildPart3": {}},
|
||||
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
|
||||
"ChildPart5": {},
|
||||
"ChildPart6": {},
|
||||
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
|
||||
},
|
||||
"MegaDeepAssy": {
|
||||
"SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},},
|
||||
# ^ assert that this is
|
||||
# first item in subassy table
|
||||
}
|
||||
}
|
||||
create_nested_bom(boms, prefix=prefix)
|
||||
|
||||
items = [prefix + item_code for item_code in boms.keys()]
|
||||
plan = create_production_plan(item_code=items[0], do_not_save=True)
|
||||
plan.append("po_items", {
|
||||
'use_multi_level_bom': 1,
|
||||
'item_code': items[1],
|
||||
'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'),
|
||||
'planned_qty': 1,
|
||||
'planned_start_date': now_datetime()
|
||||
})
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
bom_level_order = [d.bom_level for d in plan.sub_assembly_items]
|
||||
self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True))
|
||||
# lowest most level of subassembly should be first
|
||||
self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "bom_no.bom_level",
|
||||
"fieldname": "bom_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
@@ -189,7 +188,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-28 20:10:56.296410",
|
||||
"modified": "2022-01-30 21:31:10.527559",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
@@ -198,5 +197,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -31,6 +31,7 @@ from erpnext.stock.doctype.batch.batch import make_batch
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
auto_make_serial_nos,
|
||||
clean_serial_no_string,
|
||||
get_auto_serial_nos,
|
||||
get_serial_nos,
|
||||
)
|
||||
@@ -358,6 +359,7 @@ class WorkOrder(Document):
|
||||
frappe.delete_doc("Batch", row.name)
|
||||
|
||||
def make_serial_nos(self, args):
|
||||
self.serial_no = clean_serial_no_string(self.serial_no)
|
||||
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
|
||||
if serial_no_series:
|
||||
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
|
||||
|
||||
@@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
'item_code': item.item_code,
|
||||
'item_name': item.item_name,
|
||||
'indent': indent,
|
||||
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
|
||||
if item.bom_no else ""),
|
||||
'bom_level': indent,
|
||||
'bom': item.bom_no,
|
||||
'qty': item.qty * qty,
|
||||
'uom': item.uom,
|
||||
@@ -73,7 +72,7 @@ def get_columns():
|
||||
},
|
||||
{
|
||||
"label": "BOM Level",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Int",
|
||||
"fieldname": "bom_level",
|
||||
"width": 100
|
||||
},
|
||||
|
||||
@@ -17,14 +17,12 @@ frappe.query_reports["Cost of Poor Quality Report"] = {
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Datetime",
|
||||
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Datetime",
|
||||
default: frappe.datetime.now_datetime(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Job Card"),
|
||||
|
||||
@@ -3,46 +3,65 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
return get_columns(filters), get_data(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
data = []
|
||||
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
|
||||
if operations:
|
||||
operations = [d.name for d in operations]
|
||||
fields = ["production_item as item_code", "item_name", "work_order", "operation",
|
||||
"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
|
||||
if report_filters.get('operation'):
|
||||
operations = [report_filters.get('operation')]
|
||||
else:
|
||||
operations = [d.name for d in operations]
|
||||
|
||||
filters = get_filters(report_filters, operations)
|
||||
job_card = frappe.qb.DocType("Job Card")
|
||||
|
||||
job_cards = frappe.get_all("Job Card", fields = fields,
|
||||
filters = filters)
|
||||
operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost')
|
||||
item_code = (job_card.production_item).as_('item_code')
|
||||
|
||||
for row in job_cards:
|
||||
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
|
||||
data.append(row)
|
||||
query = (frappe.qb
|
||||
.from_(job_card)
|
||||
.select(job_card.name, job_card.work_order, item_code, job_card.item_name,
|
||||
job_card.operation, job_card.serial_no, job_card.batch_no,
|
||||
job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate,
|
||||
operating_cost)
|
||||
.where(
|
||||
(job_card.docstatus == 1)
|
||||
& (job_card.is_corrective_job_card == 1))
|
||||
.groupby(job_card.name)
|
||||
)
|
||||
|
||||
query = append_filters(query, report_filters, operations, job_card)
|
||||
data = query.run(as_dict=True)
|
||||
return data
|
||||
|
||||
def get_filters(report_filters, operations):
|
||||
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
|
||||
for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
|
||||
if report_filters.get(field):
|
||||
if field != 'serial_no':
|
||||
filters[field] = report_filters.get(field)
|
||||
else:
|
||||
filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
|
||||
def append_filters(query, report_filters, operations, job_card):
|
||||
"""Append optional filters to query builder. """
|
||||
|
||||
return filters
|
||||
for field in ("name", "work_order", "operation", "workstation",
|
||||
"company", "serial_no", "batch_no", "production_item"):
|
||||
if report_filters.get(field):
|
||||
if field == 'serial_no':
|
||||
query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field))))
|
||||
elif field == 'operation':
|
||||
query = query.where(job_card[field].isin(operations))
|
||||
else:
|
||||
query = query.where(job_card[field] == report_filters.get(field))
|
||||
|
||||
if report_filters.get('from_date') or report_filters.get('to_date'):
|
||||
job_card_time_log = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent)
|
||||
if report_filters.get('from_date'):
|
||||
query = query.where(job_card_time_log.from_time >= report_filters.get('from_date'))
|
||||
if report_filters.get('to_date'):
|
||||
query = query.where(job_card_time_log.to_time <= report_filters.get('to_date'))
|
||||
|
||||
return query
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
|
||||
@@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details):
|
||||
"qty": row.planned_qty,
|
||||
"document_type": "Work Order",
|
||||
"document_name": work_order or "",
|
||||
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
|
||||
"bom_level": 0,
|
||||
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
|
||||
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
erpnext.patches.v12_0.update_is_cancelled_field
|
||||
erpnext.patches.v13_0.add_bin_unique_constraint
|
||||
erpnext.patches.v11_0.rename_production_order_to_work_order
|
||||
erpnext.patches.v11_0.refactor_naming_series
|
||||
erpnext.patches.v11_0.refactor_autoname_naming
|
||||
@@ -291,7 +292,6 @@ erpnext.patches.v13_0.set_training_event_attendance
|
||||
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.update_job_card_details
|
||||
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
||||
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
|
||||
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
||||
erpnext.patches.v13_0.update_subscription_status_in_memberships
|
||||
|
||||
63
erpnext/patches/v13_0/add_bin_unique_constraint.py
Normal file
63
erpnext/patches/v13_0/add_bin_unique_constraint.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.stock_balance import (
|
||||
get_balance_qty_from_sle,
|
||||
get_indented_qty,
|
||||
get_ordered_qty,
|
||||
get_planned_qty,
|
||||
get_reserved_qty,
|
||||
)
|
||||
from erpnext.stock.utils import get_bin
|
||||
|
||||
|
||||
def execute():
|
||||
delete_broken_bins()
|
||||
delete_and_patch_duplicate_bins()
|
||||
|
||||
def delete_broken_bins():
|
||||
# delete useless bins
|
||||
frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null")
|
||||
|
||||
def delete_and_patch_duplicate_bins():
|
||||
|
||||
duplicate_bins = frappe.db.sql("""
|
||||
SELECT
|
||||
item_code, warehouse, count(*) as bin_count
|
||||
FROM
|
||||
tabBin
|
||||
GROUP BY
|
||||
item_code, warehouse
|
||||
HAVING
|
||||
bin_count > 1
|
||||
""", as_dict=1)
|
||||
|
||||
for duplicate_bin in duplicate_bins:
|
||||
item_code = duplicate_bin.item_code
|
||||
warehouse = duplicate_bin.warehouse
|
||||
existing_bins = frappe.get_list("Bin",
|
||||
filters={
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse
|
||||
},
|
||||
fields=["name"],
|
||||
order_by="creation",)
|
||||
|
||||
# keep last one
|
||||
existing_bins.pop()
|
||||
|
||||
for broken_bin in existing_bins:
|
||||
frappe.delete_doc("Bin", broken_bin.name)
|
||||
|
||||
qty_dict = {
|
||||
"reserved_qty": get_reserved_qty(item_code, warehouse),
|
||||
"indented_qty": get_indented_qty(item_code, warehouse),
|
||||
"ordered_qty": get_ordered_qty(item_code, warehouse),
|
||||
"planned_qty": get_planned_qty(item_code, warehouse),
|
||||
"actual_qty": get_balance_qty_from_sle(item_code, warehouse)
|
||||
}
|
||||
|
||||
bin = get_bin(item_code, warehouse)
|
||||
bin.update(qty_dict)
|
||||
bin.update_reserved_qty_for_production()
|
||||
bin.update_reserved_qty_for_sub_contracting()
|
||||
bin.db_update()
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for document in ["bom", "bom_item", "bom_explosion_item"]:
|
||||
frappe.reload_doc('manufacturing', 'doctype', document)
|
||||
|
||||
frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
|
||||
|
||||
bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')""")
|
||||
|
||||
count = 0
|
||||
while(count < len(bom_list)):
|
||||
for parent_bom in get_parent_boms(bom_list[count]):
|
||||
bom_doc = frappe.get_cached_doc("BOM", parent_bom)
|
||||
bom_doc.set_bom_level(update=True)
|
||||
bom_list.append(parent_bom)
|
||||
count += 1
|
||||
|
||||
def get_parent_boms(bom_no):
|
||||
return frappe.db.sql_list("""
|
||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
||||
""", bom_no)
|
||||
@@ -590,6 +590,6 @@ function check_can_calculate_pending_qty(me) {
|
||||
&& doc.fg_completed_qty
|
||||
&& erpnext.stock.bom
|
||||
&& erpnext.stock.bom.name === doc.bom_no;
|
||||
const itemChecks = !!item;
|
||||
const itemChecks = !!item && !item.allow_alternative_item;
|
||||
return docChecks && itemChecks;
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
if not party_details.place_of_supply: return party_details
|
||||
if not party_details.company_gstin: return party_details
|
||||
if not party_details.supplier_gstin: return party_details
|
||||
|
||||
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
|
||||
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
|
||||
|
||||
@@ -457,12 +457,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
make_delivery_note_based_on_delivery_date: function() {
|
||||
var me = this;
|
||||
|
||||
var delivery_dates = [];
|
||||
$.each(this.frm.doc.items || [], function(i, d) {
|
||||
if(!delivery_dates.includes(d.delivery_date)) {
|
||||
delivery_dates.push(d.delivery_date);
|
||||
}
|
||||
});
|
||||
var delivery_dates = this.frm.doc.items.map(i => i.delivery_date);
|
||||
delivery_dates = [ ...new Set(delivery_dates) ];
|
||||
|
||||
var item_grid = this.frm.fields_dict["items"].grid;
|
||||
if(!item_grid.get_selected().length && delivery_dates.length > 1) {
|
||||
@@ -500,14 +496,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
|
||||
if(!dates) return;
|
||||
|
||||
$.each(dates, function(i, d) {
|
||||
$.each(item_grid.grid_rows || [], function(j, row) {
|
||||
if(row.doc.delivery_date == d) {
|
||||
row.doc.__checked = 1;
|
||||
}
|
||||
});
|
||||
})
|
||||
me.make_delivery_note();
|
||||
me.make_delivery_note(dates);
|
||||
dialog.hide();
|
||||
});
|
||||
dialog.show();
|
||||
@@ -516,10 +505,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
}
|
||||
},
|
||||
|
||||
make_delivery_note: function() {
|
||||
make_delivery_note: function(delivery_dates) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
||||
frm: this.frm
|
||||
frm: this.frm,
|
||||
args: {
|
||||
delivery_dates
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -611,6 +611,13 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
|
||||
}
|
||||
|
||||
if not skip_item_mapping:
|
||||
def condition(doc):
|
||||
# make_mapped_doc sets js `args` into `frappe.flags.args`
|
||||
if frappe.flags.args and frappe.flags.args.delivery_dates:
|
||||
if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates:
|
||||
return False
|
||||
return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
|
||||
|
||||
mapper["Sales Order Item"] = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
@@ -619,7 +626,7 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
|
||||
"condition": condition
|
||||
}
|
||||
|
||||
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -46,6 +47,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -169,10 +171,11 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 23:09:39.572776",
|
||||
"modified": "2022-01-30 17:04:54.715288",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Bin",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -200,5 +203,6 @@
|
||||
"quick_entry": 1,
|
||||
"search_fields": "item_code,warehouse",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class Bin(Document):
|
||||
self.db_set('projected_qty', self.projected_qty)
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Bin", ["item_code", "warehouse"])
|
||||
frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse")
|
||||
|
||||
|
||||
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Bin')
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.utils import _create_bin
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
class TestBin(unittest.TestCase):
|
||||
pass
|
||||
|
||||
class TestBin(ERPNextTestCase):
|
||||
|
||||
|
||||
def test_concurrent_inserts(self):
|
||||
""" Ensure no duplicates are possible in case of concurrent inserts"""
|
||||
item_code = "_TestConcurrentBin"
|
||||
make_item(item_code)
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
|
||||
bin1.insert()
|
||||
|
||||
bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
|
||||
with self.assertRaises(frappe.UniqueValidationError):
|
||||
bin2.insert()
|
||||
|
||||
# util method should handle it
|
||||
bin = _create_bin(item_code, warehouse)
|
||||
self.assertEqual(bin.item_code, item_code)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_index_exists(self):
|
||||
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
|
||||
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
|
||||
self.fail(f"Expected unique index on item-warehouse")
|
||||
|
||||
@@ -380,8 +380,7 @@ $.extend(erpnext.item, {
|
||||
// Show Stock Levels only if is_stock_item
|
||||
if (frm.doc.is_stock_item) {
|
||||
frappe.require('assets/js/item-dashboard.min.js', function() {
|
||||
frm.dashboard.parent.find('.stock-levels').remove();
|
||||
const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels');
|
||||
const section = frm.dashboard.add_section('', __("Stock Levels"));
|
||||
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
|
||||
parent: section,
|
||||
item_code: frm.doc.name,
|
||||
|
||||
@@ -484,6 +484,13 @@ def get_serial_nos(serial_no):
|
||||
return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
|
||||
if s.strip()]
|
||||
|
||||
def clean_serial_no_string(serial_no: str) -> str:
|
||||
if not serial_no:
|
||||
return ""
|
||||
|
||||
serial_no_list = get_serial_nos(serial_no)
|
||||
return "\n".join(serial_no_list)
|
||||
|
||||
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
|
||||
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
|
||||
if args.get(field):
|
||||
|
||||
@@ -1674,6 +1674,8 @@ class StockEntry(StockController):
|
||||
for d in self.get("items"):
|
||||
item_code = d.get('original_item') or d.get('item_code')
|
||||
reserve_warehouse = item_wh.get(item_code)
|
||||
if not (reserve_warehouse and item_code):
|
||||
continue
|
||||
stock_bin = get_bin(item_code, reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
|
||||
|
||||
@@ -177,13 +177,7 @@ def get_latest_stock_balance():
|
||||
def get_bin(item_code, warehouse):
|
||||
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
|
||||
if not bin:
|
||||
bin_obj = frappe.get_doc({
|
||||
"doctype": "Bin",
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
})
|
||||
bin_obj.flags.ignore_permissions = 1
|
||||
bin_obj.insert()
|
||||
bin_obj = _create_bin(item_code, warehouse)
|
||||
else:
|
||||
bin_obj = frappe.get_doc('Bin', bin, for_update=True)
|
||||
bin_obj.flags.ignore_permissions = True
|
||||
@@ -193,16 +187,24 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str:
|
||||
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
|
||||
|
||||
if not bin_record:
|
||||
bin_obj = frappe.get_doc({
|
||||
"doctype": "Bin",
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
})
|
||||
bin_obj = _create_bin(item_code, warehouse)
|
||||
bin_record = bin_obj.name
|
||||
return bin_record
|
||||
|
||||
def _create_bin(item_code, warehouse):
|
||||
"""Create a bin and take care of concurrent inserts."""
|
||||
|
||||
bin_creation_savepoint = "create_bin"
|
||||
try:
|
||||
frappe.db.savepoint(bin_creation_savepoint)
|
||||
bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
|
||||
bin_obj.flags.ignore_permissions = 1
|
||||
bin_obj.insert()
|
||||
bin_record = bin_obj.name
|
||||
except frappe.UniqueValidationError:
|
||||
frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres
|
||||
bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
|
||||
|
||||
return bin_record
|
||||
return bin_obj
|
||||
|
||||
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
"""WARNING: This function is deprecated. Inline this function instead of using it."""
|
||||
|
||||
Reference in New Issue
Block a user