diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml
new file mode 100644
index 00000000000..ef38974ae27
--- /dev/null
+++ b/.github/workflows/initiate_release.yml
@@ -0,0 +1,32 @@
+# This workflow is agnostic to branches. Only maintain on develop branch.
+# To add/remove versions just modify the matrix.
+
+name: Create weekly release pull requests
+on:
+ schedule:
+ # 9:30 UTC => 3 PM IST Tuesday
+ - cron: "30 9 * * 2"
+ workflow_dispatch:
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ version: ["13", "14"]
+
+ steps:
+ - uses: octokit/request-action@v2.x
+ with:
+ route: POST /repos/{owner}/{repo}/pulls
+ owner: frappe
+ repo: erpnext
+ title: |-
+ "chore: release v${{ matrix.version }}"
+ body: "Automated weekly release."
+ base: version-${{ matrix.version }}
+ head: version-${{ matrix.version }}-hotfix
+ env:
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
diff --git a/README.md b/README.md
index cea3472447f..0708266a470 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt))
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
+By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
+
## Logo and Trademark Policy
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index a964965c26f..f74562086ef 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", {
},
show_import_status(frm) {
- let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
let successful_records = import_log.filter((log) => log.success);
let failed_records = import_log.filter((log) => !log.success);
if (successful_records.length === 0) return;
@@ -309,7 +309,7 @@ frappe.ui.form.on("Bank Statement Import", {
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
show_import_preview(frm, preview_data) {
- let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
if (
frm.import_preview &&
@@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", {
},
show_import_log(frm) {
- let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
let logs = import_log;
frm.toggle_display("import_log", false);
frm.toggle_display("import_log_section", logs.length > 0);
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
index 7ffff02850c..eede3bdc6d1 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
@@ -24,7 +24,7 @@
"section_import_preview",
"import_preview",
"import_log_section",
- "import_log",
+ "statement_import_log",
"show_failed_logs",
"import_log_preview",
"reference_doctype",
@@ -90,12 +90,6 @@
"options": "JSON",
"read_only": 1
},
- {
- "fieldname": "import_log",
- "fieldtype": "Code",
- "label": "Import Log",
- "options": "JSON"
- },
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
@@ -198,11 +192,17 @@
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "statement_import_log",
+ "fieldtype": "Code",
+ "label": "Statement Import Log",
+ "options": "JSON"
}
],
"hide_toolbar": 1,
"links": [],
- "modified": "2021-05-12 14:17:37.777246",
+ "modified": "2022-09-07 11:11:40.293317",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 3f5c064f4b9..d8880f70417 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -53,15 +53,13 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column"))
- from frappe.core.page.background_jobs.background_jobs import get_info
+ from frappe.utils.background_jobs import is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
- enqueued_jobs = [d.get("job_name") for d in get_info()]
-
- if self.name not in enqueued_jobs:
+ if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
index 18e5a1ac85b..7cd6d04c35c 100644
--- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
@@ -4,22 +4,20 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.doctype.account.account import merge_account
class LedgerMerge(Document):
def start_merge(self):
- from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
- enqueued_jobs = [d.get("job_name") for d in get_info()]
-
- if self.name not in enqueued_jobs:
+ if not is_job_queued(self.name):
enqueue(
start_merge,
queue="default",
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 99377421c58..f7df1ff4a10 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -6,7 +6,7 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import flt, nowdate
-from frappe.utils.background_jobs import enqueue
+from frappe.utils.background_jobs import enqueue, is_job_queued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document):
if len(invoices) < 50:
return start_import(invoices)
else:
- from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
- enqueued_jobs = [d.get("job_name") for d in get_info()]
- if self.name not in enqueued_jobs:
+ if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 81a234a20a7..a0a1c8c2696 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -6,11 +6,10 @@ import json
import frappe
from frappe import _
-from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
-from frappe.utils.background_jobs import enqueue
+from frappe.utils.background_jobs import enqueue, is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -467,7 +466,7 @@ def enqueue_job(job, **kwargs):
closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name")
- if not job_already_enqueued(job_name):
+ if not is_job_queued(job_name):
enqueue(
job,
**kwargs,
@@ -491,12 +490,6 @@ def check_scheduler_status():
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
-def job_already_enqueued(job_name):
- enqueued_jobs = [d.get("job_name") for d in get_info()]
- if job_name in enqueued_jobs:
- return True
-
-
def safe_load_json(message):
try:
json_message = json.loads(message).get("message")
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 82705a9cea4..0da44a464e7 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -25,7 +25,7 @@
-
| {{ _("Date") }} | diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index fac9be7bdb1..4d28d106604 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -34,8 +34,8 @@ pricing_rule_fields = [ other_fields = [ "min_qty", "max_qty", - "min_amt", - "max_amt", + "min_amount", + "max_amount", "priority", "warehouse", "threshold_percentage", @@ -246,7 +246,11 @@ def prepare_pricing_rule( def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) for field in other_fields + discount_fields: - pr.set(field, child_doc_fields.get(field)) + target_field = field + if target_field in ["min_amount", "max_amount"]: + target_field = "min_amt" if field == "min_amount" else "max_amt" + + pr.set(target_field, child_doc_fields.get(field)) pr.promotional_scheme_id = child_doc_fields.name pr.promotional_scheme = doc.name diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index b3b9d7b2086..9e576fb8775 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -90,6 +90,23 @@ class TestPromotionalScheme(unittest.TestCase): price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) + def test_min_max_amount_configuration(self): + ps = make_promotional_scheme() + ps.price_discount_slabs[0].min_amount = 10 + ps.price_discount_slabs[0].max_amount = 1000 + ps.save() + + price_rules_data = frappe.db.get_value( + "Pricing Rule", {"promotional_scheme": ps.name}, ["min_amt", "max_amt"], as_dict=1 + ) + + self.assertEqual(price_rules_data.min_amt, 10) + self.assertEqual(price_rules_data.max_amt, 1000) + + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) + self.assertEqual(price_rules, []) + def make_promotional_scheme(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4008863e9ba..1f5879d7bff 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -710,6 +710,7 @@ class SalesInvoice(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] @@ -2161,6 +2162,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_item(source, target, source_parent): target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item": + target.purchase_order = source.parent + target.purchase_order_item = source.name + + if ( + source.get("purchase_order") + and source.get("purchase_order_item") + and target.doctype == "Purchase Invoice Item" + ): + target.purchase_order = source.purchase_order + target.po_detail = source.purchase_order_item item_field_map = { "doctype": target_doctype + " Item", @@ -2187,6 +2199,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "serial_no": "serial_no", } ) + elif target_doctype == "Sales Order": + item_field_map["field_map"].update( + { + source_document_warehouse_field: "warehouse", + } + ) doclist = get_mapped_doc( doctype, @@ -2231,6 +2249,7 @@ def get_received_items(reference_name, doctype, reference_fieldname): def set_purchase_references(doc): # add internal PO or PR links if any + if doc.is_internal_transfer(): if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) @@ -2260,15 +2279,6 @@ def set_purchase_references(doc): warehouse_map, ) - if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = get_pd_details( - "Purchase Order Item", so_item_map, "sales_order_item" - ) - - update_pi_items( - doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map - ) - def update_pi_items( doc, @@ -2284,13 +2294,19 @@ def update_pi_items( item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): for item in doc.get("items"): - item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) - item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def get_delivery_note_details(internal_reference): diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 7cddf123e24..4f97b63789a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -96,6 +96,10 @@ "delivery_note", "dn_detail", "delivered_qty", + "internal_transfer_section", + "purchase_order", + "column_break_92", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -840,12 +844,38 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_92", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-08-26 12:06:31.205417", + "modified": "2022-09-06 14:17:43.394309", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 16072f331f1..f4a50a5f915 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -489,7 +489,6 @@ def make_reverse_gl_entries( ).run(as_dict=1) if gl_entries: - create_payment_ledger_entry(gl_entries, cancel=1) create_payment_ledger_entry( gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding ) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 3f504b13f67..8557c03bd5e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -784,7 +784,7 @@ class ReceivablePayableReport(object): def add_customer_filters( self, ): - self.customter = qb.DocType("Customer") + self.customer = qb.DocType("Customer") if self.filters.get("customer_group"): self.get_hierarchical_filters("Customer Group", "customer_group") @@ -838,7 +838,7 @@ class ReceivablePayableReport(object): customer = self.customer groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt)) customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups)) - self.qb_selection_filter.append(ple.isin(ple.party.isin(customers))) + self.qb_selection_filter.append(ple.party.isin(customers)) def add_accounting_dimensions_filters(self): accounting_dimensions = get_accounting_dimensions(as_list=False) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index aa50487d78e..acca380672d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -60,6 +60,7 @@ "section_break_45", "before_items_section", "scan_barcode", + "set_from_warehouse", "items_col_break", "set_warehouse", "items_section", @@ -1166,13 +1167,20 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 + }, + { + "depends_on": "is_internal_supplier", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-07 11:06:46.035093", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 01b55c00d6b..05b5a8e7b8c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -23,5 +23,6 @@ def get_data(): "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, + {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index bd7e4e8d865..6c1bcc7dd49 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -7,8 +7,10 @@ import json import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) @@ -796,6 +798,111 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def test_internal_transfer_flow(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + make_inter_company_purchase_invoice, + ) + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + frappe.db.set_value("Selling Settings", None, "maintain_same_sales_rate", 1) + frappe.db.set_value("Buying Settings", None, "maintain_same_rate", 1) + + prepare_data_for_internal_transfer() + supplier = "_Test Internal Supplier 2" + + po = create_purchase_order( + company="_Test Company with perpetual inventory", + supplier=supplier, + warehouse="Stores - TCP1", + from_warehouse="_Test Internal Warehouse New 1 - TCP1", + qty=2, + rate=1, + ) + + so = make_inter_company_sales_order(po.name) + so.items[0].delivery_date = today() + self.assertEqual(so.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(so.items[0].purchase_order) + self.assertTrue(so.items[0].purchase_order_item) + so.submit() + + dn = make_delivery_note(so.name) + dn.items[0].target_warehouse = "_Test Internal Warehouse GIT - TCP1" + self.assertEqual(dn.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(dn.items[0].purchase_order) + self.assertTrue(dn.items[0].purchase_order_item) + + self.assertEqual(po.items[0].name, dn.items[0].purchase_order_item) + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + self.assertEqual(pr.items[0].warehouse, "Stores - TCP1") + self.assertTrue(pr.items[0].purchase_order) + self.assertTrue(pr.items[0].purchase_order_item) + self.assertEqual(po.items[0].name, pr.items[0].purchase_order_item) + pr.submit() + + si = make_sales_invoice(so.name) + self.assertEqual(si.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(si.items[0].purchase_order) + self.assertTrue(si.items[0].purchase_order_item) + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + self.assertTrue(pi.items[0].purchase_order) + self.assertTrue(pi.items[0].po_detail) + pi.submit() + + po.load_from_db() + self.assertEqual(po.status, "Completed") + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company) + + create_warehouse("_Test Internal Warehouse GIT", company=company) + + make_purchase_receipt(company=company, warehouse=warehouse, qty=2, rate=100) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -847,6 +954,7 @@ def create_purchase_order(**args): { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", + "from_warehouse": args.from_warehouse, "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9845396ff..82e92e87bc9 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -10,12 +10,14 @@ "item_code", "supplier_part_no", "item_name", + "brand", "product_bundle", "fg_item", "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", + "item_group", "section_break_5", "description", "col_break1", @@ -58,9 +60,12 @@ "base_net_rate", "base_net_amount", "warehouse_and_reference", + "from_warehouse", "warehouse", + "column_break_54", "actual_qty", "company_total_stock", + "references_section", "material_request", "material_request_item", "sales_order", @@ -73,8 +78,6 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", - "item_group", - "brand", "section_break_56", "received_qty", "returned_qty", @@ -442,13 +445,13 @@ { "fieldname": "warehouse_and_reference", "fieldtype": "Section Break", - "label": "Warehouse and Reference" + "label": "Warehouse Settings" }, { "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "Warehouse", + "label": "Target Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -760,7 +763,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at Warehouse", + "label": "Available Qty at Target Warehouse", "print_hide": 1, "read_only": 1 }, @@ -868,13 +871,30 @@ "fieldtype": "Float", "label": "Finished Good Item Qty", "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" + }, + { + "depends_on": "eval:parent.is_internal_supplier", + "fieldname": "from_warehouse", + "fieldtype": "Link", + "label": "From Warehouse", + "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:29:40.602349", + "modified": "2022-09-07 11:12:38.634976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e689d567a17..9244844e584 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -205,6 +205,10 @@ class AccountsController(TransactionBase): def on_trash(self): # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + ple = frappe.qb.DocType("Payment Ledger Entry") + frappe.qb.from_(ple).delete().where( + (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) + ).run() frappe.db.sql( "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) ) @@ -373,7 +377,7 @@ class AccountsController(TransactionBase): ) def validate_inter_company_reference(self): - if self.doctype not in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): + if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): return if self.is_internal_transfer(): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 7ab8f81bd9a..584266d53b1 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -309,7 +309,11 @@ class BuyingController(SubcontractingController): rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: field = "incoming_rate" if self.get("is_internal_supplier") else "rate" - rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) + rate = flt( + frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) + * (d.conversion_factor or 1), + d.precision("rate"), + ) if self.is_internal_transfer(): if rate != d.rate: diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py deleted file mode 100644 index c06fb5930dc..00000000000 --- a/erpnext/controllers/employee_boarding_controller.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe -from frappe import _ -from frappe.desk.form import assign_to -from frappe.model.document import Document -from frappe.utils import add_days, flt, unique - -from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee -from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday - - -class EmployeeBoardingController(Document): - """ - Create the project and the task for the boarding process - Assign to the concerned person and roles as per the onboarding/separation template - """ - - def validate(self): - # remove the task if linked before submitting the form - if self.amended_from: - for activity in self.activities: - activity.task = "" - - def on_submit(self): - # create the project for the given employee onboarding - project_name = _(self.doctype) + " : " - if self.doctype == "Employee Onboarding": - project_name += self.job_applicant - else: - project_name += self.employee - - project = frappe.get_doc( - { - "doctype": "Project", - "project_name": project_name, - "expected_start_date": self.date_of_joining - if self.doctype == "Employee Onboarding" - else self.resignation_letter_date, - "department": self.department, - "company": self.company, - } - ).insert(ignore_permissions=True, ignore_mandatory=True) - - self.db_set("project", project.name) - self.db_set("boarding_status", "Pending") - self.reload() - self.create_task_and_notify_user() - - def create_task_and_notify_user(self): - # create the task for the given project and assign to the concerned person - holiday_list = self.get_holiday_list() - - for activity in self.activities: - if activity.task: - continue - - dates = self.get_task_dates(activity, holiday_list) - - task = frappe.get_doc( - { - "doctype": "Task", - "project": self.project, - "subject": activity.activity_name + " : " + self.employee_name, - "description": activity.description, - "department": self.department, - "company": self.company, - "task_weight": activity.task_weight, - "exp_start_date": dates[0], - "exp_end_date": dates[1], - } - ).insert(ignore_permissions=True) - activity.db_set("task", task.name) - - users = [activity.user] if activity.user else [] - if activity.role: - user_list = frappe.db.sql_list( - """ - SELECT - DISTINCT(has_role.parent) - FROM - `tabHas Role` has_role - LEFT JOIN `tabUser` user - ON has_role.parent = user.name - WHERE - has_role.parenttype = 'User' - AND user.enabled = 1 - AND has_role.role = %s - """, - activity.role, - ) - users = unique(users + user_list) - - if "Administrator" in users: - users.remove("Administrator") - - # assign the task the users - if users: - self.assign_task_to_users(task, users) - - def get_holiday_list(self): - if self.doctype == "Employee Separation": - return get_holiday_list_for_employee(self.employee) - else: - if self.employee: - return get_holiday_list_for_employee(self.employee) - else: - if not self.holiday_list: - frappe.throw(_("Please set the Holiday List."), frappe.MandatoryError) - else: - return self.holiday_list - - def get_task_dates(self, activity, holiday_list): - start_date = end_date = None - - if activity.begin_on is not None: - start_date = add_days(self.boarding_begins_on, activity.begin_on) - start_date = self.update_if_holiday(start_date, holiday_list) - - if activity.duration is not None: - end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration) - end_date = self.update_if_holiday(end_date, holiday_list) - - return [start_date, end_date] - - def update_if_holiday(self, date, holiday_list): - while is_holiday(holiday_list, date): - date = add_days(date, 1) - return date - - def assign_task_to_users(self, task, users): - for user in users: - args = { - "assign_to": [user], - "doctype": task.doctype, - "name": task.name, - "description": task.description or task.subject, - "notify": self.notify_users_by_email, - } - assign_to.add(args) - - def on_cancel(self): - # delete task project - project = self.project - for task in frappe.get_all("Task", filters={"project": project}): - frappe.delete_doc("Task", task.name, force=1) - frappe.delete_doc("Project", project, force=1) - self.db_set("project", "") - for activity in self.activities: - activity.db_set("task", "") - - frappe.msgprint( - _("Linked Project {} and Tasks deleted.").format(project), alert=True, indicator="blue" - ) - - -@frappe.whitelist() -def get_onboarding_details(parent, parenttype): - return frappe.get_all( - "Employee Boarding Activity", - fields=[ - "activity_name", - "role", - "user", - "required_for_employee_creation", - "description", - "task_weight", - "begin_on", - "duration", - ], - filters={"parent": parent, "parenttype": parenttype}, - order_by="idx", - ) - - -def update_employee_boarding_status(project): - employee_onboarding = frappe.db.exists("Employee Onboarding", {"project": project.name}) - employee_separation = frappe.db.exists("Employee Separation", {"project": project.name}) - - if not (employee_onboarding or employee_separation): - return - - status = "Pending" - if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0: - status = "In Process" - elif flt(project.percent_complete) == 100.0: - status = "Completed" - - if employee_onboarding: - frappe.db.set_value("Employee Onboarding", employee_onboarding, "boarding_status", status) - elif employee_separation: - frappe.db.set_value("Employee Separation", employee_separation, "boarding_status", status) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index a3d41ab29af..5e9c069b1d5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -311,6 +311,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": p.get("incoming_rate"), + "item_row": p, } ) ) @@ -334,6 +335,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": d.get("incoming_rate"), + "item_row": d, } ) ) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 197d2ba2dc8..6e7d2b33c28 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -307,6 +307,20 @@ class StatusUpdater(Document): def limits_crossed_error(self, args, item, qty_or_amount): """Raise exception for limits crossed""" + if ( + self.doctype in ["Sales Invoice", "Delivery Note"] + and qty_or_amount == "amount" + and self.is_internal_customer + ): + return + + elif ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and qty_or_amount == "amount" + and self.is_internal_supplier + ): + return + if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 36bed36484e..9149b4d8570 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -390,6 +390,10 @@ class StockController(AccountsController): return sl_dict def update_inventory_dimensions(self, row, sl_dict) -> None: + # To handle delivery note and sales invoice + if row.get("item_row"): + row = row.get("item_row") + dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) for dimension in dimensions: if not dimension: @@ -407,9 +411,17 @@ class StockController(AccountsController): "DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" ) + if not fieldname: + fieldname = frappe.get_cached_value( + "Custom Field", {"dt": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" + ) + if fieldname and self.get(fieldname): sl_dict[dimension.target_fieldname] = self.get(fieldname) + if sl_dict[dimension.target_fieldname] and self.docstatus == 1: + row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname]) + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries @@ -688,6 +700,47 @@ class StockController(AccountsController): else: create_repost_item_valuation_entry(args) + def add_gl_entry( + self, + gl_entries, + account, + cost_center, + debit, + credit, + remarks, + against_account, + debit_in_account_currency=None, + credit_in_account_currency=None, + account_currency=None, + project=None, + voucher_detail_no=None, + item=None, + posting_date=None, + ): + + gl_entry = { + "account": account, + "cost_center": cost_center, + "debit": debit, + "credit": credit, + "against": against_account, + "remarks": remarks, + } + + if voucher_detail_no: + gl_entry.update({"voucher_detail_no": voucher_detail_no}) + + if debit_in_account_currency: + gl_entry.update({"debit_in_account_currency": debit_in_account_currency}) + + if credit_in_account_currency: + gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) + + if posting_date: + gl_entry.update({"posting_date": posting_date}) + + gl_entries.append(self.get_gl_dict(gl_entry, item=item)) + def repost_required_for_queue(doc: StockController) -> bool: """check if stock document contains repeated item-warehouse with queue based valuation. diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 1372c89d470..bbd950ed37a 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -7,6 +7,7 @@ from collections import defaultdict import frappe from frappe import _ +from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, cstr, flt, get_link_to_form from erpnext.controllers.stock_controller import StockController @@ -870,7 +871,18 @@ def add_items_in_ste( def make_return_stock_entry_for_subcontract( available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" ): - ste_doc = frappe.new_doc("Stock Entry") + ste_doc = get_mapped_doc( + order_doctype, + order_doc.name, + { + order_doctype: { + "doctype": "Stock Entry", + "field_no_map": ["purchase_order", "subcontracting_order"], + }, + }, + ignore_child_tables=True, + ) + ste_doc.purpose = "Material Transfer" if order_doctype == "Purchase Order": diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index bc503f5440a..8490d145286 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -897,7 +897,7 @@ def make_stock_transfer_entry(**args): "item_name": row.item_code, "rate": row.rate or 100, "stock_uom": row.stock_uom or "Nos", - "warehouse": row.warehuose or "_Test Warehouse - _TC", + "warehouse": row.warehouse or "_Test Warehouse - _TC", } item_details = args.itemwise_details.get(row.item_code) @@ -1031,9 +1031,9 @@ def get_subcontracting_order(**args): if not args.service_items: service_items = [ { - "warehouse": "_Test Warehouse - _TC", + "warehouse": args.warehouse or "_Test Warehouse - _TC", "item_code": "Subcontracted Service Item 7", - "qty": 5, + "qty": 10, "rate": 100, "fg_item": "Subcontracted Item SA7", "fg_item_qty": 10, @@ -1046,6 +1046,7 @@ def get_subcontracting_order(**args): rm_items=service_items, is_subcontracted=1, supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC", + company=args.company, ) return create_subcontracting_order(po_name=po.name, **args) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js index 0d5bfcbaf40..a0fd91e866f 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.js @@ -11,17 +11,24 @@ frappe.query_reports["BOM Stock Calculated"] = { "options": "BOM", "reqd": 1 }, - { - "fieldname": "qty_to_make", - "label": __("Quantity to Make"), - "fieldtype": "Int", - "default": "1" - }, - - { + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + }, + { + "fieldname": "qty_to_make", + "label": __("Quantity to Make"), + "fieldtype": "Float", + "default": "1.0", + "reqd": 1 + }, + { "fieldname": "show_exploded_view", "label": __("Show exploded view"), - "fieldtype": "Check" + "fieldtype": "Check", + "default": false, } ] } diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 933be3e0140..ec4b25c859f 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -4,29 +4,31 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils.data import comma_and +from pypika.terms import ExistsCriterion def execute(filters=None): - # if not filters: filters = {} columns = get_columns() - summ_data = [] + data = [] - data = get_bom_stock(filters) + bom_data = get_bom_data(filters) qty_to_make = filters.get("qty_to_make") - manufacture_details = get_manufacturer_records() - for row in data: - reqd_qty = qty_to_make * row.actual_qty - last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") - summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details)) - return columns, summ_data + for row in bom_data: + required_qty = qty_to_make * row.qty_per_unit + last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate") + + data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details)) + + return columns, data -def get_report_data(last_pur_price, reqd_qty, row, manufacture_details): - to_build = row.to_build if row.to_build > 0 else 0 - diff_qty = to_build - reqd_qty +def get_report_data(last_purchase_rate, required_qty, row, manufacture_details): + qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0 + difference_qty = row.actual_qty - required_qty return [ row.item_code, row.description, @@ -34,85 +36,126 @@ def get_report_data(last_pur_price, reqd_qty, row, manufacture_details): comma_and( manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False ), + qty_per_unit, row.actual_qty, - str(to_build), - reqd_qty, - diff_qty, - last_pur_price, + required_qty, + difference_qty, + last_purchase_rate, ] def get_columns(): - """return columns""" - columns = [ - _("Item") + ":Link/Item:100", - _("Description") + "::150", - _("Manufacturer") + "::250", - _("Manufacturer Part Number") + "::250", - _("Qty") + ":Float:50", - _("Stock Qty") + ":Float:100", - _("Reqd Qty") + ":Float:100", - _("Diff Qty") + ":Float:100", - _("Last Purchase Price") + ":Float:100", + return [ + { + "fieldname": "item", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "fieldname": "description", + "label": _("Description"), + "fieldtype": "Data", + "width": 150, + }, + { + "fieldname": "manufacturer", + "label": _("Manufacturer"), + "fieldtype": "Data", + "width": 120, + }, + { + "fieldname": "manufacturer_part_number", + "label": _("Manufacturer Part Number"), + "fieldtype": "Data", + "width": 150, + }, + { + "fieldname": "qty_per_unit", + "label": _("Qty Per Unit"), + "fieldtype": "Float", + "width": 110, + }, + { + "fieldname": "available_qty", + "label": _("Available Qty"), + "fieldtype": "Float", + "width": 120, + }, + { + "fieldname": "required_qty", + "label": _("Required Qty"), + "fieldtype": "Float", + "width": 120, + }, + { + "fieldname": "difference_qty", + "label": _("Difference Qty"), + "fieldtype": "Float", + "width": 130, + }, + { + "fieldname": "last_purchase_rate", + "label": _("Last Purchase Rate"), + "fieldtype": "Float", + "width": 160, + }, ] - return columns -def get_bom_stock(filters): - conditions = "" - bom = filters.get("bom") - - table = "`tabBOM Item`" - qty_field = "qty" - +def get_bom_data(filters): if filters.get("show_exploded_view"): - table = "`tabBOM Explosion Item`" - qty_field = "stock_qty" + bom_item_table = "BOM Explosion Item" + else: + bom_item_table = "BOM Item" + + bom_item = frappe.qb.DocType(bom_item_table) + bin = frappe.qb.DocType("Bin") + + query = ( + frappe.qb.from_(bom_item) + .left_join(bin) + .on(bom_item.item_code == bin.item_code) + .select( + bom_item.item_code, + bom_item.description, + bom_item.qty_consumed_per_unit.as_("qty_per_unit"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + ) + .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) + .groupby(bom_item.item_code) + ) if filters.get("warehouse"): warehouse_details = frappe.db.get_value( "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) + if warehouse_details: - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) ) else: - conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) + query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse"))) - else: - conditions += "" - - return frappe.db.sql( - """ - SELECT - bom_item.item_code, - bom_item.description, - bom_item.{qty_field}, - ifnull(sum(ledger.actual_qty), 0) as actual_qty, - ifnull(sum(FLOOR(ledger.actual_qty / bom_item.{qty_field})), 0) as to_build - FROM - {table} AS bom_item - LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code - {conditions} - - WHERE - bom_item.parent = '{bom}' and bom_item.parenttype='BOM' - - GROUP BY bom_item.item_code""".format( - qty_field=qty_field, table=table, conditions=conditions, bom=bom - ), - as_dict=1, - ) + return query.run(as_dict=True) def get_manufacturer_records(): details = frappe.get_all( "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] ) + manufacture_details = frappe._dict() for detail in details: dic = manufacture_details.setdefault(detail.get("item_code"), {}) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py new file mode 100644 index 00000000000..8ad980fa19a --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_calculated/test_bom_stock_calculated.py @@ -0,0 +1,115 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from frappe.tests.utils import FrappeTestCase + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import ( + execute as bom_stock_calculated_report, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestBOMStockCalculated(FrappeTestCase): + def setUp(self): + self.fg_item, self.rm_items = create_items() + self.boms = create_boms(self.fg_item, self.rm_items) + + def test_bom_stock_calculated(self): + qty_to_make = 10 + + # Case 1: When Item(s) Qty and Stock Qty are equal. + data = bom_stock_calculated_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[0].name, + } + )[1] + expected_data = get_expected_data(self.boms[0], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1. + data = bom_stock_calculated_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[1].name, + } + )[1] + expected_data = get_expected_data(self.boms[1], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1. + data = bom_stock_calculated_report( + filters={ + "qty_to_make": qty_to_make, + "bom": self.boms[2].name, + } + )[1] + expected_data = get_expected_data(self.boms[2], qty_to_make) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def create_boms(fg_item, rm_items): + def update_bom_items(bom, uom, conversion_factor): + for item in bom.items: + item.uom = uom + item.conversion_factor = conversion_factor + + return bom + + bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10) + + bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom2 = update_bom_items(bom2, "Box", 10) + bom2.save() + bom2.submit() + + bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True) + bom3 = update_bom_items(bom3, "Box", 10) + bom3.save() + bom3.submit() + + return [bom1, bom2, bom3] + + +def get_expected_data(bom, qty_to_make): + expected_data = [] + + for idx in range(len(bom.items)): + expected_data.append( + [ + bom.items[idx].item_code, + bom.items[idx].item_code, + "", + "", + float(bom.items[idx].stock_qty / bom.quantity), + float(100 * (idx + 1)), + float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)), + float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))), + float(100 * (idx + 1)), + ] + ) + + return expected_data diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index b10e6434223..ce8f4f35a3f 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -5,6 +5,7 @@ from typing import Dict, List, Tuple import frappe from frappe import _ +from frappe.query_builder.functions import Sum Filters = frappe._dict Row = frappe._dict @@ -14,15 +15,50 @@ QueryArgs = Dict[str, str] def execute(filters: Filters) -> Tuple[Columns, Data]: + filters = frappe._dict(filters or {}) columns = get_columns() data = get_data(filters) return columns, data def get_data(filters: Filters) -> Data: - query_args = get_query_args(filters) - data = run_query(query_args) + wo = frappe.qb.DocType("Work Order") + se = frappe.qb.DocType("Stock Entry") + + query = ( + frappe.qb.from_(wo) + .inner_join(se) + .on(wo.name == se.work_order) + .select( + wo.name, + wo.status, + wo.production_item, + wo.qty, + wo.produced_qty, + wo.process_loss_qty, + (wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"), + Sum(se.total_incoming_value).as_("total_fg_value"), + Sum(se.total_outgoing_value).as_("total_rm_value"), + ) + .where( + (wo.process_loss_qty > 0) + & (wo.company == filters.company) + & (se.docstatus == 1) + & (se.posting_date.between(filters.from_date, filters.to_date)) + ) + .groupby(se.work_order) + ) + + if "item" in filters: + query.where(wo.production_item == filters.item) + + if "work_order" in filters: + query.where(wo.name == filters.work_order) + + data = query.run(as_dict=True) + update_data_with_total_pl_value(data) + return data @@ -67,54 +103,7 @@ def get_columns() -> Columns: ] -def get_query_args(filters: Filters) -> QueryArgs: - query_args = {} - query_args.update(filters) - query_args.update(get_filter_conditions(filters)) - return query_args - - -def run_query(query_args: QueryArgs) -> Data: - return frappe.db.sql( - """ - SELECT - wo.name, wo.status, wo.production_item, wo.qty, - wo.produced_qty, wo.process_loss_qty, - (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty, - sum(se.total_incoming_value) as total_fg_value, - sum(se.total_outgoing_value) as total_rm_value - FROM - `tabWork Order` wo INNER JOIN `tabStock Entry` se - ON wo.name=se.work_order - WHERE - process_loss_qty > 0 - AND wo.company = %(company)s - AND se.docstatus = 1 - AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s - {item_filter} - {work_order_filter} - GROUP BY - se.work_order - """.format( - **query_args - ), - query_args, - as_dict=1, - ) - - def update_data_with_total_pl_value(data: Data) -> None: for row in data: value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"] row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg - - -def get_filter_conditions(filters: Filters) -> QueryArgs: - filter_conditions = dict(item_filter="", work_order_filter="") - if "item" in filters: - production_item = filters.get("item") - filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"}) - if "work_order" in filters: - work_order_name = filters.get("work_order") - filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"}) - return filter_conditions diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py index 063ebba0597..998b0e4bccd 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import cint @@ -17,70 +18,70 @@ def execute(filters=None): def get_item_list(wo_list, filters): out = [] - # Add a row for each item/qty - for wo_details in wo_list: - desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") + if wo_list: + bin = frappe.qb.DocType("Bin") + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") - for wo_item_details in frappe.db.get_values( - "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1 - ): + # Add a row for each item/qty + for wo_details in wo_list: + desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") - item_list = frappe.db.sql( - """SELECT - bom_item.item_code as item_code, - ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty - FROM - `tabBOM` as bom, `tabBOM Item` AS bom_item - LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code - AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s) - WHERE - bom.name = bom_item.parent - and bom_item.item_code = %(item_code)s - and bom.name = %(bom)s - GROUP BY - bom_item.item_code""", - { - "bom": wo_details.bom_no, - "warehouse": wo_item_details.source_warehouse, - "filterhouse": filters.warehouse, - "item_code": wo_item_details.item_code, - }, - as_dict=1, - ) + for wo_item_details in frappe.db.get_values( + "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1 + ): + item_list = ( + frappe.qb.from_(bom) + .from_(bom_item) + .left_join(bin) + .on( + (bom_item.item_code == bin.item_code) + & (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse)) + ) + .select( + bom_item.item_code.as_("item_code"), + IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"), + ) + .where( + (bom.name == bom_item.parent) + & (bom_item.item_code == wo_item_details.item_code) + & (bom.name == wo_details.bom_no) + ) + .groupby(bom_item.item_code) + ).run(as_dict=1) - stock_qty = 0 - count = 0 - buildable_qty = wo_details.qty - for item in item_list: - count = count + 1 - if item.build_qty >= (wo_details.qty - wo_details.produced_qty): - stock_qty = stock_qty + 1 - elif buildable_qty >= item.build_qty: - buildable_qty = item.build_qty + stock_qty = 0 + count = 0 + buildable_qty = wo_details.qty + for item in item_list: + count = count + 1 + if item.build_qty >= (wo_details.qty - wo_details.produced_qty): + stock_qty = stock_qty + 1 + elif buildable_qty >= item.build_qty: + buildable_qty = item.build_qty - if count == stock_qty: - build = "Y" - else: - build = "N" + if count == stock_qty: + build = "Y" + else: + build = "N" - row = frappe._dict( - { - "work_order": wo_details.name, - "status": wo_details.status, - "req_items": cint(count), - "instock": stock_qty, - "description": desc, - "source_warehouse": wo_item_details.source_warehouse, - "item_code": wo_item_details.item_code, - "bom_no": wo_details.bom_no, - "qty": wo_details.qty, - "buildable_qty": buildable_qty, - "ready_to_build": build, - } - ) + row = frappe._dict( + { + "work_order": wo_details.name, + "status": wo_details.status, + "req_items": cint(count), + "instock": stock_qty, + "description": desc, + "source_warehouse": wo_item_details.source_warehouse, + "item_code": wo_item_details.item_code, + "bom_no": wo_details.bom_no, + "qty": wo_details.qty, + "buildable_qty": buildable_qty, + "ready_to_build": build, + } + ) - out.append(row) + out.append(row) return out diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f48b2a1bb0a..2a0ca8c4961 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -313,4 +313,5 @@ erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes +erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/fix_subcontracting_receipt_gl_entries.py b/erpnext/patches/v14_0/fix_subcontracting_receipt_gl_entries.py new file mode 100644 index 00000000000..159c6dc82d9 --- /dev/null +++ b/erpnext/patches/v14_0/fix_subcontracting_receipt_gl_entries.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import ( + get_data, +) + + +def execute(): + data = [] + + for company in frappe.db.get_list("Company", pluck="name"): + data += get_data( + frappe._dict( + { + "company": company, + } + ) + ) + + if data: + for d in data: + if d and d.get("voucher_type") == "Subcontracting Receipt": + doc = frappe.new_doc("Repost Item Valuation") + doc.voucher_type = d.get("voucher_type") + doc.voucher_no = d.get("voucher_no") + doc.save() + doc.submit() diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index a2be9367688..d80133c988a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -10,7 +10,6 @@ from frappe.model.document import Document from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today from erpnext import get_default_company -from erpnext.controllers.employee_boarding_controller import update_employee_boarding_status from erpnext.controllers.queries import get_filters_cond from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday @@ -43,7 +42,6 @@ class Project(Document): self.send_welcome_email() self.update_costing() self.update_percent_complete() - update_employee_boarding_status(self) def copy_from_template(self): """ @@ -145,7 +143,6 @@ class Project(Document): def update_project(self): """Called externally by Task""" self.update_percent_complete() - update_employee_boarding_status(self) self.update_costing() self.db_update() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f2bea5829c2..6d64625270b 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -226,7 +226,7 @@ $.extend(erpnext.utils, { if (!found) { filters.splice(index, 0, { "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), + "label": __(dimension["doctype"]), "fieldtype": "MultiSelectList", get_data: function(txt) { return frappe.db.get_link_options(dimension["doctype"], txt); diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index b47adc95f72..cac5ec113e8 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -84,7 +84,7 @@ def create_qr_code(doc, method=None): tlv_array.append("".join([tag, length, value])) # Invoice Amount - invoice_amount = str(doc.grand_total) + invoice_amount = str(doc.base_grand_total) tag = bytes([4]).hex() length = bytes([len(invoice_amount)]).hex() value = invoice_amount.encode("utf-8").hex() @@ -144,7 +144,7 @@ def get_vat_amount(doc): for tax in doc.get("taxes"): if tax.account_head in vat_accounts: - vat_amount += tax.tax_amount + vat_amount += tax.base_tax_amount return vat_amount diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6b6ea89b638..386c12b6386 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -59,7 +59,36 @@ frappe.ui.form.on("Sales Order", { }) }); } + + if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) { + frm.events.get_items_from_internal_purchase_order(frm); + } }, + + get_items_from_internal_purchase_order(frm) { + frm.add_custom_button(__('Purchase Order'), () => { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order', + source_doctype: 'Purchase Order', + target: frm, + setters: [ + { + label: 'Supplier', + fieldname: 'supplier', + fieldtype: 'Link', + options: 'Supplier' + } + ], + get_query_filters: { + company: frm.doc.company, + is_internal_supplier: 1, + docstatus: 1, + status: ['!=', 'Completed'] + } + }); + }, __('Get Items From')); + }, + onload: function(frm) { if (!frm.doc.transaction_date){ frm.set_value('transaction_date', frappe.datetime.get_today()) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 318799907ed..2cf836f9fcc 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -92,7 +92,11 @@ "section_break_63", "page_break", "item_tax_rate", - "transaction_date" + "transaction_date", + "inter_transfer_reference_section", + "purchase_order", + "column_break_89", + "purchase_order_item" ], "fields": [ { @@ -809,12 +813,36 @@ "label": "Picked Qty (in Stock UOM)", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "inter_transfer_reference_section", + "fieldtype": "Section Break", + "label": "Inter Transfer Reference" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_89", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:27:41.603006", + "modified": "2022-09-06 13:24:18.065312", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 7a806d5906f..39e0acd02aa 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -10,79 +10,89 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "basic_details_tab", "basic_information", "employee", "naming_series", "first_name", "middle_name", "last_name", - "salutation", "employee_name", - "image", - "column_break1", - "company", - "status", + "column_break_9", "gender", "date_of_birth", + "salutation", + "column_break1", "date_of_joining", - "employee_number", - "emergency_contact_details", - "person_to_be_contacted", - "relation", - "column_break_19", - "emergency_phone_number", + "image", + "status", "erpnext_user", "user_id", "create_user", "create_user_permission", - "employment_details", - "scheduled_confirmation_date", - "final_confirmation_date", - "col_break_22", - "contract_end_date", - "notice_number_of_days", - "date_of_retirement", - "job_profile", + "company_details_section", + "company", "department", + "employee_number", + "column_break_25", "designation", "reports_to", - "column_break_31", + "column_break_18", "branch", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", + "contact_details", + "cell_number", + "column_break_40", + "personal_email", + "company_email", + "column_break4", + "prefered_contact_email", + "prefered_email", + "unsubscribed", + "address_section", + "current_address", + "current_accommodation_type", + "column_break_46", + "permanent_address", + "permanent_accommodation_type", + "emergency_contact_details", + "person_to_be_contacted", + "column_break_55", + "emergency_phone_number", + "column_break_19", + "relation", "attendance_and_leave_details", "attendance_device_id", "column_break_44", "holiday_list", "salary_information", - "salary_currency", "ctc", - "payroll_cost_center", - "column_break_52", + "salary_currency", + "salary_mode", + "bank_details_section", "bank_name", "bank_ac_no", - "contact_details", - "cell_number", - "prefered_email", - "personal_email", - "unsubscribed", - "permanent_accommodation_type", - "permanent_address", - "column_break4", - "prefered_contact_email", - "company_email", - "current_accommodation_type", - "current_address", - "sb53", - "bio", "personal_details", - "passport_number", - "date_of_issue", - "valid_upto", - "place_of_issue", "marital_status", - "blood_group", - "column_break6", "family_background", + "column_break6", + "blood_group", "health_details", + "passport_details_section", + "passport_number", + "valid_upto", + "column_break_73", + "date_of_issue", + "place_of_issue", + "profile_tab", + "bio", "educational_qualification", "education", "previous_work_experience", @@ -92,16 +102,20 @@ "exit", "resignation_letter_date", "relieving_date", - "reason_for_leaving", - "leave_encashed", - "encashment_date", "exit_interview_details", "held_on", "new_workplace", + "column_break_99", + "leave_encashed", + "encashment_date", + "feedback_section", + "reason_for_leaving", + "column_break_104", "feedback", "lft", "rgt", - "old_parent" + "old_parent", + "connections_tab" ], "fields": [ { @@ -261,7 +275,7 @@ "collapsible": 1, "fieldname": "erpnext_user", "fieldtype": "Section Break", - "label": "ERPNext User" + "label": "User Details" }, { "description": "System User (login) ID. If set, it will become default for all HR forms.", @@ -289,8 +303,8 @@ "allow_in_quick_entry": 1, "collapsible": 1, "fieldname": "employment_details", - "fieldtype": "Section Break", - "label": "Joining Details" + "fieldtype": "Tab Break", + "label": "Joining" }, { "fieldname": "scheduled_confirmation_date", @@ -331,12 +345,6 @@ "oldfieldname": "date_of_retirement", "oldfieldtype": "Date" }, - { - "collapsible": 1, - "fieldname": "job_profile", - "fieldtype": "Section Break", - "label": "Department" - }, { "fieldname": "department", "fieldtype": "Link", @@ -366,10 +374,6 @@ "oldfieldtype": "Link", "options": "Employee" }, - { - "fieldname": "column_break_31", - "fieldtype": "Column Break" - }, { "fieldname": "branch", "fieldtype": "Link", @@ -391,7 +395,7 @@ { "collapsible": 1, "fieldname": "salary_information", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Salary Details", "oldfieldtype": "Section Break", "width": "50%" @@ -423,8 +427,8 @@ { "collapsible": 1, "fieldname": "contact_details", - "fieldtype": "Section Break", - "label": "Contact Details" + "fieldtype": "Tab Break", + "label": "Contact" }, { "fieldname": "cell_number", @@ -493,12 +497,6 @@ "fieldtype": "Small Text", "label": "Current Address" }, - { - "collapsible": 1, - "fieldname": "sb53", - "fieldtype": "Section Break", - "label": "Personal Bio" - }, { "description": "Short biography for website and other publications.", "fieldname": "bio", @@ -508,7 +506,7 @@ { "collapsible": 1, "fieldname": "personal_details", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Personal Details" }, { @@ -601,7 +599,7 @@ { "collapsible": 1, "fieldname": "exit", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Exit", "oldfieldtype": "Section Break" }, @@ -702,7 +700,7 @@ { "collapsible": 1, "fieldname": "attendance_and_leave_details", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Attendance and Leave Details" }, { @@ -713,10 +711,6 @@ "fieldname": "column_break_19", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_52", - "fieldtype": "Column Break" - }, { "fieldname": "salary_currency", "fieldtype": "Link", @@ -728,13 +722,95 @@ "fieldtype": "Currency", "label": "Cost to Company (CTC)", "options": "salary_currency" + }, + { + "fieldname": "basic_details_tab", + "fieldtype": "Tab Break", + "label": "Basic Details" + }, + { + "fieldname": "company_details_section", + "fieldtype": "Section Break", + "label": "Company Details" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "address_section", + "fieldtype": "Section Break", + "label": "Address" + }, + { + "fieldname": "column_break_46", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_tab", + "fieldtype": "Tab Break", + "label": "Profile" + }, + { + "fieldname": "passport_details_section", + "fieldtype": "Section Break", + "label": "Passport Details" + }, + { + "fieldname": "column_break_73", + "fieldtype": "Column Break" + }, + { + "fieldname": "bank_details_section", + "fieldtype": "Section Break", + "label": "Bank Details" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_32", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_99", + "fieldtype": "Column Break" + }, + { + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "fieldname": "column_break_104", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2022-06-27 01:29:32.952091", + "modified": "2022-08-23 13:47:46.944993", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0e68e858065..36d5a6ce0e1 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -178,6 +178,7 @@ class DeliveryNote(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [ @@ -896,6 +897,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "name": "delivery_note_item", "batch_no": "batch_no", "serial_no": "serial_no", + "purchase_order": "purchase_order", + "purchase_order_item": "purchase_order_item", }, "field_no_map": ["warehouse"], }, diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 2de4842ebea..0911cdb476c 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -86,6 +86,10 @@ "expense_account", "allow_zero_valuation_rate", "column_break_71", + "internal_transfer_section", + "purchase_order", + "column_break_82", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -777,13 +781,39 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_82", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:25:47.711177", + "modified": "2022-09-06 14:19:42.876357", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 07cb73b1d56..79e7895f6d0 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -30,6 +30,7 @@ frappe.ui.form.on('Inventory Dimension', { onload(frm) { frm.trigger('render_traget_field'); + frm.trigger("set_parent_fields"); }, refresh(frm) { @@ -52,6 +53,30 @@ frappe.ui.form.on('Inventory Dimension', { } }, + document_type(frm) { + frm.trigger("set_parent_fields"); + }, + + set_parent_fields(frm) { + if (frm.doc.apply_to_all_doctypes) { + frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document); + } else if (frm.doc.document_type && frm.doc.istable) { + frappe.call({ + method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields', + args: { + child_doctype: frm.doc.document_type, + dimension_name: frm.doc.reference_document + }, + callback: (r) => { + if (r.message && r.message.length) { + frm.set_df_property("fetch_from_parent", "options", + [""].concat(r.message)); + } + } + }); + } + }, + delete_dimension(frm) { let msg = (` Custom fields related to this dimension will be deleted on deletion of dimension. diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 03e7fda8411..09f4f63031c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -144,16 +144,15 @@ "fieldtype": "Column Break" }, { - "depends_on": "istable", "description": "Set fieldname or DocType name like Supplier, Customer etc.", "fieldname": "fetch_from_parent", - "fieldtype": "Data", + "fieldtype": "Select", "label": "Fetch Value From Parent Form" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-17 11:43:24.722441", + "modified": "2022-09-02 13:29:04.098469", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 4ff8f33b409..9e8c10b394d 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -236,3 +236,30 @@ def get_inventory_dimensions(): def delete_dimension(dimension): doc = frappe.get_doc("Inventory Dimension", dimension) doc.delete() + + +@frappe.whitelist() +def get_parent_fields(child_doctype, dimension_name): + parent_doctypes = frappe.get_all( + "DocField", fields=["parent"], filters={"options": child_doctype} + ) + + fields = [] + + fields.extend( + frappe.get_all( + "DocField", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "parent": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + fields.extend( + frappe.get_all( + "Custom Field", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "dt": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + return fields diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index cc90b74ee85..19ddc449f0e 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -2,14 +2,17 @@ # See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.tests.utils import FrappeTestCase +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( CanNotBeChildDoc, CanNotBeDefaultDimension, DoNotChangeError, delete_dimension, ) +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -136,6 +139,58 @@ class TestInventoryDimension(FrappeTestCase): self.assertTrue(inv_dim1.has_stock_ledger()) self.assertRaises(DoNotChangeError, inv_dim1.save) + def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + create_inventory_dimension( + reference_document="Rack", + type_of_transaction="Both", + dimension_name="Rack", + apply_to_all_doctypes=1, + fetch_from_parent="Rack", + ) + + create_custom_field( + "Purchase Receipt", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + create_custom_field( + "Delivery Note", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + frappe.reload_doc("stock", "doctype", "purchase_receipt_item") + frappe.reload_doc("stock", "doctype", "delivery_note_item") + + pr_doc = make_purchase_receipt(qty=2, do_not_submit=True) + pr_doc.rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc.load_from_db() + + self.assertEqual(pr_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": pr_doc.items[0].name, "voucher_type": pr_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + + dn_doc = create_delivery_note(qty=2, do_not_submit=True) + dn_doc.rack = "Rack 1" + dn_doc.save() + dn_doc.submit() + + dn_doc.load_from_db() + + self.assertEqual(dn_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": dn_doc.items[0].name, "voucher_type": dn_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"): @@ -160,6 +215,28 @@ def prepare_test_data(): create_warehouse("Shelf Warehouse") + if not frappe.db.exists("DocType", "Rack"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Rack", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:rack_name", + "fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for rack in ["Rack 1"]: + if not frappe.db.exists("Rack", rack): + frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) + + create_warehouse("Rack Warehouse") + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 76cb31dc42e..247140b657e 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -795,7 +795,7 @@ }, { "fieldname": "customer_code", - "fieldtype": "Data", + "fieldtype": "Small Text", "hidden": 1, "label": "Customer Code", "no_copy": 1, @@ -910,7 +910,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-15 09:02:06.177691", + "modified": "2022-09-12 15:00:10.130340", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 3366c737cbd..89da72195fc 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -778,6 +778,14 @@ class TestItem(FrappeTestCase): item.has_batch_no = 1 item.save() + def test_customer_codes_length(self): + """Check if item code with special characters are allowed.""" + item = make_item(properties={"item_code": "Test Item Code With Special Characters"}) + for row in range(3): + item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)}) + item.save() + self.assertTrue(len(item.customer_code) > 140) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index 56832f32d30..bda1218817c 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -17,6 +17,7 @@ "in_list_view": 1, "label": "Barcode", "no_copy": 1, + "reqd": 1, "unique": 1 }, { @@ -36,7 +37,7 @@ ], "istable": 1, "links": [], - "modified": "2022-06-01 06:24:33.969534", + "modified": "2022-08-24 19:59:47.871677", "modified_by": "Administrator", "module": "Stock", "name": "Item Barcode", diff --git a/erpnext/stock/doctype/item_supplier/item_supplier.json b/erpnext/stock/doctype/item_supplier/item_supplier.json index 6cff8e0892e..84649a67d00 100644 --- a/erpnext/stock/doctype/item_supplier/item_supplier.json +++ b/erpnext/stock/doctype/item_supplier/item_supplier.json @@ -1,95 +1,43 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:28:01", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2013-02-22 01:28:01", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "supplier", + "supplier_part_no" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_part_no", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Supplier Part Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "supplier_part_no", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Supplier Part Number", + "print_width": "200px", "width": "200px" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-02-20 13:29:32.569715", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Supplier", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2022-09-07 12:33:55.780062", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Supplier", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index d31d695c803..adddb413824 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -183,7 +183,7 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item - key = (item_code, item.uom, reference) + key = (item_code, item.uom, item.warehouse, reference) item.idx = None item.name = None diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 84da3cc41d5..f85c478a72f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -362,6 +362,12 @@ class PurchaseReceipt(BuyingController): if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) ) + + outgoing_amount = d.base_net_amount + if self.is_internal_supplier and d.valuation_rate: + outgoing_amount = d.valuation_rate * d.stock_qty + credit_amount = outgoing_amount + if credit_amount: account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb @@ -369,7 +375,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=account, cost_center=d.cost_center, - debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")), + debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")), credit=0.0, remarks=remarks, against_account=warehouse_account_name, @@ -456,7 +462,7 @@ class PurchaseReceipt(BuyingController): # divisional loss adjustment valuation_amount_as_per_doc = ( - flt(d.base_net_amount, d.precision("base_net_amount")) + flt(outgoing_amount, d.precision("base_net_amount")) + flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) @@ -631,47 +637,6 @@ class PurchaseReceipt(BuyingController): i += 1 - def add_gl_entry( - self, - gl_entries, - account, - cost_center, - debit, - credit, - remarks, - against_account, - debit_in_account_currency=None, - credit_in_account_currency=None, - account_currency=None, - project=None, - voucher_detail_no=None, - item=None, - posting_date=None, - ): - - gl_entry = { - "account": account, - "cost_center": cost_center, - "debit": debit, - "credit": credit, - "against": against_account, - "remarks": remarks, - } - - if voucher_detail_no: - gl_entry.update({"voucher_detail_no": voucher_detail_no}) - - if debit_in_account_currency: - gl_entry.update({"debit_in_account_currency": debit_in_account_currency}) - - if credit_in_account_currency: - gl_entry.update({"credit_in_account_currency": credit_in_account_currency}) - - if posting_date: - gl_entry.update({"posting_date": posting_date}) - - gl_entries.append(self.get_gl_dict(gl_entry, item=item)) - def get_asset_gl_entry(self, gl_entries): for item in self.get("items"): if item.is_fixed_asset: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d0d115d96a1..b77c3a51348 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5,6 +5,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today +from pypika import functions as fn import erpnext from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1156,6 +1157,125 @@ class TestPurchaseReceipt(FrappeTestCase): if gle.account == account: self.assertEqual(gle.credit, 50) + def test_backdated_transaction_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + item_doc = create_item("Test Internal Transfer Item") + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=1, + rate=100, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + self.assertEqual(dn1.items[0].rate, 100) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].warehouse = to_warehouse + self.assertEqual(pr1.items[0].rate, 100) + pr1.submit() + + # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1 + make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -2), + warehouse=from_warehouse, + qty=1, + rate=200, + ) + + dn_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(dn_value), 200.00) + + pr_value = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse}, + "stock_value_difference", + ) + + self.assertEqual(abs(pr_value), 200.00) + pr1.load_from_db() + + self.assertEqual(pr1.items[0].valuation_rate, 200) + self.assertEqual(pr1.items[0].rate, 100) + + Gl = frappe.qb.DocType("GL Entry") + + query = ( + frappe.qb.from_(Gl) + .select( + (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"), + ) + .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name)) + ).run(as_dict=True) + + self.assertEqual(query[0].value, 0) + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql( diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index eae73050b23..d595a80b20a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -58,6 +58,21 @@ frappe.ui.form.on('Repost Item Valuation', { } frm.trigger('show_reposting_progress'); + + if (frm.doc.status === 'Queued' && frm.doc.docstatus === 1) { + frm.trigger('execute_reposting'); + } + }, + + execute_reposting(frm) { + frm.add_custom_button(__("Start Reposting"), () => { + frappe.call({ + method: 'erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_repost_item_valuation', + callback: function() { + frappe.msgprint(__('Reposting has been started in the background.')); + } + }); + }); }, show_reposting_progress: function(frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 7c57ecd73e6..c4705246b3c 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -307,3 +307,9 @@ def in_configured_timeslot(repost_settings=None, current_time=None): return end_time >= now_time >= start_time else: return now_time >= start_time or now_time <= end_time + + +@frappe.whitelist() +def execute_repost_item_valuation(): + """Execute repost item valuation via scheduler.""" + frappe.get_doc("Scheduled Job Type", "repost_item_valuation.repost_entries").enqueue(force=True) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1bbe5708071..a952a93ac72 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -815,7 +815,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return { "filters": { "docstatus": 1, - "company": me.frm.doc.company + "company": me.frm.doc.company, + "status": ["not in", ["Completed", "Closed"]] } }; }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d70952282d6..76bba8af646 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -117,6 +117,7 @@ class StockEntry(StockController): self.validate_work_order() self.validate_bom() self.validate_purchase_order() + self.validate_subcontracting_order() if self.purpose in ("Manufacture", "Repack"): self.mark_finished_and_scrap_items() @@ -875,25 +876,24 @@ class StockEntry(StockController): ) ) - parent = frappe.qb.DocType("Stock Entry") - child = frappe.qb.DocType("Stock Entry Detail") - - conditions = ( - (parent.docstatus == 1) - & (child.item_code == se_item.item_code) - & ( - (parent.purchase_order == self.purchase_order) - if self.subcontract_data.order_doctype == "Purchase Order" - else (parent.subcontracting_order == self.subcontracting_order) - ) - ) + se = frappe.qb.DocType("Stock Entry") + se_detail = frappe.qb.DocType("Stock Entry Detail") total_supplied = ( - frappe.qb.from_(parent) - .inner_join(child) - .on(parent.name == child.parent) - .select(Sum(child.transfer_qty)) - .where(conditions) + frappe.qb.from_(se) + .inner_join(se_detail) + .on(se.name == se_detail.parent) + .select(Sum(se_detail.transfer_qty)) + .where( + (se.purpose == "Send to Subcontractor") + & (se.docstatus == 1) + & (se_detail.item_code == se_item.item_code) + & ( + (se.purchase_order == self.purchase_order) + if self.subcontract_data.order_doctype == "Purchase Order" + else (se.subcontracting_order == self.subcontracting_order) + ) + ) ).run()[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): @@ -960,6 +960,20 @@ class StockEntry(StockController): ) ) + def validate_subcontracting_order(self): + if self.get("subcontracting_order") and self.purpose in [ + "Send to Subcontractor", + "Material Transfer", + ]: + sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status") + + if sco_status == "Closed": + frappe.throw( + _("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format( + self.subcontracting_order + ) + ) + def mark_finished_and_scrap_items(self): if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): return diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3524a47c713..50309647de0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -649,21 +649,25 @@ class update_entries_after(object): elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] - and sle.actual_qty > 0 + and sle.voucher_detail_no and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") ): - sle_details = frappe.db.get_value( - "Stock Ledger Entry", - { - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no, - "dependant_sle_voucher_detail_no": sle.voucher_detail_no, - }, - ["stock_value_difference", "actual_qty"], - as_dict=1, + field = ( + "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item" + ) + doctype = ( + "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item" + ) + refernce_name = frappe.get_cached_value( + sle.voucher_type + " Item", sle.voucher_detail_no, field ) - rate = abs(sle_details.stock_value_difference / sle.actual_qty) + if refernce_name: + rate = frappe.get_cached_value( + doctype, + refernce_name, + "incoming_rate", + ) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" @@ -745,7 +749,12 @@ class update_entries_after(object): def update_rate_on_purchase_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): frappe.db.set_value( - sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate + sle.voucher_type + " Item", + sle.voucher_detail_no, + { + "base_net_rate": outgoing_rate, + "valuation_rate": outgoing_rate, + }, ) else: frappe.db.set_value( diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 065ef39db3e..40963f86373 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', { get_materials_from_supplier: function (frm) { let sco_rm_details = []; - if (frm.doc.supplied_items && frm.doc.per_received > 0) { + if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) { frm.doc.supplied_items.forEach(d => { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) { sco_rm_details.push(d.name); diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 098242aed89..d054ce0f9d4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -187,22 +187,13 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(len(ste.items), len(rm_items)) def test_update_reserved_qty_for_subcontracting(self): - # Make stock available for raw materials - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + # Create RM Material Receipt + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) make_stock_entry( target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=30, - basic_rate=100, - ) - bin1 = frappe.db.get_value( + bin_before_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], @@ -222,102 +213,97 @@ class TestSubcontractingOrder(FrappeTestCase): ] sco = get_subcontracting_order(service_items=service_items) - bin2 = frappe.db.get_value( + bin_after_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1, ) - self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) - self.assertNotEqual(bin1.modified, bin2.modified) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + 10 + ) - # Create stock transfer + # projected_qty should be decreased by 10 + self.assertEqual(bin_after_sco.projected_qty, bin_before_sco.projected_qty - 10) + + self.assertNotEqual(bin_before_sco.modified, bin_after_sco.modified) + + # Create Stock Entry(Send to Subcontractor) rm_items = [ { "item_code": "_Test FG Item", "rm_item_code": "_Test Item", "item_name": "_Test Item", - "qty": 6, + "qty": 10, "warehouse": "_Test Warehouse - _TC", "rate": 100, - "amount": 600, + "amount": 1000, "stock_uom": "Nos", - } + }, + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 2000, + "stock_uom": "Nos", + }, ] ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) ste.to_warehouse = "_Test Warehouse 1 - _TC" ste.save() ste.submit() - bin3 = frappe.db.get_value( + bin_after_rm_transfer = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=40, - basic_rate=100, + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_rm_transfer.reserved_qty_for_sub_contract, + bin_after_sco.reserved_qty_for_sub_contract - 10, ) - # Make SCR against the SCO - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.submit() - - bin4 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel SCR - scr.reload() - scr.cancel() - bin5 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Cancel Stock Entry + # Cancel Stock Entry(Send to Subcontractor) ste.cancel() - bin6 = frappe.db.get_value( + bin_after_cancel_ste = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_cancel_ste.reserved_qty_for_sub_contract, + bin_after_rm_transfer.reserved_qty_for_sub_contract + 10, + ) - # Cancel PO + # Cancel SCO sco.reload() sco.cancel() - bin7 = frappe.db.get_value( + bin_after_cancel_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, + bin_after_cancel_ste.reserved_qty_for_sub_contract - 10, + ) + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + ) def test_exploded_items(self): item_code = "_Test Subcontracted FG Item 11" @@ -516,6 +502,35 @@ class TestSubcontractingOrder(FrappeTestCase): set_backflush_based_on("BOM") + def test_get_materials_from_supplier(self): + # Create SCO + sco = get_subcontracting_order() + + # Transfer RM + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create SCR (Partial) + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty -= 5 + scr.save() + scr.submit() + + # Get RM from Supplier + ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) + ste.save() + ste.submit() + + sco.load_from_db() + + self.assertEqual(sco.status, "Closed") + self.assertEqual(sco.supplied_items[0].returned_qty, 5) + def create_subcontracting_order(**args): args = frappe._dict(args) @@ -524,7 +539,7 @@ def create_subcontracting_order(**args): for item in sco.items: item.include_exploded_items = args.get("include_exploded_items", 1) - if args.get("warehouse"): + if args.warehouse: for item in sco.items: item.warehouse = args.warehouse else: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 021d9aa8547..cd05b745e6d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -5,6 +5,8 @@ import frappe from frappe import _ from frappe.utils import cint, flt, getdate, nowdate +import erpnext +from erpnext.accounts.utils import get_account_currency from erpnext.controllers.subcontracting_controller import SubcontractingController @@ -75,6 +77,7 @@ class SubcontractingReceipt(SubcontractingController): self.get_current_stock() def on_submit(self): + self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status() @@ -107,10 +110,42 @@ class SubcontractingReceipt(SubcontractingController): self.set_missing_values_in_supplied_items() self.set_missing_values_in_items() + def set_available_qty_for_consumption(self): + supplied_items_details = {} + + sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + for item in self.get("items"): + supplied_items = ( + frappe.qb.from_(sco_supplied_item) + .select( + sco_supplied_item.rm_item_code, + sco_supplied_item.reference_name, + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + ) + .where( + (sco_supplied_item.parent == item.subcontracting_order) + & (sco_supplied_item.main_item_code == item.item_code) + & (sco_supplied_item.reference_name == item.subcontracting_order_item) + ) + ).run(as_dict=True) + + if supplied_items: + supplied_items_details[item.name] = {} + + for supplied_item in supplied_items: + supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + else: + for item in self.get("supplied_items"): + item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( + item.rm_item_code, 0 + ) + def set_missing_values_in_supplied_items(self): for item in self.get("supplied_items") or []: item.amount = item.rate * item.consumed_qty + self.set_available_qty_for_consumption() + def set_missing_values_in_items(self): rm_supp_cost = {} for item in self.get("supplied_items") or []: @@ -147,6 +182,17 @@ class SubcontractingReceipt(SubcontractingController): _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code) ) + def validate_available_qty_for_consumption(self): + for item in self.get("supplied_items"): + if ( + item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty + ): + frappe.throw( + _( + "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." + ).format(item.idx) + ) + def set_items_cost_center(self): if self.company: cost_center = frappe.get_cached_value("Company", self.company, "cost_center") @@ -181,6 +227,137 @@ class SubcontractingReceipt(SubcontractingController): if status: frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified) + def get_gl_entries(self, warehouse_account=None): + from erpnext.accounts.general_ledger import process_gl_map + + gl_entries = [] + self.make_item_gl_entries(gl_entries, warehouse_account) + + return process_gl_map(gl_entries) + + def make_item_gl_entries(self, gl_entries, warehouse_account=None): + if erpnext.is_perpetual_inventory_enabled(self.company): + stock_rbnb = self.get_company_default("stock_received_but_not_billed") + expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + + warehouse_with_no_account = [] + + for item in self.items: + if flt(item.rate) and flt(item.qty): + if warehouse_account.get(item.warehouse): + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Subcontracting Receipt", + "voucher_no": self.name, + "voucher_detail_no": item.name, + "warehouse": item.warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) + + warehouse_account_name = warehouse_account[item.warehouse]["account"] + warehouse_account_currency = warehouse_account[item.warehouse]["account_currency"] + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) + remarks = self.get("remarks") or _("Accounting Entry for Stock") + + # FG Warehouse Account (Debit) + self.add_gl_entry( + gl_entries=gl_entries, + account=warehouse_account_name, + cost_center=item.cost_center, + debit=stock_value_diff, + credit=0.0, + remarks=remarks, + against_account=stock_rbnb, + account_currency=warehouse_account_currency, + item=item, + ) + + # Supplier Warehouse Account (Credit) + if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): + self.add_gl_entry( + gl_entries=gl_entries, + account=supplier_warehouse_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.rm_supp_cost), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=supplier_warehouse_account_currency, + item=item, + ) + + # Expense Account (Credit) + if flt(item.service_cost_per_qty): + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=flt(item.service_cost_per_qty) * flt(item.qty), + remarks=remarks, + against_account=warehouse_account_name, + account_currency=get_account_currency(item.expense_account), + item=item, + ) + + # Loss Account (Credit) + divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) + + if divisional_loss: + if self.is_return: + loss_account = expenses_included_in_valuation + else: + loss_account = item.expense_account + + self.add_gl_entry( + gl_entries=gl_entries, + account=loss_account, + cost_center=item.cost_center, + debit=divisional_loss, + credit=0.0, + remarks=remarks, + against_account=warehouse_account_name, + account_currency=get_account_currency(loss_account), + project=item.project, + item=item, + ) + elif ( + item.warehouse not in warehouse_with_no_account + or item.rejected_warehouse not in warehouse_with_no_account + ): + warehouse_with_no_account.append(item.warehouse) + + # Additional Costs Expense Accounts (Credit) + for row in self.additional_costs: + credit_amount = ( + flt(row.base_amount) + if (row.base_amount or row.account_currency != self.company_currency) + else flt(row.amount) + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=row.expense_account, + cost_center=self.cost_center or self.get_company_default("cost_center"), + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=None, + ) + + if warehouse_with_no_account: + frappe.msgprint( + _("No accounting entries for the following warehouses") + + ": \n" + + "\n".join(warehouse_with_no_account) + ) + @frappe.whitelist() def make_subcontract_return(source_name, target_doc=None): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 763e76882e0..090f1457d95 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -6,8 +6,10 @@ import copy import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import flt +from frappe.utils import cint, flt +import erpnext +from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.tests.test_subcontracting_controller import ( get_rm_items, @@ -22,6 +24,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, ) from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( make_subcontracting_receipt, @@ -70,6 +73,55 @@ class TestSubcontractingReceipt(FrappeTestCase): rm_supp_cost = sum(item.amount for item in scr.get("supplied_items")) self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) + def test_available_qty_for_consumption(self): + make_stock_entry( + item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, + ) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item", + "qty": 5.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item Home Desktop 100", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + self.assertRaises(frappe.ValidationError, scr.submit) + def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -317,6 +369,103 @@ class TestSubcontractingReceipt(FrappeTestCase): args = frappe._dict(scr_name=scr1.name, qty=-15) self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args) + def test_subcontracting_receipt_no_gl_entry(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + scr.append( + "additional_costs", + { + "expense_account": "Expenses Included In Valuation - _TC", + "description": "Test Additional Costs", + "amount": 100, + }, + ) + scr.save() + scr.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Subcontracting Receipt", + "voucher_no": scr.name, + "item_code": "Subcontracted Item SA7", + "warehouse": "_Test Warehouse - _TC", + }, + "stock_value_difference", + ) + + # Service Cost(100 * 10) + Raw Materials Cost(50 * 10) + Additional Costs(100) = 1600 + self.assertEqual(stock_value_difference, 1600) + self.assertFalse(get_gl_entries("Subcontracting Receipt", scr.name)) + + def test_subcontracting_receipt_gl_entry(self): + sco = get_subcontracting_order( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + additional_costs_expense_account = "Expenses Included In Valuation - TCP1" + scr.append( + "additional_costs", + { + "expense_account": additional_costs_expense_account, + "description": "Test Additional Costs", + "amount": 100, + "base_amount": 100, + }, + ) + scr.save() + scr.submit() + + self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(scr.company)), 1) + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + + self.assertTrue(gl_entries) + + fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse) + supplier_warehouse_ac = get_inventory_account(scr.company, scr.supplier_warehouse) + expense_account = scr.items[0].expense_account + + if fg_warehouse_ac == supplier_warehouse_ac: + expected_values = { + fg_warehouse_ac: [2100.0, 1000.0], # FG Amount (D), RM Cost (C) + expense_account: [0.0, 1000.0], # Service Cost (C) + additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) + } + else: + expected_values = { + fg_warehouse_ac: [2100.0, 0.0], # FG Amount (D) + supplier_warehouse_ac: [0.0, 1000.0], # RM Cost (C) + expense_account: [0.0, 1000.0], # Service Cost (C) + additional_costs_expense_account: [0.0, 100.0], # Additional Cost (C) + } + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.debit) + self.assertEqual(expected_values[gle.account][1], gle.credit) + + scr.reload() + scr.cancel() + self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 100a8060e8c..ddbb80661ad 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -19,6 +19,7 @@ "col_break2", "amount", "secbreak_2", + "available_qty_for_consumption", "required_qty", "col_break3", "consumed_qty", @@ -75,8 +76,7 @@ { "fieldname": "required_qty", "fieldtype": "Float", - "in_list_view": 1, - "label": "Available Qty For Consumption", + "label": "Required Qty", "print_hide": 1, "read_only": 1 }, @@ -85,7 +85,7 @@ "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Qty to be Consumed", + "label": "Consumed Qty", "reqd": 1 }, { @@ -179,12 +179,21 @@ "options": "Subcontracting Order", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "available_qty_for_consumption", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Available Qty For Consumption", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-04-18 10:45:16.538479", + "modified": "2022-09-02 22:28:53.392381", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", @@ -193,6 +202,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index cd1bf9f321b..21a0a551b62 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -71,6 +71,9 @@ class TransactionBase(StatusUpdater): self.validate_value(field, condition, prevdoc_values[field], doc) def validate_rate_with_reference_doc(self, ref_details): + if self.get("is_internal_supplier"): + return + buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] if self.doctype in buying_doctypes: diff --git a/license.txt b/license.txt index a238a97b060..f288702d2fa 100644 --- a/license.txt +++ b/license.txt @@ -1,205 +1,200 @@ -### GNU GENERAL PUBLIC LICENSE + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Version 3, 29 June 2007 + Copyright (C) 2007 Free Software Foundation, Inc.
|---|