Merge pull request #29601 from deepeshgarg007/provisonal_loss_bs
fix: Incorrect provisional profit and loss in balance sheet
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 9031968..eab6d50 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -8,7 +8,10 @@
pip install frappe-bench
-git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1
+frappeuser=${FRAPPE_USER:-"frappe"}
+frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
+
+git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 7347a58..40f9365 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -6,12 +6,23 @@
- '**.js'
- '**.md'
- '**.html'
- workflow_dispatch:
push:
branches: [ develop ]
paths-ignore:
- '**.js'
- '**.md'
+ workflow_dispatch:
+ inputs:
+ user:
+ description: 'user'
+ required: true
+ default: 'frappe'
+ type: string
+ branch:
+ description: 'Branch name'
+ default: 'develop'
+ required: false
+ type: string
concurrency:
group: server-mariadb-develop-${{ github.event.number }}
@@ -95,6 +106,8 @@
env:
DB: mariadb
TYPE: server
+ FRAPPE_USER: ${{ github.event.inputs.user }}
+ FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 617b376..3cc28a3 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,6 +8,7 @@
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'];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 279557a..76d9cc7 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -537,8 +537,11 @@
voucher_wise_stock_value = {}
if self.update_stock:
- for d in frappe.get_all('Stock Ledger Entry',
- fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
+ stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
+ fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
+ filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
+ )
+ for d in stock_ledger_entries:
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 76a7cda..affde4a 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -400,6 +400,16 @@
ref_doc = frappe.get_doc(ref_dt, ref_dn)
ref_doc.db_set("per_billed", per_billed)
+
+ # set billling status
+ if hasattr(ref_doc, 'billing_status'):
+ if ref_doc.per_billed < 0.001:
+ ref_doc.db_set("billing_status", "Not Billed")
+ elif ref_doc.per_billed > 99.999999:
+ ref_doc.db_set("billing_status", "Fully Billed")
+ else:
+ ref_doc.db_set("billing_status", "Partly Billed")
+
ref_doc.set_status(update=True)
def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):
diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
index 8368db6..e1e7225 100644
--- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
+++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
@@ -172,10 +172,15 @@
self.purchase_details = {}
- for d in frappe.get_all("Purchase Order Item",
+ purchased_items = frappe.get_all("Purchase Order Item",
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"],
- filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)},
- group_by = "item_code, warehouse"):
+ filters={
+ "item_code": ("in", self.item_codes),
+ "warehouse": ("in", self.warehouses),
+ "docstatus": 1,
+ },
+ group_by = "item_code, warehouse")
+ for d in purchased_items:
key = (d.item_code, d.warehouse)
if key not in self.purchase_details:
self.purchase_details.setdefault(key, d)
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json
index 5dd1d70..8df9957 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.json
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json
@@ -58,6 +58,7 @@
"width": "50%"
},
{
+ "allow_on_submit": 1,
"default": "Yes",
"fieldname": "is_active",
"fieldtype": "Select",
@@ -232,10 +233,11 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-03-31 15:41:12.342380",
+ "modified": "2022-02-03 23:50:10.205676",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -271,5 +273,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index d696ef5..54e5daa 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -441,7 +441,7 @@
type: "GET",
method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle",
args: {
- args: {
+ row: {
item_code: args.product_bundle,
quantity: args.quantity,
parenttype: frm.doc.doctype,
diff --git a/erpnext/public/js/erpnext-web.bundle.js b/erpnext/public/js/erpnext-web.bundle.js
index 576abd2..cbe899d 100644
--- a/erpnext/public/js/erpnext-web.bundle.js
+++ b/erpnext/public/js/erpnext-web.bundle.js
@@ -1,7 +1,6 @@
import "./website_utils";
import "./wishlist";
import "./shopping_cart";
-import "./cart";
import "./customer_reviews";
import "../../e_commerce/product_ui/list";
import "../../e_commerce/product_ui/views";
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 42bc0b7..acf048e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1375,6 +1375,30 @@
automatically_fetch_payment_terms(enable=0)
+ def test_zero_amount_sales_order_billing_status(self):
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ so = make_sales_order(uom="Nos", do_not_save=1)
+ so.items[0].rate = 0
+ so.save()
+ so.submit()
+
+ self.assertEqual(so.net_total, 0)
+ self.assertEqual(so.billing_status, 'Not Billed')
+
+ si = create_sales_invoice(qty=10, do_not_save=1)
+ si.price_list = '_Test Price List'
+ si.items[0].rate = 0
+ si.items[0].price_list_rate = 0
+ si.items[0].sales_order = so.name
+ si.items[0].so_detail = so.items[0].name
+ si.save()
+ si.submit()
+
+ self.assertEqual(si.net_total, 0)
+ so.load_from_db()
+ self.assertEqual(so.billing_status, 'Fully Billed')
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 9204842..df8cadd 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -4,10 +4,11 @@
import frappe
-from frappe.utils import flt
+from frappe.utils import add_to_date, flt, now
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
@@ -28,7 +29,8 @@
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
+ "is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
@@ -41,14 +43,39 @@
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
- "warehouse": "Stores - TCP1"
+ "warehouse": "Stores - TCP1",
+ "is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
-
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
+ # assert after submit
+ self.assertPurchaseReceiptLCVGLEntries(pr)
+
+ # Mess up cancelled SLE modified timestamp to check
+ # if they aren't effective in any business logic.
+ frappe.db.set_value("Stock Ledger Entry",
+ {
+ "is_cancelled": 1,
+ "voucher_type": pr.doctype,
+ "voucher_no": pr.name
+ },
+ "is_cancelled", 1,
+ modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True)
+ )
+
+ items, warehouses = pr.get_items_and_warehouses()
+ update_gl_entries_after(pr.posting_date, pr.posting_time,
+ warehouses, items, company=pr.company)
+
+ # reassert after reposting
+ self.assertPurchaseReceiptLCVGLEntries(pr)
+
+
+ def assertPurchaseReceiptLCVGLEntries(self, pr):
+
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -74,8 +101,8 @@
for gle in gl_entries:
if not gle.get('is_cancelled'):
- self.assertEqual(expected_values[gle.account][0], gle.debit)
- self.assertEqual(expected_values[gle.account][1], gle.credit)
+ self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}")
+ self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}")
def test_landed_cost_voucher_against_purchase_invoice(self):
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 830d546..d2d4789 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -218,8 +218,6 @@
"label": "Conversion Factor"
},
{
- "fetch_from": "item_code.valuation_rate",
- "fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -232,7 +230,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-01 15:10:29.646399",
+ "modified": "2022-01-28 16:03:30.780111",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
@@ -240,5 +238,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index e4091c4..07c2f1f 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -8,187 +8,253 @@
import frappe
from frappe.model.document import Document
-from frappe.utils import cstr, flt
+from frappe.utils import flt
-from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.get_item_details import get_item_details, get_price_list_rate
class PackedItem(Document):
pass
-def get_product_bundle_items(item_code):
- return frappe.db.sql("""select t1.item_code, t1.qty, t1.uom, t1.description
- from `tabProduct Bundle Item` t1, `tabProduct Bundle` t2
- where t2.new_item_code=%s and t1.parent = t2.name order by t1.idx""", item_code, as_dict=1)
-
-def get_packing_item_details(item, company):
- return frappe.db.sql("""
- select i.item_name, i.is_stock_item, i.description, i.stock_uom, id.default_warehouse
- from `tabItem` i LEFT JOIN `tabItem Default` id ON id.parent=i.name and id.company=%s
- where i.name = %s""",
- (company, item), as_dict = 1)[0]
-
-def get_bin_qty(item, warehouse):
- det = frappe.db.sql("""select actual_qty, projected_qty from `tabBin`
- where item_code = %s and warehouse = %s""", (item, warehouse), as_dict = 1)
- return det and det[0] or frappe._dict()
-
-def update_packing_list_item(doc, packing_item_code, qty, main_item_row, description):
- if doc.amended_from:
- old_packed_items_map = get_old_packed_item_details(doc.packed_items)
- else:
- old_packed_items_map = False
- item = get_packing_item_details(packing_item_code, doc.company)
-
- # check if exists
- exists = 0
- for d in doc.get("packed_items"):
- if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code:
- if d.parent_detail_docname != main_item_row.name:
- d.parent_detail_docname = main_item_row.name
-
- pi, exists = d, 1
- break
-
- if not exists:
- pi = doc.append('packed_items', {})
-
- pi.parent_item = main_item_row.item_code
- pi.item_code = packing_item_code
- pi.item_name = item.item_name
- pi.parent_detail_docname = main_item_row.name
- pi.uom = item.stock_uom
- pi.qty = flt(qty)
- pi.conversion_factor = main_item_row.conversion_factor
- if description and not pi.description:
- pi.description = description
- if not pi.warehouse and not doc.amended_from:
- pi.warehouse = (main_item_row.warehouse if ((doc.get('is_pos') or item.is_stock_item \
- or not item.default_warehouse) and main_item_row.warehouse) else item.default_warehouse)
- if not pi.batch_no and not doc.amended_from:
- pi.batch_no = cstr(main_item_row.get("batch_no"))
- if not pi.target_warehouse:
- pi.target_warehouse = main_item_row.get("target_warehouse")
- bin = get_bin_qty(packing_item_code, pi.warehouse)
- pi.actual_qty = flt(bin.get("actual_qty"))
- pi.projected_qty = flt(bin.get("projected_qty"))
- if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)):
- pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no
- pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no
- pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse
def make_packing_list(doc):
- """make packing list for Product Bundle item"""
- if doc.get("_action") and doc._action == "update_after_submit": return
-
- parent_items = []
- for d in doc.get("items"):
- if frappe.db.get_value("Product Bundle", {"new_item_code": d.item_code}):
- for i in get_product_bundle_items(d.item_code):
- update_packing_list_item(doc, i.item_code, flt(i.qty)*flt(d.stock_qty), d, i.description)
-
- if [d.item_code, d.name] not in parent_items:
- parent_items.append([d.item_code, d.name])
-
- cleanup_packing_list(doc, parent_items)
-
- if frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates"):
- update_product_bundle_price(doc, parent_items)
-
-def cleanup_packing_list(doc, parent_items):
- """Remove all those child items which are no longer present in main item table"""
- delete_list = []
- for d in doc.get("packed_items"):
- if [d.parent_item, d.parent_detail_docname] not in parent_items:
- # mark for deletion from doclist
- delete_list.append(d)
-
- if not delete_list:
- return doc
-
- packed_items = doc.get("packed_items")
- doc.set("packed_items", [])
-
- for d in packed_items:
- if d not in delete_list:
- add_item_to_packing_list(doc, d)
-
-def add_item_to_packing_list(doc, packed_item):
- doc.append("packed_items", {
- 'parent_item': packed_item.parent_item,
- 'item_code': packed_item.item_code,
- 'item_name': packed_item.item_name,
- 'uom': packed_item.uom,
- 'qty': packed_item.qty,
- 'rate': packed_item.rate,
- 'conversion_factor': packed_item.conversion_factor,
- 'description': packed_item.description,
- 'warehouse': packed_item.warehouse,
- 'batch_no': packed_item.batch_no,
- 'actual_batch_qty': packed_item.actual_batch_qty,
- 'serial_no': packed_item.serial_no,
- 'target_warehouse': packed_item.target_warehouse,
- 'actual_qty': packed_item.actual_qty,
- 'projected_qty': packed_item.projected_qty,
- 'incoming_rate': packed_item.incoming_rate,
- 'prevdoc_doctype': packed_item.prevdoc_doctype,
- 'parent_detail_docname': packed_item.parent_detail_docname
- })
-
-def update_product_bundle_price(doc, parent_items):
- """Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
-
- if not doc.get('items'):
+ "Make/Update packing list for Product Bundle Item."
+ if doc.get("_action") and doc._action == "update_after_submit":
return
- parent_items_index = 0
- bundle_price = 0
+ parent_items_price, reset = {}, False
+ set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates")
- for bundle_item in doc.get("packed_items"):
- if parent_items[parent_items_index][0] == bundle_item.parent_item:
- bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
- bundle_price += bundle_item.qty * bundle_item_rate
- else:
- update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
+ stale_packed_items_table = get_indexed_packed_items_table(doc)
- bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
- bundle_price = bundle_item.qty * bundle_item_rate
- parent_items_index += 1
+ reset = reset_packing_list(doc)
- # for the last product bundle
- if doc.get("packed_items"):
- update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
+ for item_row in doc.get("items"):
+ if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}):
+ for bundle_item in get_product_bundle_items(item_row.item_code):
+ pi_row = add_packed_item_row(
+ doc=doc, packing_item=bundle_item,
+ main_item_row=item_row, packed_items_table=stale_packed_items_table,
+ reset=reset
+ )
+ item_data = get_packed_item_details(bundle_item.item_code, doc.company)
+ update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
+ update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
+ update_packed_item_price_data(pi_row, item_data, doc)
+ update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
-def update_parent_item_price(doc, parent_item_code, bundle_price):
- parent_item_doc = doc.get('items', {'item_code': parent_item_code})[0]
+ if set_price_from_children: # create/update bundle item wise price dict
+ update_product_bundle_rate(parent_items_price, pi_row)
- current_parent_item_price = parent_item_doc.amount
- if current_parent_item_price != bundle_price:
- parent_item_doc.amount = bundle_price
- update_parent_item_rate(parent_item_doc, bundle_price)
+ if parent_items_price:
+ set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
-def update_parent_item_rate(parent_item_doc, bundle_price):
- parent_item_doc.rate = bundle_price/parent_item_doc.qty
+def get_indexed_packed_items_table(doc):
+ """
+ Create dict from stale packed items table like:
+ {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}}
-@frappe.whitelist()
-def get_items_from_product_bundle(args):
- args = json.loads(args)
- items = []
- bundled_items = get_product_bundle_items(args["item_code"])
- for item in bundled_items:
- args.update({
- "item_code": item.item_code,
- "qty": flt(args["quantity"]) * flt(item.qty)
- })
- items.append(get_item_details(args))
+ Use: to quickly retrieve/check if row existed in table instead of looping n times
+ """
+ indexed_table = {}
+ for packed_item in doc.get("packed_items"):
+ key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname)
+ indexed_table[key] = packed_item
- return items
+ return indexed_table
+
+def reset_packing_list(doc):
+ "Conditionally reset the table and return if it was reset or not."
+ reset_table = False
+ doc_before_save = doc.get_doc_before_save()
+
+ if doc_before_save:
+ # reset table if:
+ # 1. items were deleted
+ # 2. if bundle item replaced by another item (same no. of items but different items)
+ # we maintain list to track recurring item rows as well
+ items_before_save = [item.item_code for item in doc_before_save.get("items")]
+ items_after_save = [item.item_code for item in doc.get("items")]
+ reset_table = items_before_save != items_after_save
+ else:
+ # reset: if via Update Items OR
+ # if new mapped doc with packed items set (SO -> DN)
+ # (cannot determine action)
+ reset_table = True
+
+ if reset_table:
+ doc.set("packed_items", [])
+ return reset_table
+
+def get_product_bundle_items(item_code):
+ product_bundle = frappe.qb.DocType("Product Bundle")
+ product_bundle_item = frappe.qb.DocType("Product Bundle Item")
+
+ query = (
+ frappe.qb.from_(product_bundle_item)
+ .join(product_bundle).on(product_bundle_item.parent == product_bundle.name)
+ .select(
+ product_bundle_item.item_code,
+ product_bundle_item.qty,
+ product_bundle_item.uom,
+ product_bundle_item.description
+ ).where(
+ product_bundle.new_item_code == item_code
+ ).orderby(
+ product_bundle_item.idx
+ )
+ )
+ return query.run(as_dict=True)
+
+def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset):
+ """Add and return packed item row.
+ doc: Transaction document
+ packing_item (dict): Packed Item details
+ main_item_row (dict): Items table row corresponding to packed item
+ packed_items_table (dict): Packed Items table before save (indexed)
+ reset (bool): State if table is reset or preserved as is
+ """
+ exists, pi_row = False, {}
+
+ # check if row already exists in packed items table
+ key = (main_item_row.item_code, packing_item.item_code, main_item_row.name)
+ if packed_items_table.get(key):
+ pi_row, exists = packed_items_table.get(key), True
+
+ if not exists:
+ pi_row = doc.append('packed_items', {})
+ elif reset: # add row if row exists but table is reset
+ pi_row.idx, pi_row.name = None, None
+ pi_row = doc.append('packed_items', pi_row)
+
+ return pi_row
+
+def get_packed_item_details(item_code, company):
+ item = frappe.qb.DocType("Item")
+ item_default = frappe.qb.DocType("Item Default")
+ query = (
+ frappe.qb.from_(item)
+ .left_join(item_default)
+ .on(
+ (item_default.parent == item.name)
+ & (item_default.company == company)
+ ).select(
+ item.item_name, item.is_stock_item,
+ item.description, item.stock_uom,
+ item.valuation_rate,
+ item_default.default_warehouse
+ ).where(
+ item.name == item_code
+ )
+ )
+ return query.run(as_dict=True)[0]
+
+def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data):
+ pi_row.parent_item = main_item_row.item_code
+ pi_row.parent_detail_docname = main_item_row.name
+ pi_row.item_code = packing_item.item_code
+ pi_row.item_name = item_data.item_name
+ pi_row.uom = item_data.stock_uom
+ pi_row.qty = flt(packing_item.qty) * flt(main_item_row.stock_qty)
+ pi_row.conversion_factor = main_item_row.conversion_factor
+
+ if not pi_row.description:
+ pi_row.description = packing_item.get("description")
+
+def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc):
+ # TODO batch_no, actual_batch_qty, incoming_rate
+ if not pi_row.warehouse and not doc.amended_from:
+ fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse)
+ pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse)
+ else item_data.default_warehouse)
+
+ if not pi_row.target_warehouse:
+ pi_row.target_warehouse = main_item_row.get("target_warehouse")
+
+ bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse)
+ pi_row.actual_qty = flt(bin.get("actual_qty"))
+ pi_row.projected_qty = flt(bin.get("projected_qty"))
+
+def update_packed_item_price_data(pi_row, item_data, doc):
+ "Set price as per price list or from the Item master."
+ if pi_row.rate:
+ return
+
+ item_doc = frappe.get_cached_doc("Item", pi_row.item_code)
+ row_data = pi_row.as_dict().copy()
+ row_data.update({
+ "company": doc.get("company"),
+ "price_list": doc.get("selling_price_list"),
+ "currency": doc.get("currency")
+ })
+ rate = get_price_list_rate(row_data, item_doc).get("price_list_rate")
+
+ pi_row.rate = rate or item_data.get("valuation_rate") or 0.0
+
+def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc):
+ "Update packed item row details from cancelled doc into amended doc."
+ prev_doc_packed_items_map = None
+ if doc.amended_from:
+ prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items)
+
+ if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)):
+ prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code))
+ pi_row.batch_no = prev_doc_row[0].batch_no
+ pi_row.serial_no = prev_doc_row[0].serial_no
+ pi_row.warehouse = prev_doc_row[0].warehouse
+
+def get_packed_item_bin_qty(item, warehouse):
+ bin_data = frappe.db.get_values(
+ "Bin",
+ fieldname=["actual_qty", "projected_qty"],
+ filters={"item_code": item, "warehouse": warehouse},
+ as_dict=True
+ )
+
+ return bin_data[0] if bin_data else {}
+
+def get_cancelled_doc_packed_item_details(old_packed_items):
+ prev_doc_packed_items_map = {}
+ for items in old_packed_items:
+ prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict())
+ return prev_doc_packed_items_map
+
+def update_product_bundle_rate(parent_items_price, pi_row):
+ """
+ Update the price dict of Product Bundles based on the rates of the Items in the bundle.
+
+ Stucture:
+ {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0}
+ """
+ key = (pi_row.parent_item, pi_row.parent_detail_docname)
+ rate = parent_items_price.get(key)
+ if not rate:
+ parent_items_price[key] = 0.0
+
+ parent_items_price[key] += flt(pi_row.rate)
+
+def set_product_bundle_rate_amount(doc, parent_items_price):
+ "Set cumulative rate and amount in bundle item."
+ for item in doc.get("items"):
+ bundle_rate = parent_items_price.get((item.item_code, item.name))
+ if bundle_rate and bundle_rate != item.rate:
+ item.rate = bundle_rate
+ item.amount = flt(bundle_rate * item.qty)
def on_doctype_update():
frappe.db.add_index("Packed Item", ["item_code", "warehouse"])
-def get_old_packed_item_details(old_packed_items):
- old_packed_items_map = {}
- for items in old_packed_items:
- old_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict())
- return old_packed_items_map
+
+@frappe.whitelist()
+def get_items_from_product_bundle(row):
+ row, items = json.loads(row), []
+
+ bundled_items = get_product_bundle_items(row["item_code"])
+ for item in bundled_items:
+ row.update({
+ "item_code": item.item_code,
+ "qty": flt(row["quantity"]) * flt(item.qty)
+ })
+ items.append(get_item_details(row))
+
+ return items
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
new file mode 100644
index 0000000..5cbaa1e
--- /dev/null
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.tests.utils import ERPNextTestCase, change_settings
+
+
+class TestPackedItem(ERPNextTestCase):
+ "Test impact on Packed Items table in various scenarios."
+ @classmethod
+ def setUpClass(cls) -> None:
+ make_item("_Test Product Bundle X", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Normal Stock Item", {"is_stock_item": 1})
+
+ make_product_bundle(
+ "_Test Product Bundle X",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"],
+ qty=2
+ )
+
+ def test_adding_bundle_item(self):
+ "Test impact on packed items if bundle item row is added."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
+ do_not_submit=True)
+
+ self.assertEqual(so.items[0].qty, 1)
+ self.assertEqual(len(so.packed_items), 2)
+ self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1")
+ self.assertEqual(so.packed_items[0].qty, 2)
+
+ def test_updating_bundle_item(self):
+ "Test impact on packed items if bundle item row is updated."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
+ do_not_submit=True)
+
+ so.items[0].qty = 2 # change qty
+ so.save()
+
+ self.assertEqual(so.packed_items[0].qty, 4)
+ self.assertEqual(so.packed_items[1].qty, 4)
+
+ # change item code to non bundle item
+ so.items[0].item_code = "_Test Normal Stock Item"
+ so.save()
+
+ self.assertEqual(len(so.packed_items), 0)
+
+ def test_recurring_bundle_item(self):
+ "Test impact on packed items if same bundle item is added and removed."
+ so_items = []
+ for qty in [2, 4, 6, 8]:
+ so_items.append({
+ "item_code": "_Test Product Bundle X",
+ "qty": qty,
+ "rate": 400,
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ # create SO with recurring bundle item
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 8)
+ self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2")
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 8)
+ self.assertEqual(so.packed_items[5].qty, 12)
+ self.assertEqual(so.packed_items[7].qty, 16)
+
+ # delete intermediate row (2nd)
+ del so.items[1]
+ so.save()
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 6)
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 12)
+ self.assertEqual(so.packed_items[5].qty, 16)
+
+ # delete last row
+ del so.items[2]
+ so.save()
+
+ # check alternate rows for qty
+ self.assertEqual(len(so.packed_items), 4)
+ self.assertEqual(so.packed_items[1].qty, 4)
+ self.assertEqual(so.packed_items[3].qty, 12)
+
+ @change_settings("Selling Settings", {"editable_bundle_item_rates": 1})
+ def test_bundle_item_cumulative_price(self):
+ "Test if Bundle Item rate is cumulative from packed items."
+ so = make_sales_order(item_code = "_Test Product Bundle X", qty=2,
+ do_not_submit=True)
+
+ so.packed_items[0].rate = 150
+ so.packed_items[1].rate = 200
+ so.save()
+
+ self.assertEqual(so.items[0].rate, 350)
+ self.assertEqual(so.items[0].amount, 700)
+
+ def test_newly_mapped_doc_packed_items(self):
+ "Test impact on packed items in newly mapped DN from SO."
+ so_items = []
+ for qty in [2, 4]:
+ so_items.append({
+ "item_code": "_Test Product Bundle X",
+ "qty": qty,
+ "rate": 400,
+ "warehouse": "_Test Warehouse - _TC"
+ })
+
+ # create SO with recurring bundle item
+ so = make_sales_order(item_list=so_items)
+
+ dn = make_delivery_note(so.name)
+ dn.items[1].qty = 3 # change second row qty for inserting doc
+ dn.save()
+
+ self.assertEqual(len(dn.packed_items), 4)
+ self.assertEqual(dn.packed_items[2].qty, 6)
+ self.assertEqual(dn.packed_items[3].qty, 6)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 1257057..ffdf8c4 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -286,7 +286,7 @@
if warehouse_account.get(d.warehouse):
stock_value_diff = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": self.name,
- "voucher_detail_no": d.name, "warehouse": d.warehouse}, "stock_value_difference")
+ "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
if not stock_value_diff:
continue
diff --git a/erpnext/public/js/cart.js b/erpnext/templates/pages/cart.js
similarity index 99%
rename from erpnext/public/js/cart.js
rename to erpnext/templates/pages/cart.js
index 69357ee..fb2d159 100644
--- a/erpnext/public/js/cart.js
+++ b/erpnext/templates/pages/cart.js
@@ -1,9 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-// js inside blog page
-
-// shopping cart
+// JS exclusive to /cart page
frappe.provide("erpnext.e_commerce.shopping_cart");
var shopping_cart = erpnext.e_commerce.shopping_cart;
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 0aca1a0..4a6c834 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -783,6 +783,7 @@
Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein,
Default BOM for {0} not found,Standardstückliste für {0} nicht gefunden,
Default BOM not found for Item {0} and Project {1},Standard-Stückliste nicht gefunden für Position {0} und Projekt {1},
+Default In-Transit Warehouse, Standardlager für Waren im Transit,
Default Letter Head,Standardbriefkopf,
Default Tax Template,Standardsteuervorlage,
Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,"Die Standard-Maßeinheit für Artikel {0} kann nicht direkt geändert werden, weil Sie bereits einige Transaktionen mit einer anderen Maßeinheit durchgeführt haben. Sie müssen einen neuen Artikel erstellen, um eine andere Standard-Maßeinheit verwenden zukönnen.",
@@ -1054,6 +1055,7 @@
Fiscal Year {0} not found,Das Geschäftsjahr {0} nicht gefunden,
Fixed Asset,Anlagevermögen,
Fixed Asset Item must be a non-stock item.,Posten des Anlagevermögens muss ein Nichtlagerposition sein.,
+Fixed Asset Defaults, Standards für Anlagevermögen,
Fixed Assets,Anlagevermögen,
Following Material Requests have been raised automatically based on Item's re-order level,Folgende Materialanfragen wurden automatisch auf der Grundlage der Nachbestellmenge des Artikels generiert,
Following accounts might be selected in GST Settings:,In den GST-Einstellungen können folgende Konten ausgewählt werden:,
@@ -2352,6 +2354,7 @@
Reopen,Wieder öffnen,
Reorder Level,Meldebestand,
Reorder Qty,Nachbestellmenge,
+Repair and Maintenance Account, Konto für Reparatur/Instandhaltung von Anlagen und Maschinen,
Repeat Customer Revenue,Umsatz Bestandskunden,
Repeat Customers,Bestandskunden,
Replace BOM and update latest price in all BOMs,Ersetzen Sie die Stückliste und aktualisieren Sie den aktuellen Preis in allen Stücklisten,
@@ -3796,7 +3799,7 @@
Invalid Barcode. There is no Item attached to this barcode.,Ungültiger Barcode. Es ist kein Artikel an diesen Barcode angehängt.,
Invalid credentials,Ungültige Anmeldeinformationen,
Invite as User,Als Benutzer einladen,
-Issue Priority.,Ausgabepriorität.,
+Issue Priority.,Anfragepriorität.,
Issue Type.,Problemtyp.,
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.","Es scheint, dass ein Problem mit der Stripe-Konfiguration des Servers vorliegt. Im Falle eines Fehlers wird der Betrag Ihrem Konto gutgeschrieben.",
Item Reported,Gegenstand gemeldet,
@@ -4857,6 +4860,7 @@
Allocated,Zugewiesen,
Payment Gateway Account,Payment Gateway Konto,
Payment Account,Zahlungskonto,
+Default Payment Discount Account, Standard Rabattkonto für Zahlungen,
Default Payment Request Message,Standard Payment Request Message,
PMO-,PMO-,
Payment Order Type,Zahlungsauftragsart,
@@ -7789,7 +7793,8 @@
Discount Allowed Account,Rabatt erlaubtes Konto,
Discount Received Account,Discount Received Account,
Exchange Gain / Loss Account,Konto für Wechselkursdifferenzen,
-Unrealized Exchange Gain/Loss Account,Konto für unrealisierte Wechselkurs-Gewinne / -Verluste,
+Unrealized Exchange Gain/Loss Account,Konto für nicht realisierte Wechselkurs-Gewinne/ -Verluste,
+Unrealized Profit / Loss Account, Konto für nicht realisierten Gewinn/Verlust,
Allow Account Creation Against Child Company,Kontoerstellung für untergeordnete Unternehmen zulassen,
Default Payable Account,Standard-Verbindlichkeitenkonto,
Default Employee Advance Account,Standardkonto für Vorschüsse an Arbeitnehmer,