Merge branch 'develop' into lead-notes-patch
This commit is contained in:
commit
51c37aeee3
32
.github/workflows/initiate_release.yml
vendored
Normal file
32
.github/workflows/initiate_release.yml
vendored
Normal file
@ -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 }}
|
@ -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).
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered" style="font-size: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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"]},
|
||||
],
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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)
|
@ -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,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -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.'
|
||||
|
@ -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.
|
||||
|
@ -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":
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"), {})
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -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()
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"],
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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"]]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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:
|
||||
|
713
license.txt
713
license.txt
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user