Merge branch 'develop' into refactor/report/bom-stock-calculated

This commit is contained in:
Sagar Sharma 2022-09-12 17:23:15 +05:30 committed by GitHub
commit d3cd3bc5ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 501 additions and 492 deletions

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. 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 ## Logo and Trademark Policy
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md). Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).

View File

@ -53,15 +53,13 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]): if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column")) 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 from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()] if not is_job_queued(self.name):
if self.name not in enqueued_jobs:
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",

View File

@ -4,22 +4,20 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.doctype.account.account import merge_account from erpnext.accounts.doctype.account.account import merge_account
class LedgerMerge(Document): class LedgerMerge(Document):
def start_merge(self): 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.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()] if not is_job_queued(self.name):
if self.name not in enqueued_jobs:
enqueue( enqueue(
start_merge, start_merge,
queue="default", queue="default",

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate 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 ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document):
if len(invoices) < 50: if len(invoices) < 50:
return start_import(invoices) return start_import(invoices)
else: else:
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()] if not is_job_queued(self.name):
if self.name not in enqueued_jobs:
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",

View File

@ -6,11 +6,10 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime 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 from frappe.utils.scheduler import is_scheduler_inactive
@ -467,7 +466,7 @@ def enqueue_job(job, **kwargs):
closing_entry = kwargs.get("closing_entry") or {} closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name") job_name = closing_entry.get("name")
if not job_already_enqueued(job_name): if not is_job_queued(job_name):
enqueue( enqueue(
job, job,
**kwargs, **kwargs,
@ -491,12 +490,6 @@ def check_scheduler_status():
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) 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): def safe_load_json(message):
try: try:
json_message = json.loads(message).get("message") json_message = json.loads(message).get("message")

View File

@ -489,7 +489,6 @@ def make_reverse_gl_entries(
).run(as_dict=1) ).run(as_dict=1)
if gl_entries: if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
create_payment_ledger_entry( create_payment_ledger_entry(
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding 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( def add_customer_filters(
self, self,
): ):
self.customter = qb.DocType("Customer") self.customer = qb.DocType("Customer")
if self.filters.get("customer_group"): if self.filters.get("customer_group"):
self.get_hierarchical_filters("Customer Group", "customer_group") self.get_hierarchical_filters("Customer Group", "customer_group")
@ -838,7 +838,7 @@ class ReceivablePayableReport(object):
customer = self.customer customer = self.customer
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt)) 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)) 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): def add_accounting_dimensions_filters(self):
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@ -205,6 +205,10 @@ class AccountsController(TransactionBase):
def on_trash(self): def on_trash(self):
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): 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( frappe.db.sql(
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
) )

View File

@ -309,7 +309,11 @@ class BuyingController(SubcontractingController):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else: else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate" 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 self.is_internal_transfer():
if rate != d.rate: if rate != d.rate:

View File

@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum
Filters = frappe._dict Filters = frappe._dict
Row = frappe._dict Row = frappe._dict
@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
def execute(filters: Filters) -> Tuple[Columns, Data]: def execute(filters: Filters) -> Tuple[Columns, Data]:
filters = frappe._dict(filters or {})
columns = get_columns() columns = get_columns()
data = get_data(filters) data = get_data(filters)
return columns, data return columns, data
def get_data(filters: Filters) -> Data: def get_data(filters: Filters) -> Data:
query_args = get_query_args(filters) wo = frappe.qb.DocType("Work Order")
data = run_query(query_args) 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) update_data_with_total_pl_value(data)
return 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: def update_data_with_total_pl_value(data: Data) -> None:
for row in data: for row in data:
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"] value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg 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 import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import IfNull
from frappe.utils import cint from frappe.utils import cint
@ -17,70 +18,70 @@ def execute(filters=None):
def get_item_list(wo_list, filters): def get_item_list(wo_list, filters):
out = [] out = []
# Add a row for each item/qty if wo_list:
for wo_details in wo_list: bin = frappe.qb.DocType("Bin")
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType("BOM Item")
for wo_item_details in frappe.db.get_values( # Add a row for each item/qty
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1 for wo_details in wo_list:
): desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
item_list = frappe.db.sql( for wo_item_details in frappe.db.get_values(
"""SELECT "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
bom_item.item_code as item_code, ):
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty item_list = (
FROM frappe.qb.from_(bom)
`tabBOM` as bom, `tabBOM Item` AS bom_item .from_(bom_item)
LEFT JOIN `tabBin` AS ledger .left_join(bin)
ON bom_item.item_code = ledger.item_code .on(
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s) (bom_item.item_code == bin.item_code)
WHERE & (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
bom.name = bom_item.parent )
and bom_item.item_code = %(item_code)s .select(
and bom.name = %(bom)s bom_item.item_code.as_("item_code"),
GROUP BY IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
bom_item.item_code""", )
{ .where(
"bom": wo_details.bom_no, (bom.name == bom_item.parent)
"warehouse": wo_item_details.source_warehouse, & (bom_item.item_code == wo_item_details.item_code)
"filterhouse": filters.warehouse, & (bom.name == wo_details.bom_no)
"item_code": wo_item_details.item_code, )
}, .groupby(bom_item.item_code)
as_dict=1, ).run(as_dict=1)
)
stock_qty = 0 stock_qty = 0
count = 0 count = 0
buildable_qty = wo_details.qty buildable_qty = wo_details.qty
for item in item_list: for item in item_list:
count = count + 1 count = count + 1
if item.build_qty >= (wo_details.qty - wo_details.produced_qty): if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
stock_qty = stock_qty + 1 stock_qty = stock_qty + 1
elif buildable_qty >= item.build_qty: elif buildable_qty >= item.build_qty:
buildable_qty = item.build_qty buildable_qty = item.build_qty
if count == stock_qty: if count == stock_qty:
build = "Y" build = "Y"
else: else:
build = "N" build = "N"
row = frappe._dict( row = frappe._dict(
{ {
"work_order": wo_details.name, "work_order": wo_details.name,
"status": wo_details.status, "status": wo_details.status,
"req_items": cint(count), "req_items": cint(count),
"instock": stock_qty, "instock": stock_qty,
"description": desc, "description": desc,
"source_warehouse": wo_item_details.source_warehouse, "source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code, "item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no, "bom_no": wo_details.bom_no,
"qty": wo_details.qty, "qty": wo_details.qty,
"buildable_qty": buildable_qty, "buildable_qty": buildable_qty,
"ready_to_build": build, "ready_to_build": build,
} }
) )
out.append(row) out.append(row)
return out return out

View File

@ -795,7 +795,7 @@
}, },
{ {
"fieldname": "customer_code", "fieldname": "customer_code",
"fieldtype": "Data", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"label": "Customer Code", "label": "Customer Code",
"no_copy": 1, "no_copy": 1,
@ -910,7 +910,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-06-15 09:02:06.177691", "modified": "2022-09-12 15:00:10.130340",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -778,6 +778,14 @@ class TestItem(FrappeTestCase):
item.has_batch_no = 1 item.has_batch_no = 1
item.save() 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): def set_item_variant_settings(fields):
doc = frappe.get_doc("Item Variant Settings") doc = frappe.get_doc("Item Variant Settings")

View File

@ -815,7 +815,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
return { return {
"filters": { "filters": {
"docstatus": 1, "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_work_order()
self.validate_bom() self.validate_bom()
self.validate_purchase_order() self.validate_purchase_order()
self.validate_subcontracting_order()
if self.purpose in ("Manufacture", "Repack"): if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items() self.mark_finished_and_scrap_items()
@ -959,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): 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)]): if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return return

File diff suppressed because it is too large Load Diff