Merge branch 'develop' into lead-notes-patch

This commit is contained in:
Deepesh Garg 2022-09-13 09:42:07 +05:30 committed by GitHub
commit 51c37aeee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2160 additions and 1123 deletions

32
.github/workflows/initiate_release.yml vendored Normal file
View 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 }}

View File

@ -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).

View File

@ -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);

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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")

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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",

View File

@ -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
)

View File

@ -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)

View File

@ -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",

View File

@ -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"]},
],
}

View File

@ -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),

View File

@ -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",

View File

@ -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():

View File

@ -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:

View File

@ -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)

View File

@ -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,
}
)
)

View File

@ -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.'

View File

@ -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.

View File

@ -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":

View File

@ -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)

View File

@ -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,
}
]
}

View File

@ -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"), {})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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);

View File

@ -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

View File

@ -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())

View File

@ -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",

View File

@ -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",

View File

@ -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"],
},

View File

@ -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",

View File

@ -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.

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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")

View File

@ -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",

View File

@ -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
}

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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) {

View File

@ -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)

View File

@ -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"]]
}
};
});

View File

@ -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

View File

@ -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(

View File

@ -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);

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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
}

View File

@ -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:

File diff suppressed because it is too large Load Diff