Merge branch 'develop' into lead-notes-patch
This commit is contained in:
commit
51c37aeee3
32
.github/workflows/initiate_release.yml
vendored
Normal file
32
.github/workflows/initiate_release.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# This workflow is agnostic to branches. Only maintain on develop branch.
|
||||||
|
# To add/remove versions just modify the matrix.
|
||||||
|
|
||||||
|
name: Create weekly release pull requests
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# 9:30 UTC => 3 PM IST Tuesday
|
||||||
|
- cron: "30 9 * * 2"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
version: ["13", "14"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: octokit/request-action@v2.x
|
||||||
|
with:
|
||||||
|
route: POST /repos/{owner}/{repo}/pulls
|
||||||
|
owner: frappe
|
||||||
|
repo: erpnext
|
||||||
|
title: |-
|
||||||
|
"chore: release v${{ matrix.version }}"
|
||||||
|
body: "Automated weekly release."
|
||||||
|
base: version-${{ matrix.version }}
|
||||||
|
head: version-${{ matrix.version }}-hotfix
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt))
|
|||||||
|
|
||||||
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
|
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).
|
||||||
|
@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
show_import_status(frm) {
|
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 successful_records = import_log.filter((log) => log.success);
|
||||||
let failed_records = import_log.filter((log) => !log.success);
|
let failed_records = import_log.filter((log) => !log.success);
|
||||||
if (successful_records.length === 0) return;
|
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',
|
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||||
|
|
||||||
show_import_preview(frm, preview_data) {
|
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 (
|
if (
|
||||||
frm.import_preview &&
|
frm.import_preview &&
|
||||||
@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
show_import_log(frm) {
|
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;
|
let logs = import_log;
|
||||||
frm.toggle_display("import_log", false);
|
frm.toggle_display("import_log", false);
|
||||||
frm.toggle_display("import_log_section", logs.length > 0);
|
frm.toggle_display("import_log_section", logs.length > 0);
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"section_import_preview",
|
"section_import_preview",
|
||||||
"import_preview",
|
"import_preview",
|
||||||
"import_log_section",
|
"import_log_section",
|
||||||
"import_log",
|
"statement_import_log",
|
||||||
"show_failed_logs",
|
"show_failed_logs",
|
||||||
"import_log_preview",
|
"import_log_preview",
|
||||||
"reference_doctype",
|
"reference_doctype",
|
||||||
@ -90,12 +90,6 @@
|
|||||||
"options": "JSON",
|
"options": "JSON",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "import_log",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"label": "Import Log",
|
|
||||||
"options": "JSON"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "import_log_section",
|
"fieldname": "import_log_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -198,11 +192,17 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "statement_import_log",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Statement Import Log",
|
||||||
|
"options": "JSON"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-12 14:17:37.777246",
|
"modified": "2022-09-07 11:11:40.293317",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Statement Import",
|
"name": "Bank Statement Import",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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")
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered" style="font-size: 10px">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%">{{ _("Date") }}</th>
|
<th style="width: 12%">{{ _("Date") }}</th>
|
||||||
|
@ -34,8 +34,8 @@ pricing_rule_fields = [
|
|||||||
other_fields = [
|
other_fields = [
|
||||||
"min_qty",
|
"min_qty",
|
||||||
"max_qty",
|
"max_qty",
|
||||||
"min_amt",
|
"min_amount",
|
||||||
"max_amt",
|
"max_amount",
|
||||||
"priority",
|
"priority",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"threshold_percentage",
|
"threshold_percentage",
|
||||||
@ -246,7 +246,11 @@ def prepare_pricing_rule(
|
|||||||
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||||
pr.update(args)
|
pr.update(args)
|
||||||
for field in other_fields + discount_fields:
|
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_id = child_doc_fields.name
|
||||||
pr.promotional_scheme = doc.name
|
pr.promotional_scheme = doc.name
|
||||||
|
@ -90,6 +90,23 @@ class TestPromotionalScheme(unittest.TestCase):
|
|||||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||||
self.assertEqual(price_rules, [])
|
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):
|
def make_promotional_scheme(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -710,6 +710,7 @@ class SalesInvoice(SellingController):
|
|||||||
if (
|
if (
|
||||||
cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
|
cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
|
||||||
and not self.is_return
|
and not self.is_return
|
||||||
|
and not self.is_internal_customer
|
||||||
):
|
):
|
||||||
self.validate_rate_with_reference_doc(
|
self.validate_rate_with_reference_doc(
|
||||||
[["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]]
|
[["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):
|
def update_item(source, target, source_parent):
|
||||||
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
|
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 = {
|
item_field_map = {
|
||||||
"doctype": target_doctype + " Item",
|
"doctype": target_doctype + " Item",
|
||||||
@ -2187,6 +2199,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
|||||||
"serial_no": "serial_no",
|
"serial_no": "serial_no",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
elif target_doctype == "Sales Order":
|
||||||
|
item_field_map["field_map"].update(
|
||||||
|
{
|
||||||
|
source_document_warehouse_field: "warehouse",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
doctype,
|
doctype,
|
||||||
@ -2231,6 +2249,7 @@ def get_received_items(reference_name, doctype, reference_fieldname):
|
|||||||
|
|
||||||
def set_purchase_references(doc):
|
def set_purchase_references(doc):
|
||||||
# add internal PO or PR links if any
|
# add internal PO or PR links if any
|
||||||
|
|
||||||
if doc.is_internal_transfer():
|
if doc.is_internal_transfer():
|
||||||
if doc.doctype == "Purchase Receipt":
|
if doc.doctype == "Purchase Receipt":
|
||||||
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
|
so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
|
||||||
@ -2260,15 +2279,6 @@ def set_purchase_references(doc):
|
|||||||
warehouse_map,
|
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(
|
def update_pi_items(
|
||||||
doc,
|
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)))
|
item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
|
||||||
if doc.update_stock:
|
if doc.update_stock:
|
||||||
item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
|
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):
|
def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
|
||||||
for item in doc.get("items"):
|
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.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):
|
def get_delivery_note_details(internal_reference):
|
||||||
|
@ -96,6 +96,10 @@
|
|||||||
"delivery_note",
|
"delivery_note",
|
||||||
"dn_detail",
|
"dn_detail",
|
||||||
"delivered_qty",
|
"delivered_qty",
|
||||||
|
"internal_transfer_section",
|
||||||
|
"purchase_order",
|
||||||
|
"column_break_92",
|
||||||
|
"purchase_order_item",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@ -840,12 +844,38 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Grant Commission",
|
"label": "Grant Commission",
|
||||||
"read_only": 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_92",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_order_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Purchase Order Item",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-08-26 12:06:31.205417",
|
"modified": "2022-09-06 14:17:43.394309",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"section_break_45",
|
"section_break_45",
|
||||||
"before_items_section",
|
"before_items_section",
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
|
"set_from_warehouse",
|
||||||
"items_col_break",
|
"items_col_break",
|
||||||
"set_warehouse",
|
"set_warehouse",
|
||||||
"items_section",
|
"items_section",
|
||||||
@ -1166,13 +1167,20 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Is Old Subcontracting Flow",
|
"label": "Is Old Subcontracting Flow",
|
||||||
"read_only": 1
|
"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",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-15 15:40:58.527065",
|
"modified": "2022-09-07 11:06:46.035093",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
@ -23,5 +23,6 @@ def get_data():
|
|||||||
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
||||||
},
|
},
|
||||||
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
|
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
|
||||||
|
{"label": _("Internal"), "items": ["Sales Order"]},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,10 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate
|
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.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 (
|
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||||
make_purchase_invoice as make_pi_from_po,
|
make_purchase_invoice as make_pi_from_po,
|
||||||
)
|
)
|
||||||
@ -796,6 +798,111 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
|
|
||||||
automatically_fetch_payment_terms(enable=0)
|
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):
|
def make_pr_against_po(po, received_qty=0):
|
||||||
pr = make_purchase_receipt(po)
|
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",
|
"item_code": args.item or args.item_code or "_Test Item",
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
|
"from_warehouse": args.from_warehouse,
|
||||||
"qty": args.qty or 10,
|
"qty": args.qty or 10,
|
||||||
"rate": args.rate or 500,
|
"rate": args.rate or 500,
|
||||||
"schedule_date": add_days(nowdate(), 1),
|
"schedule_date": add_days(nowdate(), 1),
|
||||||
|
@ -10,12 +10,14 @@
|
|||||||
"item_code",
|
"item_code",
|
||||||
"supplier_part_no",
|
"supplier_part_no",
|
||||||
"item_name",
|
"item_name",
|
||||||
|
"brand",
|
||||||
"product_bundle",
|
"product_bundle",
|
||||||
"fg_item",
|
"fg_item",
|
||||||
"fg_item_qty",
|
"fg_item_qty",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"schedule_date",
|
"schedule_date",
|
||||||
"expected_delivery_date",
|
"expected_delivery_date",
|
||||||
|
"item_group",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"description",
|
"description",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
@ -58,9 +60,12 @@
|
|||||||
"base_net_rate",
|
"base_net_rate",
|
||||||
"base_net_amount",
|
"base_net_amount",
|
||||||
"warehouse_and_reference",
|
"warehouse_and_reference",
|
||||||
|
"from_warehouse",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
|
"column_break_54",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
"company_total_stock",
|
"company_total_stock",
|
||||||
|
"references_section",
|
||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
@ -73,8 +78,6 @@
|
|||||||
"against_blanket_order",
|
"against_blanket_order",
|
||||||
"blanket_order",
|
"blanket_order",
|
||||||
"blanket_order_rate",
|
"blanket_order_rate",
|
||||||
"item_group",
|
|
||||||
"brand",
|
|
||||||
"section_break_56",
|
"section_break_56",
|
||||||
"received_qty",
|
"received_qty",
|
||||||
"returned_qty",
|
"returned_qty",
|
||||||
@ -442,13 +445,13 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "warehouse_and_reference",
|
"fieldname": "warehouse_and_reference",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Warehouse and Reference"
|
"label": "Warehouse Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Warehouse",
|
"label": "Target Warehouse",
|
||||||
"oldfieldname": "warehouse",
|
"oldfieldname": "warehouse",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
@ -760,7 +763,7 @@
|
|||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "actual_qty",
|
"fieldname": "actual_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Available Qty at Warehouse",
|
"label": "Available Qty at Target Warehouse",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -868,13 +871,30 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Finished Good Item Qty",
|
"label": "Finished Good Item Qty",
|
||||||
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow"
|
"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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-17 05:29:40.602349",
|
"modified": "2022-09-07 11:12:38.634976",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
@ -373,7 +377,7 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_inter_company_reference(self):
|
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
|
return
|
||||||
|
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
|
@ -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:
|
||||||
|
@ -1,193 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.desk.form import assign_to
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import add_days, flt, unique
|
|
||||||
|
|
||||||
from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee
|
|
||||||
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
|
||||||
|
|
||||||
|
|
||||||
class EmployeeBoardingController(Document):
|
|
||||||
"""
|
|
||||||
Create the project and the task for the boarding process
|
|
||||||
Assign to the concerned person and roles as per the onboarding/separation template
|
|
||||||
"""
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
# remove the task if linked before submitting the form
|
|
||||||
if self.amended_from:
|
|
||||||
for activity in self.activities:
|
|
||||||
activity.task = ""
|
|
||||||
|
|
||||||
def on_submit(self):
|
|
||||||
# create the project for the given employee onboarding
|
|
||||||
project_name = _(self.doctype) + " : "
|
|
||||||
if self.doctype == "Employee Onboarding":
|
|
||||||
project_name += self.job_applicant
|
|
||||||
else:
|
|
||||||
project_name += self.employee
|
|
||||||
|
|
||||||
project = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Project",
|
|
||||||
"project_name": project_name,
|
|
||||||
"expected_start_date": self.date_of_joining
|
|
||||||
if self.doctype == "Employee Onboarding"
|
|
||||||
else self.resignation_letter_date,
|
|
||||||
"department": self.department,
|
|
||||||
"company": self.company,
|
|
||||||
}
|
|
||||||
).insert(ignore_permissions=True, ignore_mandatory=True)
|
|
||||||
|
|
||||||
self.db_set("project", project.name)
|
|
||||||
self.db_set("boarding_status", "Pending")
|
|
||||||
self.reload()
|
|
||||||
self.create_task_and_notify_user()
|
|
||||||
|
|
||||||
def create_task_and_notify_user(self):
|
|
||||||
# create the task for the given project and assign to the concerned person
|
|
||||||
holiday_list = self.get_holiday_list()
|
|
||||||
|
|
||||||
for activity in self.activities:
|
|
||||||
if activity.task:
|
|
||||||
continue
|
|
||||||
|
|
||||||
dates = self.get_task_dates(activity, holiday_list)
|
|
||||||
|
|
||||||
task = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Task",
|
|
||||||
"project": self.project,
|
|
||||||
"subject": activity.activity_name + " : " + self.employee_name,
|
|
||||||
"description": activity.description,
|
|
||||||
"department": self.department,
|
|
||||||
"company": self.company,
|
|
||||||
"task_weight": activity.task_weight,
|
|
||||||
"exp_start_date": dates[0],
|
|
||||||
"exp_end_date": dates[1],
|
|
||||||
}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
activity.db_set("task", task.name)
|
|
||||||
|
|
||||||
users = [activity.user] if activity.user else []
|
|
||||||
if activity.role:
|
|
||||||
user_list = frappe.db.sql_list(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
DISTINCT(has_role.parent)
|
|
||||||
FROM
|
|
||||||
`tabHas Role` has_role
|
|
||||||
LEFT JOIN `tabUser` user
|
|
||||||
ON has_role.parent = user.name
|
|
||||||
WHERE
|
|
||||||
has_role.parenttype = 'User'
|
|
||||||
AND user.enabled = 1
|
|
||||||
AND has_role.role = %s
|
|
||||||
""",
|
|
||||||
activity.role,
|
|
||||||
)
|
|
||||||
users = unique(users + user_list)
|
|
||||||
|
|
||||||
if "Administrator" in users:
|
|
||||||
users.remove("Administrator")
|
|
||||||
|
|
||||||
# assign the task the users
|
|
||||||
if users:
|
|
||||||
self.assign_task_to_users(task, users)
|
|
||||||
|
|
||||||
def get_holiday_list(self):
|
|
||||||
if self.doctype == "Employee Separation":
|
|
||||||
return get_holiday_list_for_employee(self.employee)
|
|
||||||
else:
|
|
||||||
if self.employee:
|
|
||||||
return get_holiday_list_for_employee(self.employee)
|
|
||||||
else:
|
|
||||||
if not self.holiday_list:
|
|
||||||
frappe.throw(_("Please set the Holiday List."), frappe.MandatoryError)
|
|
||||||
else:
|
|
||||||
return self.holiday_list
|
|
||||||
|
|
||||||
def get_task_dates(self, activity, holiday_list):
|
|
||||||
start_date = end_date = None
|
|
||||||
|
|
||||||
if activity.begin_on is not None:
|
|
||||||
start_date = add_days(self.boarding_begins_on, activity.begin_on)
|
|
||||||
start_date = self.update_if_holiday(start_date, holiday_list)
|
|
||||||
|
|
||||||
if activity.duration is not None:
|
|
||||||
end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
|
|
||||||
end_date = self.update_if_holiday(end_date, holiday_list)
|
|
||||||
|
|
||||||
return [start_date, end_date]
|
|
||||||
|
|
||||||
def update_if_holiday(self, date, holiday_list):
|
|
||||||
while is_holiday(holiday_list, date):
|
|
||||||
date = add_days(date, 1)
|
|
||||||
return date
|
|
||||||
|
|
||||||
def assign_task_to_users(self, task, users):
|
|
||||||
for user in users:
|
|
||||||
args = {
|
|
||||||
"assign_to": [user],
|
|
||||||
"doctype": task.doctype,
|
|
||||||
"name": task.name,
|
|
||||||
"description": task.description or task.subject,
|
|
||||||
"notify": self.notify_users_by_email,
|
|
||||||
}
|
|
||||||
assign_to.add(args)
|
|
||||||
|
|
||||||
def on_cancel(self):
|
|
||||||
# delete task project
|
|
||||||
project = self.project
|
|
||||||
for task in frappe.get_all("Task", filters={"project": project}):
|
|
||||||
frappe.delete_doc("Task", task.name, force=1)
|
|
||||||
frappe.delete_doc("Project", project, force=1)
|
|
||||||
self.db_set("project", "")
|
|
||||||
for activity in self.activities:
|
|
||||||
activity.db_set("task", "")
|
|
||||||
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Linked Project {} and Tasks deleted.").format(project), alert=True, indicator="blue"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_onboarding_details(parent, parenttype):
|
|
||||||
return frappe.get_all(
|
|
||||||
"Employee Boarding Activity",
|
|
||||||
fields=[
|
|
||||||
"activity_name",
|
|
||||||
"role",
|
|
||||||
"user",
|
|
||||||
"required_for_employee_creation",
|
|
||||||
"description",
|
|
||||||
"task_weight",
|
|
||||||
"begin_on",
|
|
||||||
"duration",
|
|
||||||
],
|
|
||||||
filters={"parent": parent, "parenttype": parenttype},
|
|
||||||
order_by="idx",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_employee_boarding_status(project):
|
|
||||||
employee_onboarding = frappe.db.exists("Employee Onboarding", {"project": project.name})
|
|
||||||
employee_separation = frappe.db.exists("Employee Separation", {"project": project.name})
|
|
||||||
|
|
||||||
if not (employee_onboarding or employee_separation):
|
|
||||||
return
|
|
||||||
|
|
||||||
status = "Pending"
|
|
||||||
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
|
|
||||||
status = "In Process"
|
|
||||||
elif flt(project.percent_complete) == 100.0:
|
|
||||||
status = "Completed"
|
|
||||||
|
|
||||||
if employee_onboarding:
|
|
||||||
frappe.db.set_value("Employee Onboarding", employee_onboarding, "boarding_status", status)
|
|
||||||
elif employee_separation:
|
|
||||||
frappe.db.set_value("Employee Separation", employee_separation, "boarding_status", status)
|
|
@ -311,6 +311,7 @@ class SellingController(StockController):
|
|||||||
"sales_invoice_item": d.get("sales_invoice_item"),
|
"sales_invoice_item": d.get("sales_invoice_item"),
|
||||||
"dn_detail": d.get("dn_detail"),
|
"dn_detail": d.get("dn_detail"),
|
||||||
"incoming_rate": p.get("incoming_rate"),
|
"incoming_rate": p.get("incoming_rate"),
|
||||||
|
"item_row": p,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -334,6 +335,7 @@ class SellingController(StockController):
|
|||||||
"sales_invoice_item": d.get("sales_invoice_item"),
|
"sales_invoice_item": d.get("sales_invoice_item"),
|
||||||
"dn_detail": d.get("dn_detail"),
|
"dn_detail": d.get("dn_detail"),
|
||||||
"incoming_rate": d.get("incoming_rate"),
|
"incoming_rate": d.get("incoming_rate"),
|
||||||
|
"item_row": d,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -307,6 +307,20 @@ class StatusUpdater(Document):
|
|||||||
|
|
||||||
def limits_crossed_error(self, args, item, qty_or_amount):
|
def limits_crossed_error(self, args, item, qty_or_amount):
|
||||||
"""Raise exception for limits crossed"""
|
"""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":
|
if qty_or_amount == "qty":
|
||||||
action_msg = _(
|
action_msg = _(
|
||||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||||
|
@ -390,6 +390,10 @@ class StockController(AccountsController):
|
|||||||
return sl_dict
|
return sl_dict
|
||||||
|
|
||||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
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)
|
dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
|
||||||
for dimension in dimensions:
|
for dimension in dimensions:
|
||||||
if not dimension:
|
if not dimension:
|
||||||
@ -407,9 +411,17 @@ class StockController(AccountsController):
|
|||||||
"DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname"
|
"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):
|
if fieldname and self.get(fieldname):
|
||||||
sl_dict[dimension.target_fieldname] = 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):
|
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
|
from erpnext.stock.stock_ledger import make_sl_entries
|
||||||
|
|
||||||
@ -688,6 +700,47 @@ class StockController(AccountsController):
|
|||||||
else:
|
else:
|
||||||
create_repost_item_valuation_entry(args)
|
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:
|
def repost_required_for_queue(doc: StockController) -> bool:
|
||||||
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
||||||
|
@ -7,6 +7,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, cstr, flt, get_link_to_form
|
from frappe.utils import cint, cstr, flt, get_link_to_form
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
@ -870,7 +871,18 @@ def add_items_in_ste(
|
|||||||
def make_return_stock_entry_for_subcontract(
|
def make_return_stock_entry_for_subcontract(
|
||||||
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
|
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"
|
ste_doc.purpose = "Material Transfer"
|
||||||
|
|
||||||
if order_doctype == "Purchase Order":
|
if order_doctype == "Purchase Order":
|
||||||
|
@ -897,7 +897,7 @@ def make_stock_transfer_entry(**args):
|
|||||||
"item_name": row.item_code,
|
"item_name": row.item_code,
|
||||||
"rate": row.rate or 100,
|
"rate": row.rate or 100,
|
||||||
"stock_uom": row.stock_uom or "Nos",
|
"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)
|
item_details = args.itemwise_details.get(row.item_code)
|
||||||
@ -1031,9 +1031,9 @@ def get_subcontracting_order(**args):
|
|||||||
if not args.service_items:
|
if not args.service_items:
|
||||||
service_items = [
|
service_items = [
|
||||||
{
|
{
|
||||||
"warehouse": "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"item_code": "Subcontracted Service Item 7",
|
"item_code": "Subcontracted Service Item 7",
|
||||||
"qty": 5,
|
"qty": 10,
|
||||||
"rate": 100,
|
"rate": 100,
|
||||||
"fg_item": "Subcontracted Item SA7",
|
"fg_item": "Subcontracted Item SA7",
|
||||||
"fg_item_qty": 10,
|
"fg_item_qty": 10,
|
||||||
@ -1046,6 +1046,7 @@ def get_subcontracting_order(**args):
|
|||||||
rm_items=service_items,
|
rm_items=service_items,
|
||||||
is_subcontracted=1,
|
is_subcontracted=1,
|
||||||
supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC",
|
supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC",
|
||||||
|
company=args.company,
|
||||||
)
|
)
|
||||||
|
|
||||||
return create_subcontracting_order(po_name=po.name, **args)
|
return create_subcontracting_order(po_name=po.name, **args)
|
||||||
|
@ -11,17 +11,24 @@ frappe.query_reports["BOM Stock Calculated"] = {
|
|||||||
"options": "BOM",
|
"options": "BOM",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"label": __("Warehouse"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Warehouse",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "qty_to_make",
|
"fieldname": "qty_to_make",
|
||||||
"label": __("Quantity to Make"),
|
"label": __("Quantity to Make"),
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"default": "1"
|
"default": "1.0",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"fieldname": "show_exploded_view",
|
"fieldname": "show_exploded_view",
|
||||||
"label": __("Show exploded view"),
|
"label": __("Show exploded view"),
|
||||||
"fieldtype": "Check"
|
"fieldtype": "Check",
|
||||||
|
"default": false,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -4,29 +4,31 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils.data import comma_and
|
from frappe.utils.data import comma_and
|
||||||
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
# if not filters: filters = {}
|
|
||||||
columns = get_columns()
|
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")
|
qty_to_make = filters.get("qty_to_make")
|
||||||
|
|
||||||
manufacture_details = get_manufacturer_records()
|
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))
|
for row in bom_data:
|
||||||
return columns, summ_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):
|
def get_report_data(last_purchase_rate, required_qty, row, manufacture_details):
|
||||||
to_build = row.to_build if row.to_build > 0 else 0
|
qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0
|
||||||
diff_qty = to_build - reqd_qty
|
difference_qty = row.actual_qty - required_qty
|
||||||
return [
|
return [
|
||||||
row.item_code,
|
row.item_code,
|
||||||
row.description,
|
row.description,
|
||||||
@ -34,85 +36,126 @@ def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
|
|||||||
comma_and(
|
comma_and(
|
||||||
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
|
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
|
||||||
),
|
),
|
||||||
|
qty_per_unit,
|
||||||
row.actual_qty,
|
row.actual_qty,
|
||||||
str(to_build),
|
required_qty,
|
||||||
reqd_qty,
|
difference_qty,
|
||||||
diff_qty,
|
last_purchase_rate,
|
||||||
last_pur_price,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_columns():
|
def get_columns():
|
||||||
"""return columns"""
|
return [
|
||||||
columns = [
|
{
|
||||||
_("Item") + ":Link/Item:100",
|
"fieldname": "item",
|
||||||
_("Description") + "::150",
|
"label": _("Item"),
|
||||||
_("Manufacturer") + "::250",
|
"fieldtype": "Link",
|
||||||
_("Manufacturer Part Number") + "::250",
|
"options": "Item",
|
||||||
_("Qty") + ":Float:50",
|
"width": 120,
|
||||||
_("Stock Qty") + ":Float:100",
|
},
|
||||||
_("Reqd Qty") + ":Float:100",
|
{
|
||||||
_("Diff Qty") + ":Float:100",
|
"fieldname": "description",
|
||||||
_("Last Purchase Price") + ":Float:100",
|
"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):
|
def get_bom_data(filters):
|
||||||
conditions = ""
|
|
||||||
bom = filters.get("bom")
|
|
||||||
|
|
||||||
table = "`tabBOM Item`"
|
|
||||||
qty_field = "qty"
|
|
||||||
|
|
||||||
if filters.get("show_exploded_view"):
|
if filters.get("show_exploded_view"):
|
||||||
table = "`tabBOM Explosion Item`"
|
bom_item_table = "BOM Explosion Item"
|
||||||
qty_field = "stock_qty"
|
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"):
|
if filters.get("warehouse"):
|
||||||
warehouse_details = frappe.db.get_value(
|
warehouse_details = frappe.db.get_value(
|
||||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|
||||||
if warehouse_details:
|
if warehouse_details:
|
||||||
conditions += (
|
wh = frappe.qb.DocType("Warehouse")
|
||||||
" and exists (select name from `tabWarehouse` wh \
|
query = query.where(
|
||||||
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
|
ExistsCriterion(
|
||||||
% (warehouse_details.lft, warehouse_details.rgt)
|
frappe.qb.from_(wh)
|
||||||
|
.select(wh.name)
|
||||||
|
.where(
|
||||||
|
(wh.lft >= warehouse_details.lft)
|
||||||
|
& (wh.rgt <= warehouse_details.rgt)
|
||||||
|
& (bin.warehouse == wh.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
|
query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse")))
|
||||||
|
|
||||||
else:
|
return query.run(as_dict=True)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_manufacturer_records():
|
def get_manufacturer_records():
|
||||||
details = frappe.get_all(
|
details = frappe.get_all(
|
||||||
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
|
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacture_details = frappe._dict()
|
manufacture_details = frappe._dict()
|
||||||
for detail in details:
|
for detail in details:
|
||||||
dic = manufacture_details.setdefault(detail.get("item_code"), {})
|
dic = manufacture_details.setdefault(detail.get("item_code"), {})
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import (
|
||||||
|
execute as bom_stock_calculated_report,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestBOMStockCalculated(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.fg_item, self.rm_items = create_items()
|
||||||
|
self.boms = create_boms(self.fg_item, self.rm_items)
|
||||||
|
|
||||||
|
def test_bom_stock_calculated(self):
|
||||||
|
qty_to_make = 10
|
||||||
|
|
||||||
|
# Case 1: When Item(s) Qty and Stock Qty are equal.
|
||||||
|
data = bom_stock_calculated_report(
|
||||||
|
filters={
|
||||||
|
"qty_to_make": qty_to_make,
|
||||||
|
"bom": self.boms[0].name,
|
||||||
|
}
|
||||||
|
)[1]
|
||||||
|
expected_data = get_expected_data(self.boms[0], qty_to_make)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
# Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1.
|
||||||
|
data = bom_stock_calculated_report(
|
||||||
|
filters={
|
||||||
|
"qty_to_make": qty_to_make,
|
||||||
|
"bom": self.boms[1].name,
|
||||||
|
}
|
||||||
|
)[1]
|
||||||
|
expected_data = get_expected_data(self.boms[1], qty_to_make)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
# Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1.
|
||||||
|
data = bom_stock_calculated_report(
|
||||||
|
filters={
|
||||||
|
"qty_to_make": qty_to_make,
|
||||||
|
"bom": self.boms[2].name,
|
||||||
|
}
|
||||||
|
)[1]
|
||||||
|
expected_data = get_expected_data(self.boms[2], qty_to_make)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
|
||||||
|
def create_items():
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 100,
|
||||||
|
"opening_stock": 100,
|
||||||
|
"last_purchase_rate": 100,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
rm_item2 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 200,
|
||||||
|
"opening_stock": 200,
|
||||||
|
"last_purchase_rate": 200,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
return fg_item, [rm_item1, rm_item2]
|
||||||
|
|
||||||
|
|
||||||
|
def create_boms(fg_item, rm_items):
|
||||||
|
def update_bom_items(bom, uom, conversion_factor):
|
||||||
|
for item in bom.items:
|
||||||
|
item.uom = uom
|
||||||
|
item.conversion_factor = conversion_factor
|
||||||
|
|
||||||
|
return bom
|
||||||
|
|
||||||
|
bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10)
|
||||||
|
|
||||||
|
bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
|
||||||
|
bom2 = update_bom_items(bom2, "Box", 10)
|
||||||
|
bom2.save()
|
||||||
|
bom2.submit()
|
||||||
|
|
||||||
|
bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
|
||||||
|
bom3 = update_bom_items(bom3, "Box", 10)
|
||||||
|
bom3.save()
|
||||||
|
bom3.submit()
|
||||||
|
|
||||||
|
return [bom1, bom2, bom3]
|
||||||
|
|
||||||
|
|
||||||
|
def get_expected_data(bom, qty_to_make):
|
||||||
|
expected_data = []
|
||||||
|
|
||||||
|
for idx in range(len(bom.items)):
|
||||||
|
expected_data.append(
|
||||||
|
[
|
||||||
|
bom.items[idx].item_code,
|
||||||
|
bom.items[idx].item_code,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
float(bom.items[idx].stock_qty / bom.quantity),
|
||||||
|
float(100 * (idx + 1)),
|
||||||
|
float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)),
|
||||||
|
float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))),
|
||||||
|
float(100 * (idx + 1)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return expected_data
|
@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
|
|||||||
|
|
||||||
import frappe
|
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
|
|
||||||
|
@ -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,6 +18,11 @@ def execute(filters=None):
|
|||||||
def get_item_list(wo_list, filters):
|
def get_item_list(wo_list, filters):
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
|
if wo_list:
|
||||||
|
bin = frappe.qb.DocType("Bin")
|
||||||
|
bom = frappe.qb.DocType("BOM")
|
||||||
|
bom_item = frappe.qb.DocType("BOM Item")
|
||||||
|
|
||||||
# Add a row for each item/qty
|
# Add a row for each item/qty
|
||||||
for wo_details in wo_list:
|
for wo_details in wo_list:
|
||||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||||
@ -24,30 +30,25 @@ def get_item_list(wo_list, filters):
|
|||||||
for wo_item_details in frappe.db.get_values(
|
for wo_item_details in frappe.db.get_values(
|
||||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||||
):
|
):
|
||||||
|
item_list = (
|
||||||
item_list = frappe.db.sql(
|
frappe.qb.from_(bom)
|
||||||
"""SELECT
|
.from_(bom_item)
|
||||||
bom_item.item_code as item_code,
|
.left_join(bin)
|
||||||
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
|
.on(
|
||||||
FROM
|
(bom_item.item_code == bin.item_code)
|
||||||
`tabBOM` as bom, `tabBOM Item` AS bom_item
|
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
.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
|
stock_qty = 0
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -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.remove_hr_and_payroll_modules # 20-07-2022
|
||||||
erpnext.patches.v14_0.fix_crm_no_of_employees
|
erpnext.patches.v14_0.fix_crm_no_of_employees
|
||||||
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
|
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
|
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import (
|
||||||
|
get_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for company in frappe.db.get_list("Company", pluck="name"):
|
||||||
|
data += get_data(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"company": company,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
for d in data:
|
||||||
|
if d and d.get("voucher_type") == "Subcontracting Receipt":
|
||||||
|
doc = frappe.new_doc("Repost Item Valuation")
|
||||||
|
doc.voucher_type = d.get("voucher_type")
|
||||||
|
doc.voucher_no = d.get("voucher_no")
|
||||||
|
doc.save()
|
||||||
|
doc.submit()
|
@ -10,7 +10,6 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
||||||
|
|
||||||
from erpnext import get_default_company
|
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.controllers.queries import get_filters_cond
|
||||||
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
@ -43,7 +42,6 @@ class Project(Document):
|
|||||||
self.send_welcome_email()
|
self.send_welcome_email()
|
||||||
self.update_costing()
|
self.update_costing()
|
||||||
self.update_percent_complete()
|
self.update_percent_complete()
|
||||||
update_employee_boarding_status(self)
|
|
||||||
|
|
||||||
def copy_from_template(self):
|
def copy_from_template(self):
|
||||||
"""
|
"""
|
||||||
@ -145,7 +143,6 @@ class Project(Document):
|
|||||||
def update_project(self):
|
def update_project(self):
|
||||||
"""Called externally by Task"""
|
"""Called externally by Task"""
|
||||||
self.update_percent_complete()
|
self.update_percent_complete()
|
||||||
update_employee_boarding_status(self)
|
|
||||||
self.update_costing()
|
self.update_costing()
|
||||||
self.db_update()
|
self.db_update()
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ $.extend(erpnext.utils, {
|
|||||||
if (!found) {
|
if (!found) {
|
||||||
filters.splice(index, 0, {
|
filters.splice(index, 0, {
|
||||||
"fieldname": dimension["fieldname"],
|
"fieldname": dimension["fieldname"],
|
||||||
"label": __(dimension["label"]),
|
"label": __(dimension["doctype"]),
|
||||||
"fieldtype": "MultiSelectList",
|
"fieldtype": "MultiSelectList",
|
||||||
get_data: function(txt) {
|
get_data: function(txt) {
|
||||||
return frappe.db.get_link_options(dimension["doctype"], txt);
|
return frappe.db.get_link_options(dimension["doctype"], txt);
|
||||||
|
@ -84,7 +84,7 @@ def create_qr_code(doc, method=None):
|
|||||||
tlv_array.append("".join([tag, length, value]))
|
tlv_array.append("".join([tag, length, value]))
|
||||||
|
|
||||||
# Invoice Amount
|
# Invoice Amount
|
||||||
invoice_amount = str(doc.grand_total)
|
invoice_amount = str(doc.base_grand_total)
|
||||||
tag = bytes([4]).hex()
|
tag = bytes([4]).hex()
|
||||||
length = bytes([len(invoice_amount)]).hex()
|
length = bytes([len(invoice_amount)]).hex()
|
||||||
value = invoice_amount.encode("utf-8").hex()
|
value = invoice_amount.encode("utf-8").hex()
|
||||||
@ -144,7 +144,7 @@ def get_vat_amount(doc):
|
|||||||
|
|
||||||
for tax in doc.get("taxes"):
|
for tax in doc.get("taxes"):
|
||||||
if tax.account_head in vat_accounts:
|
if tax.account_head in vat_accounts:
|
||||||
vat_amount += tax.tax_amount
|
vat_amount += tax.base_tax_amount
|
||||||
|
|
||||||
return vat_amount
|
return vat_amount
|
||||||
|
|
||||||
|
@ -59,7 +59,36 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) {
|
||||||
|
frm.events.get_items_from_internal_purchase_order(frm);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_items_from_internal_purchase_order(frm) {
|
||||||
|
frm.add_custom_button(__('Purchase Order'), () => {
|
||||||
|
erpnext.utils.map_current_doc({
|
||||||
|
method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order',
|
||||||
|
source_doctype: 'Purchase Order',
|
||||||
|
target: frm,
|
||||||
|
setters: [
|
||||||
|
{
|
||||||
|
label: 'Supplier',
|
||||||
|
fieldname: 'supplier',
|
||||||
|
fieldtype: 'Link',
|
||||||
|
options: 'Supplier'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
get_query_filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
is_internal_supplier: 1,
|
||||||
|
docstatus: 1,
|
||||||
|
status: ['!=', 'Completed']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, __('Get Items From'));
|
||||||
|
},
|
||||||
|
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
if (!frm.doc.transaction_date){
|
if (!frm.doc.transaction_date){
|
||||||
frm.set_value('transaction_date', frappe.datetime.get_today())
|
frm.set_value('transaction_date', frappe.datetime.get_today())
|
||||||
|
@ -92,7 +92,11 @@
|
|||||||
"section_break_63",
|
"section_break_63",
|
||||||
"page_break",
|
"page_break",
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"transaction_date"
|
"transaction_date",
|
||||||
|
"inter_transfer_reference_section",
|
||||||
|
"purchase_order",
|
||||||
|
"column_break_89",
|
||||||
|
"purchase_order_item"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -809,12 +813,36 @@
|
|||||||
"label": "Picked Qty (in Stock UOM)",
|
"label": "Picked Qty (in Stock UOM)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-17 05:27:41.603006",
|
"modified": "2022-09-06 13:24:18.065312",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
@ -10,79 +10,89 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"basic_details_tab",
|
||||||
"basic_information",
|
"basic_information",
|
||||||
"employee",
|
"employee",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"first_name",
|
"first_name",
|
||||||
"middle_name",
|
"middle_name",
|
||||||
"last_name",
|
"last_name",
|
||||||
"salutation",
|
|
||||||
"employee_name",
|
"employee_name",
|
||||||
"image",
|
"column_break_9",
|
||||||
"column_break1",
|
|
||||||
"company",
|
|
||||||
"status",
|
|
||||||
"gender",
|
"gender",
|
||||||
"date_of_birth",
|
"date_of_birth",
|
||||||
|
"salutation",
|
||||||
|
"column_break1",
|
||||||
"date_of_joining",
|
"date_of_joining",
|
||||||
"employee_number",
|
"image",
|
||||||
"emergency_contact_details",
|
"status",
|
||||||
"person_to_be_contacted",
|
|
||||||
"relation",
|
|
||||||
"column_break_19",
|
|
||||||
"emergency_phone_number",
|
|
||||||
"erpnext_user",
|
"erpnext_user",
|
||||||
"user_id",
|
"user_id",
|
||||||
"create_user",
|
"create_user",
|
||||||
"create_user_permission",
|
"create_user_permission",
|
||||||
"employment_details",
|
"company_details_section",
|
||||||
"scheduled_confirmation_date",
|
"company",
|
||||||
"final_confirmation_date",
|
|
||||||
"col_break_22",
|
|
||||||
"contract_end_date",
|
|
||||||
"notice_number_of_days",
|
|
||||||
"date_of_retirement",
|
|
||||||
"job_profile",
|
|
||||||
"department",
|
"department",
|
||||||
|
"employee_number",
|
||||||
|
"column_break_25",
|
||||||
"designation",
|
"designation",
|
||||||
"reports_to",
|
"reports_to",
|
||||||
"column_break_31",
|
"column_break_18",
|
||||||
"branch",
|
"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_and_leave_details",
|
||||||
"attendance_device_id",
|
"attendance_device_id",
|
||||||
"column_break_44",
|
"column_break_44",
|
||||||
"holiday_list",
|
"holiday_list",
|
||||||
"salary_information",
|
"salary_information",
|
||||||
"salary_currency",
|
|
||||||
"ctc",
|
"ctc",
|
||||||
"payroll_cost_center",
|
"salary_currency",
|
||||||
"column_break_52",
|
"salary_mode",
|
||||||
|
"bank_details_section",
|
||||||
"bank_name",
|
"bank_name",
|
||||||
"bank_ac_no",
|
"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",
|
"personal_details",
|
||||||
"passport_number",
|
|
||||||
"date_of_issue",
|
|
||||||
"valid_upto",
|
|
||||||
"place_of_issue",
|
|
||||||
"marital_status",
|
"marital_status",
|
||||||
"blood_group",
|
|
||||||
"column_break6",
|
|
||||||
"family_background",
|
"family_background",
|
||||||
|
"column_break6",
|
||||||
|
"blood_group",
|
||||||
"health_details",
|
"health_details",
|
||||||
|
"passport_details_section",
|
||||||
|
"passport_number",
|
||||||
|
"valid_upto",
|
||||||
|
"column_break_73",
|
||||||
|
"date_of_issue",
|
||||||
|
"place_of_issue",
|
||||||
|
"profile_tab",
|
||||||
|
"bio",
|
||||||
"educational_qualification",
|
"educational_qualification",
|
||||||
"education",
|
"education",
|
||||||
"previous_work_experience",
|
"previous_work_experience",
|
||||||
@ -92,16 +102,20 @@
|
|||||||
"exit",
|
"exit",
|
||||||
"resignation_letter_date",
|
"resignation_letter_date",
|
||||||
"relieving_date",
|
"relieving_date",
|
||||||
"reason_for_leaving",
|
|
||||||
"leave_encashed",
|
|
||||||
"encashment_date",
|
|
||||||
"exit_interview_details",
|
"exit_interview_details",
|
||||||
"held_on",
|
"held_on",
|
||||||
"new_workplace",
|
"new_workplace",
|
||||||
|
"column_break_99",
|
||||||
|
"leave_encashed",
|
||||||
|
"encashment_date",
|
||||||
|
"feedback_section",
|
||||||
|
"reason_for_leaving",
|
||||||
|
"column_break_104",
|
||||||
"feedback",
|
"feedback",
|
||||||
"lft",
|
"lft",
|
||||||
"rgt",
|
"rgt",
|
||||||
"old_parent"
|
"old_parent",
|
||||||
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -261,7 +275,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "erpnext_user",
|
"fieldname": "erpnext_user",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "ERPNext User"
|
"label": "User Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "System User (login) ID. If set, it will become default for all HR forms.",
|
"description": "System User (login) ID. If set, it will become default for all HR forms.",
|
||||||
@ -289,8 +303,8 @@
|
|||||||
"allow_in_quick_entry": 1,
|
"allow_in_quick_entry": 1,
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "employment_details",
|
"fieldname": "employment_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Joining Details"
|
"label": "Joining"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "scheduled_confirmation_date",
|
"fieldname": "scheduled_confirmation_date",
|
||||||
@ -331,12 +345,6 @@
|
|||||||
"oldfieldname": "date_of_retirement",
|
"oldfieldname": "date_of_retirement",
|
||||||
"oldfieldtype": "Date"
|
"oldfieldtype": "Date"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "job_profile",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Department"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "department",
|
"fieldname": "department",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -366,10 +374,6 @@
|
|||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Employee"
|
"options": "Employee"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_31",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "branch",
|
"fieldname": "branch",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -391,7 +395,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "salary_information",
|
"fieldname": "salary_information",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Salary Details",
|
"label": "Salary Details",
|
||||||
"oldfieldtype": "Section Break",
|
"oldfieldtype": "Section Break",
|
||||||
"width": "50%"
|
"width": "50%"
|
||||||
@ -423,8 +427,8 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "contact_details",
|
"fieldname": "contact_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact Details"
|
"label": "Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "cell_number",
|
"fieldname": "cell_number",
|
||||||
@ -493,12 +497,6 @@
|
|||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Current Address"
|
"label": "Current Address"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "sb53",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Personal Bio"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Short biography for website and other publications.",
|
"description": "Short biography for website and other publications.",
|
||||||
"fieldname": "bio",
|
"fieldname": "bio",
|
||||||
@ -508,7 +506,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "personal_details",
|
"fieldname": "personal_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Personal Details"
|
"label": "Personal Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -601,7 +599,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "exit",
|
"fieldname": "exit",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Exit",
|
"label": "Exit",
|
||||||
"oldfieldtype": "Section Break"
|
"oldfieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
@ -702,7 +700,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "attendance_and_leave_details",
|
"fieldname": "attendance_and_leave_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Attendance and Leave Details"
|
"label": "Attendance and Leave Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -713,10 +711,6 @@
|
|||||||
"fieldname": "column_break_19",
|
"fieldname": "column_break_19",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_52",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "salary_currency",
|
"fieldname": "salary_currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -728,13 +722,95 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Cost to Company (CTC)",
|
"label": "Cost to Company (CTC)",
|
||||||
"options": "salary_currency"
|
"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",
|
"icon": "fa fa-user",
|
||||||
"idx": 24,
|
"idx": 24,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-27 01:29:32.952091",
|
"modified": "2022-08-23 13:47:46.944993",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
|
@ -178,6 +178,7 @@ class DeliveryNote(SellingController):
|
|||||||
if (
|
if (
|
||||||
cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
|
cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate"))
|
||||||
and not self.is_return
|
and not self.is_return
|
||||||
|
and not self.is_internal_customer
|
||||||
):
|
):
|
||||||
self.validate_rate_with_reference_doc(
|
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",
|
"name": "delivery_note_item",
|
||||||
"batch_no": "batch_no",
|
"batch_no": "batch_no",
|
||||||
"serial_no": "serial_no",
|
"serial_no": "serial_no",
|
||||||
|
"purchase_order": "purchase_order",
|
||||||
|
"purchase_order_item": "purchase_order_item",
|
||||||
},
|
},
|
||||||
"field_no_map": ["warehouse"],
|
"field_no_map": ["warehouse"],
|
||||||
},
|
},
|
||||||
|
@ -86,6 +86,10 @@
|
|||||||
"expense_account",
|
"expense_account",
|
||||||
"allow_zero_valuation_rate",
|
"allow_zero_valuation_rate",
|
||||||
"column_break_71",
|
"column_break_71",
|
||||||
|
"internal_transfer_section",
|
||||||
|
"purchase_order",
|
||||||
|
"column_break_82",
|
||||||
|
"purchase_order_item",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@ -777,13 +781,39 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-17 05:25:47.711177",
|
"modified": "2022-09-06 14:19:42.876357",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
@ -30,6 +30,7 @@ frappe.ui.form.on('Inventory Dimension', {
|
|||||||
|
|
||||||
onload(frm) {
|
onload(frm) {
|
||||||
frm.trigger('render_traget_field');
|
frm.trigger('render_traget_field');
|
||||||
|
frm.trigger("set_parent_fields");
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
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) {
|
delete_dimension(frm) {
|
||||||
let msg = (`
|
let msg = (`
|
||||||
Custom fields related to this dimension will be deleted on deletion of dimension.
|
Custom fields related to this dimension will be deleted on deletion of dimension.
|
||||||
|
@ -144,16 +144,15 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "istable",
|
|
||||||
"description": "Set fieldname or DocType name like Supplier, Customer etc.",
|
"description": "Set fieldname or DocType name like Supplier, Customer etc.",
|
||||||
"fieldname": "fetch_from_parent",
|
"fieldname": "fetch_from_parent",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Select",
|
||||||
"label": "Fetch Value From Parent Form"
|
"label": "Fetch Value From Parent Form"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-08-17 11:43:24.722441",
|
"modified": "2022-09-02 13:29:04.098469",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Inventory Dimension",
|
"name": "Inventory Dimension",
|
||||||
|
@ -236,3 +236,30 @@ def get_inventory_dimensions():
|
|||||||
def delete_dimension(dimension):
|
def delete_dimension(dimension):
|
||||||
doc = frappe.get_doc("Inventory Dimension", dimension)
|
doc = frappe.get_doc("Inventory Dimension", dimension)
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_parent_fields(child_doctype, dimension_name):
|
||||||
|
parent_doctypes = frappe.get_all(
|
||||||
|
"DocField", fields=["parent"], filters={"options": child_doctype}
|
||||||
|
)
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
fields.extend(
|
||||||
|
frappe.get_all(
|
||||||
|
"DocField",
|
||||||
|
fields=["fieldname as value", "label"],
|
||||||
|
filters={"options": dimension_name, "parent": ("in", [d.parent for d in parent_doctypes])},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fields.extend(
|
||||||
|
frappe.get_all(
|
||||||
|
"Custom Field",
|
||||||
|
fields=["fieldname as value", "label"],
|
||||||
|
filters={"options": dimension_name, "dt": ("in", [d.parent for d in parent_doctypes])},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return fields
|
||||||
|
@ -2,14 +2,17 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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 (
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||||
CanNotBeChildDoc,
|
CanNotBeChildDoc,
|
||||||
CanNotBeDefaultDimension,
|
CanNotBeDefaultDimension,
|
||||||
DoNotChangeError,
|
DoNotChangeError,
|
||||||
delete_dimension,
|
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.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
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.assertTrue(inv_dim1.has_stock_ledger())
|
||||||
self.assertRaises(DoNotChangeError, inv_dim1.save)
|
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():
|
def prepare_test_data():
|
||||||
if not frappe.db.exists("DocType", "Shelf"):
|
if not frappe.db.exists("DocType", "Shelf"):
|
||||||
@ -160,6 +215,28 @@ def prepare_test_data():
|
|||||||
|
|
||||||
create_warehouse("Shelf Warehouse")
|
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):
|
def create_inventory_dimension(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -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",
|
||||||
|
@ -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")
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Barcode",
|
"label": "Barcode",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"reqd": 1,
|
||||||
"unique": 1
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-06-01 06:24:33.969534",
|
"modified": "2022-08-24 19:59:47.871677",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Barcode",
|
"name": "Item Barcode",
|
||||||
|
@ -1,95 +1,43 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2013-02-22 01:28:01",
|
"creation": "2013-02-22 01:28:01",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"supplier",
|
||||||
|
"supplier_part_no"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Supplier",
|
"label": "Supplier",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Supplier",
|
"options": "Supplier",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "supplier_part_no",
|
"fieldname": "supplier_part_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Supplier Part Number",
|
"label": "Supplier Part Number",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "200px",
|
"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,
|
|
||||||
"width": "200px"
|
"width": "200px"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2017-02-20 13:29:32.569715",
|
"modified": "2022-09-07 12:33:55.780062",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Supplier",
|
"name": "Item Supplier",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC",
|
||||||
"read_only_onload": 0,
|
"states": [],
|
||||||
"show_name_in_global_search": 0,
|
"track_changes": 1
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
@ -183,7 +183,7 @@ class PickList(Document):
|
|||||||
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
||||||
item_code = item.item_code
|
item_code = item.item_code
|
||||||
reference = item.sales_order_item or item.material_request_item
|
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.idx = None
|
||||||
item.name = None
|
item.name = None
|
||||||
|
@ -362,6 +362,12 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if credit_currency == self.company_currency
|
if credit_currency == self.company_currency
|
||||||
else flt(d.net_amount, d.precision("net_amount"))
|
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:
|
if credit_amount:
|
||||||
account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
|
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,
|
gl_entries=gl_entries,
|
||||||
account=account,
|
account=account,
|
||||||
cost_center=d.cost_center,
|
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,
|
credit=0.0,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
against_account=warehouse_account_name,
|
against_account=warehouse_account_name,
|
||||||
@ -456,7 +462,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
# divisional loss adjustment
|
# divisional loss adjustment
|
||||||
valuation_amount_as_per_doc = (
|
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.landed_cost_voucher_amount)
|
||||||
+ flt(d.rm_supp_cost)
|
+ flt(d.rm_supp_cost)
|
||||||
+ flt(d.item_tax_amount)
|
+ flt(d.item_tax_amount)
|
||||||
@ -631,47 +637,6 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
i += 1
|
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):
|
def get_asset_gl_entry(self, gl_entries):
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.is_fixed_asset:
|
if item.is_fixed_asset:
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, cint, cstr, flt, today
|
from frappe.utils import add_days, cint, cstr, flt, today
|
||||||
|
from pypika import functions as fn
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||||
@ -1156,6 +1157,125 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
if gle.account == account:
|
if gle.account == account:
|
||||||
self.assertEqual(gle.credit, 50)
|
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):
|
def get_sl_entries(voucher_type, voucher_no):
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
|
@ -58,6 +58,21 @@ frappe.ui.form.on('Repost Item Valuation', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frm.trigger('show_reposting_progress');
|
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) {
|
show_reposting_progress: function(frm) {
|
||||||
|
@ -307,3 +307,9 @@ def in_configured_timeslot(repost_settings=None, current_time=None):
|
|||||||
return end_time >= now_time >= start_time
|
return end_time >= now_time >= start_time
|
||||||
else:
|
else:
|
||||||
return now_time >= start_time or now_time <= end_time
|
return now_time >= start_time or now_time <= end_time
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def execute_repost_item_valuation():
|
||||||
|
"""Execute repost item valuation via scheduler."""
|
||||||
|
frappe.get_doc("Scheduled Job Type", "repost_item_valuation.repost_entries").enqueue(force=True)
|
||||||
|
@ -815,7 +815,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
|||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"company": me.frm.doc.company
|
"company": me.frm.doc.company,
|
||||||
|
"status": ["not in", ["Completed", "Closed"]]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -117,6 +117,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_work_order()
|
self.validate_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()
|
||||||
@ -875,25 +876,24 @@ class StockEntry(StockController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
parent = frappe.qb.DocType("Stock Entry")
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
child = frappe.qb.DocType("Stock Entry Detail")
|
se_detail = 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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
total_supplied = (
|
total_supplied = (
|
||||||
frappe.qb.from_(parent)
|
frappe.qb.from_(se)
|
||||||
.inner_join(child)
|
.inner_join(se_detail)
|
||||||
.on(parent.name == child.parent)
|
.on(se.name == se_detail.parent)
|
||||||
.select(Sum(child.transfer_qty))
|
.select(Sum(se_detail.transfer_qty))
|
||||||
.where(conditions)
|
.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]
|
).run()[0][0]
|
||||||
|
|
||||||
if flt(total_supplied, precision) > flt(total_allowed, precision):
|
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):
|
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
|
||||||
|
@ -649,21 +649,25 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
elif (
|
elif (
|
||||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
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")
|
and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
|
||||||
):
|
):
|
||||||
sle_details = frappe.db.get_value(
|
field = (
|
||||||
"Stock Ledger Entry",
|
"delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
|
||||||
{
|
)
|
||||||
"voucher_type": sle.voucher_type,
|
doctype = (
|
||||||
"voucher_no": sle.voucher_no,
|
"Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
|
||||||
"dependant_sle_voucher_detail_no": sle.voucher_detail_no,
|
)
|
||||||
},
|
refernce_name = frappe.get_cached_value(
|
||||||
["stock_value_difference", "actual_qty"],
|
sle.voucher_type + " Item", sle.voucher_detail_no, field
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rate = abs(sle_details.stock_value_difference / sle.actual_qty)
|
if refernce_name:
|
||||||
|
rate = frappe.get_cached_value(
|
||||||
|
doctype,
|
||||||
|
refernce_name,
|
||||||
|
"incoming_rate",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||||
rate_field = "valuation_rate"
|
rate_field = "valuation_rate"
|
||||||
@ -745,7 +749,12 @@ class update_entries_after(object):
|
|||||||
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
|
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
|
||||||
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
||||||
frappe.db.set_value(
|
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:
|
else:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', {
|
|||||||
get_materials_from_supplier: function (frm) {
|
get_materials_from_supplier: function (frm) {
|
||||||
let sco_rm_details = [];
|
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 => {
|
frm.doc.supplied_items.forEach(d => {
|
||||||
if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
|
if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
|
||||||
sco_rm_details.push(d.name);
|
sco_rm_details.push(d.name);
|
||||||
|
@ -187,22 +187,13 @@ class TestSubcontractingOrder(FrappeTestCase):
|
|||||||
self.assertEqual(len(ste.items), len(rm_items))
|
self.assertEqual(len(ste.items), len(rm_items))
|
||||||
|
|
||||||
def test_update_reserved_qty_for_subcontracting(self):
|
def test_update_reserved_qty_for_subcontracting(self):
|
||||||
# Make stock available for raw materials
|
# Create RM Material Receipt
|
||||||
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100)
|
||||||
make_stock_entry(
|
make_stock_entry(
|
||||||
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
|
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",
|
"Bin",
|
||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
||||||
@ -222,102 +213,97 @@ class TestSubcontractingOrder(FrappeTestCase):
|
|||||||
]
|
]
|
||||||
sco = get_subcontracting_order(service_items=service_items)
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
|
||||||
bin2 = frappe.db.get_value(
|
bin_after_sco = frappe.db.get_value(
|
||||||
"Bin",
|
"Bin",
|
||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
# reserved_qty_for_sub_contract should be increased by 10
|
||||||
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
|
self.assertEqual(
|
||||||
self.assertNotEqual(bin1.modified, bin2.modified)
|
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 = [
|
rm_items = [
|
||||||
{
|
{
|
||||||
"item_code": "_Test FG Item",
|
"item_code": "_Test FG Item",
|
||||||
"rm_item_code": "_Test Item",
|
"rm_item_code": "_Test Item",
|
||||||
"item_name": "_Test Item",
|
"item_name": "_Test Item",
|
||||||
"qty": 6,
|
"qty": 10,
|
||||||
"warehouse": "_Test Warehouse - _TC",
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
"rate": 100,
|
"rate": 100,
|
||||||
"amount": 600,
|
"amount": 1000,
|
||||||
"stock_uom": "Nos",
|
"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 = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||||
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||||
ste.save()
|
ste.save()
|
||||||
ste.submit()
|
ste.submit()
|
||||||
|
|
||||||
bin3 = frappe.db.get_value(
|
bin_after_rm_transfer = frappe.db.get_value(
|
||||||
"Bin",
|
"Bin",
|
||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract",
|
fieldname="reserved_qty_for_sub_contract",
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
# reserved_qty_for_sub_contract should be decreased by 10
|
||||||
|
self.assertEqual(
|
||||||
make_stock_entry(
|
bin_after_rm_transfer.reserved_qty_for_sub_contract,
|
||||||
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
|
bin_after_sco.reserved_qty_for_sub_contract - 10,
|
||||||
)
|
|
||||||
make_stock_entry(
|
|
||||||
target="_Test Warehouse 1 - _TC",
|
|
||||||
item_code="_Test Item Home Desktop 100",
|
|
||||||
qty=40,
|
|
||||||
basic_rate=100,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make SCR against the SCO
|
# Cancel Stock Entry(Send to Subcontractor)
|
||||||
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
|
|
||||||
ste.cancel()
|
ste.cancel()
|
||||||
bin6 = frappe.db.get_value(
|
bin_after_cancel_ste = frappe.db.get_value(
|
||||||
"Bin",
|
"Bin",
|
||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract",
|
fieldname="reserved_qty_for_sub_contract",
|
||||||
as_dict=1,
|
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.reload()
|
||||||
sco.cancel()
|
sco.cancel()
|
||||||
bin7 = frappe.db.get_value(
|
bin_after_cancel_sco = frappe.db.get_value(
|
||||||
"Bin",
|
"Bin",
|
||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract",
|
fieldname="reserved_qty_for_sub_contract",
|
||||||
as_dict=1,
|
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):
|
def test_exploded_items(self):
|
||||||
item_code = "_Test Subcontracted FG Item 11"
|
item_code = "_Test Subcontracted FG Item 11"
|
||||||
@ -516,6 +502,35 @@ class TestSubcontractingOrder(FrappeTestCase):
|
|||||||
|
|
||||||
set_backflush_based_on("BOM")
|
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):
|
def create_subcontracting_order(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
@ -524,7 +539,7 @@ def create_subcontracting_order(**args):
|
|||||||
for item in sco.items:
|
for item in sco.items:
|
||||||
item.include_exploded_items = args.get("include_exploded_items", 1)
|
item.include_exploded_items = args.get("include_exploded_items", 1)
|
||||||
|
|
||||||
if args.get("warehouse"):
|
if args.warehouse:
|
||||||
for item in sco.items:
|
for item in sco.items:
|
||||||
item.warehouse = args.warehouse
|
item.warehouse = args.warehouse
|
||||||
else:
|
else:
|
||||||
|
@ -5,6 +5,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt, getdate, nowdate
|
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
|
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
self.get_current_stock()
|
self.get_current_stock()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.validate_available_qty_for_consumption()
|
||||||
self.update_status_updater_args()
|
self.update_status_updater_args()
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.set_subcontracting_order_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_supplied_items()
|
||||||
self.set_missing_values_in_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):
|
def set_missing_values_in_supplied_items(self):
|
||||||
for item in self.get("supplied_items") or []:
|
for item in self.get("supplied_items") or []:
|
||||||
item.amount = item.rate * item.consumed_qty
|
item.amount = item.rate * item.consumed_qty
|
||||||
|
|
||||||
|
self.set_available_qty_for_consumption()
|
||||||
|
|
||||||
def set_missing_values_in_items(self):
|
def set_missing_values_in_items(self):
|
||||||
rm_supp_cost = {}
|
rm_supp_cost = {}
|
||||||
for item in self.get("supplied_items") or []:
|
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)
|
_("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):
|
def set_items_cost_center(self):
|
||||||
if self.company:
|
if self.company:
|
||||||
cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||||
@ -181,6 +227,137 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
if status:
|
if status:
|
||||||
frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
|
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()
|
@frappe.whitelist()
|
||||||
def make_subcontract_return(source_name, target_doc=None):
|
def make_subcontract_return(source_name, target_doc=None):
|
||||||
|
@ -6,8 +6,10 @@ import copy
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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.sales_and_purchase_return import make_return_doc
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
get_rm_items,
|
get_rm_items,
|
||||||
@ -22,6 +24,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import (
|
|||||||
set_backflush_based_on,
|
set_backflush_based_on,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
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.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||||
make_subcontracting_receipt,
|
make_subcontracting_receipt,
|
||||||
@ -70,6 +73,55 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
rm_supp_cost = sum(item.amount for item in scr.get("supplied_items"))
|
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))
|
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):
|
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
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)
|
args = frappe._dict(scr_name=scr1.name, qty=-15)
|
||||||
self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args)
|
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):
|
def make_return_subcontracting_receipt(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"col_break2",
|
"col_break2",
|
||||||
"amount",
|
"amount",
|
||||||
"secbreak_2",
|
"secbreak_2",
|
||||||
|
"available_qty_for_consumption",
|
||||||
"required_qty",
|
"required_qty",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"consumed_qty",
|
"consumed_qty",
|
||||||
@ -75,8 +76,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "required_qty",
|
"fieldname": "required_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"label": "Required Qty",
|
||||||
"label": "Available Qty For Consumption",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -85,7 +85,7 @@
|
|||||||
"fieldname": "consumed_qty",
|
"fieldname": "consumed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Qty to be Consumed",
|
"label": "Consumed Qty",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -179,12 +179,21 @@
|
|||||||
"options": "Subcontracting Order",
|
"options": "Subcontracting Order",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-18 10:45:16.538479",
|
"modified": "2022-09-02 22:28:53.392381",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Subcontracting",
|
"module": "Subcontracting",
|
||||||
"name": "Subcontracting Receipt Supplied Item",
|
"name": "Subcontracting Receipt Supplied Item",
|
||||||
@ -193,6 +202,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"states": [],
|
||||||
"states": []
|
"track_changes": 1
|
||||||
}
|
}
|
@ -71,6 +71,9 @@ class TransactionBase(StatusUpdater):
|
|||||||
self.validate_value(field, condition, prevdoc_values[field], doc)
|
self.validate_value(field, condition, prevdoc_values[field], doc)
|
||||||
|
|
||||||
def validate_rate_with_reference_doc(self, ref_details):
|
def validate_rate_with_reference_doc(self, ref_details):
|
||||||
|
if self.get("is_internal_supplier"):
|
||||||
|
return
|
||||||
|
|
||||||
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
||||||
|
|
||||||
if self.doctype in buying_doctypes:
|
if self.doctype in buying_doctypes:
|
||||||
|
597
license.txt
597
license.txt
@ -1,113 +1,107 @@
|
|||||||
### GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Version 3, 29 June 2007
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
Preamble
|
||||||
<http://fsf.org/>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
The GNU General Public License is a free, copyleft license for
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
### Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
software and other kinds of works.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
to share and change all versions of a program--to make sure it remains
|
share and change all versions of a program--to make sure it remains free
|
||||||
free software for all its users. We, the Free Software Foundation, use
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
the GNU General Public License for most of our software; it applies
|
GNU General Public License for most of our software; it applies also to
|
||||||
also to any other work released this way by its authors. You can apply
|
any other work released this way by its authors. You can apply it to
|
||||||
it to your programs, too.
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
have the freedom to distribute copies of free software (and charge for
|
have the freedom to distribute copies of free software (and charge for
|
||||||
them if you wish), that you receive source code or can get it if you
|
them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
To protect your rights, we need to prevent others from denying you
|
||||||
these rights or asking you to surrender the rights. Therefore, you
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
have certain responsibilities if you distribute copies of the
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
software, or if you modify it: responsibilities to respect the freedom
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
For example, if you distribute copies of such a program, whether
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
or can get the source code. And you must show them these terms so they
|
or can get the source code. And you must show them these terms so they
|
||||||
know their rights.
|
know their rights.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
that there is no warranty for this free software. For both users' and
|
that there is no warranty for this free software. For both users' and
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
changed, so that their problems will not be attributed erroneously to
|
changed, so that their problems will not be attributed erroneously to
|
||||||
authors of previous versions.
|
authors of previous versions.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
Some devices are designed to deny users access to install or run
|
||||||
modified versions of the software inside them, although the
|
modified versions of the software inside them, although the manufacturer
|
||||||
manufacturer can do so. This is fundamentally incompatible with the
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
aim of protecting users' freedom to change the software. The
|
protecting users' freedom to change the software. The systematic
|
||||||
systematic pattern of such abuse occurs in the area of products for
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
individuals to use, which is precisely where it is most unacceptable.
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
Therefore, we have designed this version of the GPL to prohibit the
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
practice for those products. If such problems arise substantially in
|
products. If such problems arise substantially in other domains, we
|
||||||
other domains, we stand ready to extend this provision to those
|
stand ready to extend this provision to those domains in future versions
|
||||||
domains in future versions of the GPL, as needed to protect the
|
of the GPL, as needed to protect the freedom of users.
|
||||||
freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
Finally, every program is threatened constantly by software patents.
|
||||||
States should not allow patents to restrict development and use of
|
States should not allow patents to restrict development and use of
|
||||||
software on general-purpose computers, but in those that do, we wish
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
to avoid the special danger that patents applied to a free program
|
avoid the special danger that patents applied to a free program could
|
||||||
could make it effectively proprietary. To prevent this, the GPL
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
assures that patents cannot be used to render the program non-free.
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
||||||
### TERMS AND CONDITIONS
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
#### 0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
of works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
in a fashion requiring copyright permission, other than the making of
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
an exact copy. The resulting work is called a "modified version" of
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
the earlier work or a work "based on" the earlier work.
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
A "covered work" means either the unmodified Program or a work based
|
||||||
on the Program.
|
on the Program.
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
To "propagate" a work means to do anything with it that, without
|
||||||
permission, would make you directly or secondarily liable for
|
permission, would make you directly or secondarily liable for
|
||||||
infringement under applicable copyright law, except executing it on a
|
infringement under applicable copyright law, except executing it on a
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
distribution (with or without modification), making available to the
|
distribution (with or without modification), making available to the
|
||||||
public, and in some countries other activities as well.
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
To "convey" a work means any kind of propagation that enables other
|
||||||
parties to make or receive copies. Mere interaction with a user
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
through a computer network, with no transfer of a copy, is not
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices" to
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently visible
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
@ -115,18 +109,18 @@ work under this License, and how to view a copy of this License. If
|
|||||||
the interface presents a list of user commands or options, such as a
|
the interface presents a list of user commands or options, such as a
|
||||||
menu, a prominent item in the list meets this criterion.
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
#### 1. Source Code.
|
1. Source Code.
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work for
|
The "source code" for a work means the preferred form of the work
|
||||||
making modifications to it. "Object code" means any non-source form of
|
for making modifications to it. "Object code" means any non-source
|
||||||
a work.
|
form of a work.
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
A "Standard Interface" means an interface that either is an official
|
||||||
standard defined by a recognized standards body, or, in the case of
|
standard defined by a recognized standards body, or, in the case of
|
||||||
interfaces specified for a particular programming language, one that
|
interfaces specified for a particular programming language, one that
|
||||||
is widely used among developers working in that language.
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
The "System Libraries" of an executable work include anything, other
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
packaging a Major Component, but which is not part of that Major
|
packaging a Major Component, but which is not part of that Major
|
||||||
Component, and (b) serves only to enable use of the work with that
|
Component, and (b) serves only to enable use of the work with that
|
||||||
@ -137,7 +131,7 @@ implementation is available to the public in source code form. A
|
|||||||
(if any) on which the executable work runs, or a compiler used to
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
produce the work, or an object code interpreter used to run it.
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
The "Corresponding Source" for a work in object code form means all
|
||||||
the source code needed to generate, install, and (for an executable
|
the source code needed to generate, install, and (for an executable
|
||||||
work) run the object code and to modify the work, including scripts to
|
work) run the object code and to modify the work, including scripts to
|
||||||
control those activities. However, it does not include the work's
|
control those activities. However, it does not include the work's
|
||||||
@ -150,15 +144,16 @@ linked subprograms that the work is specifically designed to require,
|
|||||||
such as by intimate data communication or control flow between those
|
such as by intimate data communication or control flow between those
|
||||||
subprograms and other parts of the work.
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users can
|
The Corresponding Source need not include anything that users
|
||||||
regenerate automatically from other parts of the Corresponding Source.
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that same
|
The Corresponding Source for a work in source code form is that
|
||||||
work.
|
same work.
|
||||||
|
|
||||||
#### 2. Basic Permissions.
|
2. Basic Permissions.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
All rights granted under this License are granted for the term of
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
permission to run the unmodified Program. The output from running a
|
permission to run the unmodified Program. The output from running a
|
||||||
@ -166,40 +161,40 @@ covered work is covered by this License only if the output, given its
|
|||||||
content, constitutes a covered work. This License acknowledges your
|
content, constitutes a covered work. This License acknowledges your
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not convey,
|
You may make, run and propagate covered works that you do not
|
||||||
without conditions so long as your license otherwise remains in force.
|
convey, without conditions so long as your license otherwise remains
|
||||||
You may convey covered works to others for the sole purpose of having
|
in force. You may convey covered works to others for the sole purpose
|
||||||
them make modifications exclusively for you, or provide you with
|
of having them make modifications exclusively for you, or provide you
|
||||||
facilities for running those works, provided that you comply with the
|
with facilities for running those works, provided that you comply with
|
||||||
terms of this License in conveying all material for which you do not
|
the terms of this License in conveying all material for which you do
|
||||||
control copyright. Those thus making or running the covered works for
|
not control copyright. Those thus making or running the covered works
|
||||||
you must do so exclusively on your behalf, under your direction and
|
for you must do so exclusively on your behalf, under your direction
|
||||||
control, on terms that prohibit them from making any copies of your
|
and control, on terms that prohibit them from making any copies of
|
||||||
copyrighted material outside their relationship with you.
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under the
|
Conveying under any other circumstances is permitted solely under
|
||||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
it unnecessary.
|
makes it unnecessary.
|
||||||
|
|
||||||
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
No covered work shall be deemed part of an effective technological
|
||||||
measure under any applicable law fulfilling obligations under article
|
measure under any applicable law fulfilling obligations under article
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
similar laws prohibiting or restricting circumvention of such
|
similar laws prohibiting or restricting circumvention of such
|
||||||
measures.
|
measures.
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
circumvention of technological measures to the extent such
|
circumvention of technological measures to the extent such circumvention
|
||||||
circumvention is effected by exercising rights under this License with
|
is effected by exercising rights under this License with respect to
|
||||||
respect to the covered work, and you disclaim any intention to limit
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
operation or modification of the work as a means of enforcing, against
|
modification of the work as a means of enforcing, against the work's
|
||||||
the work's users, your or third parties' legal rights to forbid
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
circumvention of technological measures.
|
technological measures.
|
||||||
|
|
||||||
#### 4. Conveying Verbatim Copies.
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
You may convey verbatim copies of the Program's source code as you
|
||||||
receive it, in any medium, provided that you conspicuously and
|
receive it, in any medium, provided that you conspicuously and
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
keep intact all notices stating that this License and any
|
keep intact all notices stating that this License and any
|
||||||
@ -207,35 +202,37 @@ non-permissive terms added in accord with section 7 apply to the code;
|
|||||||
keep intact all notices of the absence of any warranty; and give all
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
recipients a copy of this License along with the Program.
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
You may charge any price or no price for each copy that you convey,
|
||||||
and you may offer support or warranty protection for a fee.
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
#### 5. Conveying Modified Source Versions.
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
You may convey a work based on the Program, or the modifications to
|
||||||
produce it from the Program, in the form of source code under the
|
produce it from the Program, in the form of source code under the
|
||||||
terms of section 4, provided that you also meet all of these
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
conditions:
|
|
||||||
|
|
||||||
- a) The work must carry prominent notices stating that you modified
|
a) The work must carry prominent notices stating that you modified
|
||||||
it, and giving a relevant date.
|
it, and giving a relevant date.
|
||||||
- b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under
|
b) The work must carry prominent notices stating that it is
|
||||||
section 7. This requirement modifies the requirement in section 4
|
released under this License and any conditions added under section
|
||||||
to "keep intact all notices".
|
7. This requirement modifies the requirement in section 4 to
|
||||||
- c) You must license the entire work, as a whole, under this
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
License to anyone who comes into possession of a copy. This
|
License to anyone who comes into possession of a copy. This
|
||||||
License will therefore apply, along with any applicable section 7
|
License will therefore apply, along with any applicable section 7
|
||||||
additional terms, to the whole of the work, and all its parts,
|
additional terms, to the whole of the work, and all its parts,
|
||||||
regardless of how they are packaged. This License gives no
|
regardless of how they are packaged. This License gives no
|
||||||
permission to license the work in any other way, but it does not
|
permission to license the work in any other way, but it does not
|
||||||
invalidate such permission if you have separately received it.
|
invalidate such permission if you have separately received it.
|
||||||
- d) If the work has interactive user interfaces, each must display
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
work need not make them do so.
|
work need not make them do so.
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
A compilation of a covered work with other separate and independent
|
||||||
works, which are not by their nature extensions of the covered work,
|
works, which are not by their nature extensions of the covered work,
|
||||||
and which are not combined with it such as to form a larger program,
|
and which are not combined with it such as to form a larger program,
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
@ -245,18 +242,19 @@ beyond what the individual works permit. Inclusion of a covered work
|
|||||||
in an aggregate does not cause this License to apply to the other
|
in an aggregate does not cause this License to apply to the other
|
||||||
parts of the aggregate.
|
parts of the aggregate.
|
||||||
|
|
||||||
#### 6. Conveying Non-Source Forms.
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms of
|
You may convey a covered work in object code form under the terms
|
||||||
sections 4 and 5, provided that you also convey the machine-readable
|
of sections 4 and 5, provided that you also convey the
|
||||||
Corresponding Source under the terms of this License, in one of these
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
ways:
|
in one of these ways:
|
||||||
|
|
||||||
- a) Convey the object code in, or embodied in, a physical product
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by the
|
(including a physical distribution medium), accompanied by the
|
||||||
Corresponding Source fixed on a durable physical medium
|
Corresponding Source fixed on a durable physical medium
|
||||||
customarily used for software interchange.
|
customarily used for software interchange.
|
||||||
- b) Convey the object code in, or embodied in, a physical product
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
(including a physical distribution medium), accompanied by a
|
(including a physical distribution medium), accompanied by a
|
||||||
written offer, valid for at least three years and valid for as
|
written offer, valid for at least three years and valid for as
|
||||||
long as you offer spare parts or customer support for that product
|
long as you offer spare parts or customer support for that product
|
||||||
@ -265,14 +263,16 @@ ways:
|
|||||||
product that is covered by this License, on a durable physical
|
product that is covered by this License, on a durable physical
|
||||||
medium customarily used for software interchange, for a price no
|
medium customarily used for software interchange, for a price no
|
||||||
more than your reasonable cost of physically performing this
|
more than your reasonable cost of physically performing this
|
||||||
conveying of source, or (2) access to copy the Corresponding
|
conveying of source, or (2) access to copy the
|
||||||
Source from a network server at no charge.
|
Corresponding Source from a network server at no charge.
|
||||||
- c) Convey individual copies of the object code with a copy of the
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
written offer to provide the Corresponding Source. This
|
written offer to provide the Corresponding Source. This
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
only if you received the object code with such an offer, in accord
|
only if you received the object code with such an offer, in accord
|
||||||
with subsection 6b.
|
with subsection 6b.
|
||||||
- d) Convey the object code by offering access from a designated
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
Corresponding Source in the same way through the same place at no
|
Corresponding Source in the same way through the same place at no
|
||||||
further charge. You need not require recipients to copy the
|
further charge. You need not require recipients to copy the
|
||||||
@ -284,38 +284,38 @@ ways:
|
|||||||
Corresponding Source. Regardless of what server hosts the
|
Corresponding Source. Regardless of what server hosts the
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
available for as long as needed to satisfy these requirements.
|
available for as long as needed to satisfy these requirements.
|
||||||
- e) Convey the object code using peer-to-peer transmission,
|
|
||||||
provided you inform other peers where the object code and
|
|
||||||
Corresponding Source of the work are being offered to the general
|
|
||||||
public at no charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
from the Corresponding Source as a System Library, need not be
|
from the Corresponding Source as a System Library, need not be
|
||||||
included in conveying the object code work.
|
included in conveying the object code work.
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
tangible personal property which is normally used for personal,
|
tangible personal property which is normally used for personal, family,
|
||||||
family, or household purposes, or (2) anything designed or sold for
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
incorporation into a dwelling. In determining whether a product is a
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
consumer product, doubtful cases shall be resolved in favor of
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
coverage. For a particular product received by a particular user,
|
product received by a particular user, "normally used" refers to a
|
||||||
"normally used" refers to a typical or common use of that class of
|
typical or common use of that class of product, regardless of the status
|
||||||
product, regardless of the status of the particular user or of the way
|
of the particular user or of the way in which the particular user
|
||||||
in which the particular user actually uses, or expects or is expected
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
to use, the product. A product is a consumer product regardless of
|
is a consumer product regardless of whether the product has substantial
|
||||||
whether the product has substantial commercial, industrial or
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
non-consumer uses, unless such uses represent the only significant
|
the only significant mode of use of the product.
|
||||||
mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
"Installation Information" for a User Product means any methods,
|
||||||
procedures, authorization keys, or other information required to
|
procedures, authorization keys, or other information required to install
|
||||||
install and execute modified versions of a covered work in that User
|
and execute modified versions of a covered work in that User Product from
|
||||||
Product from a modified version of its Corresponding Source. The
|
a modified version of its Corresponding Source. The information must
|
||||||
information must suffice to ensure that the continued functioning of
|
suffice to ensure that the continued functioning of the modified object
|
||||||
the modified object code is in no case prevented or interfered with
|
code is in no case prevented or interfered with solely because
|
||||||
solely because modification has been made.
|
modification has been made.
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
If you convey an object code work under this section in, or with, or
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
part of a transaction in which the right of possession and use of the
|
part of a transaction in which the right of possession and use of the
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
@ -326,24 +326,23 @@ if neither you nor any third party retains the ability to install
|
|||||||
modified object code on the User Product (for example, the work has
|
modified object code on the User Product (for example, the work has
|
||||||
been installed in ROM).
|
been installed in ROM).
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
The requirement to provide Installation Information does not include a
|
||||||
requirement to continue to provide support service, warranty, or
|
requirement to continue to provide support service, warranty, or updates
|
||||||
updates for a work that has been modified or installed by the
|
for a work that has been modified or installed by the recipient, or for
|
||||||
recipient, or for the User Product in which it has been modified or
|
the User Product in which it has been modified or installed. Access to a
|
||||||
installed. Access to a network may be denied when the modification
|
network may be denied when the modification itself materially and
|
||||||
itself materially and adversely affects the operation of the network
|
adversely affects the operation of the network or violates the rules and
|
||||||
or violates the rules and protocols for communication across the
|
protocols for communication across the network.
|
||||||
network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
in accord with this section must be in a format that is publicly
|
in accord with this section must be in a format that is publicly
|
||||||
documented (and with an implementation available to the public in
|
documented (and with an implementation available to the public in
|
||||||
source code form), and must require no special password or key for
|
source code form), and must require no special password or key for
|
||||||
unpacking, reading or copying.
|
unpacking, reading or copying.
|
||||||
|
|
||||||
#### 7. Additional Terms.
|
7. Additional Terms.
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
License by making exceptions from one or more of its conditions.
|
License by making exceptions from one or more of its conditions.
|
||||||
Additional permissions that are applicable to the entire Program shall
|
Additional permissions that are applicable to the entire Program shall
|
||||||
be treated as though they were included in this License, to the extent
|
be treated as though they were included in this License, to the extent
|
||||||
@ -352,36 +351,41 @@ apply only to part of the Program, that part may be used separately
|
|||||||
under those permissions, but the entire Program remains governed by
|
under those permissions, but the entire Program remains governed by
|
||||||
this License without regard to the additional permissions.
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
When you convey a copy of a covered work, you may at your option
|
||||||
remove any additional permissions from that copy, or from any part of
|
remove any additional permissions from that copy, or from any part of
|
||||||
it. (Additional permissions may be written to require their own
|
it. (Additional permissions may be written to require their own
|
||||||
removal in certain cases when you modify the work.) You may place
|
removal in certain cases when you modify the work.) You may place
|
||||||
additional permissions on material, added by you to a covered work,
|
additional permissions on material, added by you to a covered work,
|
||||||
for which you have or can give appropriate copyright permission.
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
Notwithstanding any other provision of this License, for material you
|
||||||
add to a covered work, you may (if authorized by the copyright holders
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
of that material) supplement the terms of this License with terms:
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
- a) Disclaiming warranty or limiting liability differently from the
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
terms of sections 15 and 16 of this License; or
|
terms of sections 15 and 16 of this License; or
|
||||||
- b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
author attributions in that material or in the Appropriate Legal
|
author attributions in that material or in the Appropriate Legal
|
||||||
Notices displayed by works containing it; or
|
Notices displayed by works containing it; or
|
||||||
- c) Prohibiting misrepresentation of the origin of that material,
|
|
||||||
or requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
- d) Limiting the use for publicity purposes of names of licensors
|
|
||||||
or authors of the material; or
|
|
||||||
- e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
- f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions
|
|
||||||
of it) with contractual assumptions of liability to the recipient,
|
|
||||||
for any liability that these contractual assumptions directly
|
|
||||||
impose on those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
received it, or any part of it, contains a notice stating that it is
|
received it, or any part of it, contains a notice stating that it is
|
||||||
governed by this License along with a term that is a further
|
governed by this License along with a term that is a further
|
||||||
@ -391,47 +395,47 @@ License, you may add to a covered work material governed by the terms
|
|||||||
of that license document, provided that the further restriction does
|
of that license document, provided that the further restriction does
|
||||||
not survive such relicensing or conveying.
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
If you add terms to a covered work in accord with this section, you
|
||||||
must place, in the relevant source files, a statement of the
|
must place, in the relevant source files, a statement of the
|
||||||
additional terms that apply to those files, or a notice indicating
|
additional terms that apply to those files, or a notice indicating
|
||||||
where to find the applicable terms.
|
where to find the applicable terms.
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
form of a separately written license, or stated as exceptions; the
|
form of a separately written license, or stated as exceptions;
|
||||||
above requirements apply either way.
|
the above requirements apply either way.
|
||||||
|
|
||||||
#### 8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
You may not propagate or modify a covered work except as expressly
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
modify it is void, and will automatically terminate your rights under
|
modify it is void, and will automatically terminate your rights under
|
||||||
this License (including any patent licenses granted under the third
|
this License (including any patent licenses granted under the third
|
||||||
paragraph of section 11).
|
paragraph of section 11).
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your license
|
However, if you cease all violation of this License, then your
|
||||||
from a particular copyright holder is reinstated (a) provisionally,
|
license from a particular copyright holder is reinstated (a)
|
||||||
unless and until the copyright holder explicitly and finally
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
terminates your license, and (b) permanently, if the copyright holder
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
fails to notify you of the violation by some reasonable means prior to
|
holder fails to notify you of the violation by some reasonable means
|
||||||
60 days after the cessation.
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
Moreover, your license from a particular copyright holder is
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
violation by some reasonable means, this is the first time you have
|
violation by some reasonable means, this is the first time you have
|
||||||
received notice of violation of this License (for any work) from that
|
received notice of violation of this License (for any work) from that
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
your receipt of the notice.
|
your receipt of the notice.
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
Termination of your rights under this section does not terminate the
|
||||||
licenses of parties who have received copies or rights from you under
|
licenses of parties who have received copies or rights from you under
|
||||||
this License. If your rights have been terminated and not permanently
|
this License. If your rights have been terminated and not permanently
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
material under section 10.
|
material under section 10.
|
||||||
|
|
||||||
#### 9. Acceptance Not Required for Having Copies.
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or run
|
You are not required to accept this License in order to receive or
|
||||||
a copy of the Program. Ancillary propagation of a covered work
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
to receive a copy likewise does not require acceptance. However,
|
to receive a copy likewise does not require acceptance. However,
|
||||||
nothing other than this License grants you permission to propagate or
|
nothing other than this License grants you permission to propagate or
|
||||||
@ -439,14 +443,14 @@ modify any covered work. These actions infringe copyright if you do
|
|||||||
not accept this License. Therefore, by modifying or propagating a
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
#### 10. Automatic Licensing of Downstream Recipients.
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
Each time you convey a covered work, the recipient automatically
|
||||||
receives a license from the original licensors, to run, modify and
|
receives a license from the original licensors, to run, modify and
|
||||||
propagate that work, subject to this License. You are not responsible
|
propagate that work, subject to this License. You are not responsible
|
||||||
for enforcing compliance by third parties with this License.
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
An "entity transaction" is a transaction transferring control of an
|
||||||
organization, or substantially all assets of one, or subdividing an
|
organization, or substantially all assets of one, or subdividing an
|
||||||
organization, or merging organizations. If propagation of a covered
|
organization, or merging organizations. If propagation of a covered
|
||||||
work results from an entity transaction, each party to that
|
work results from an entity transaction, each party to that
|
||||||
@ -456,7 +460,7 @@ give under the previous paragraph, plus a right to possession of the
|
|||||||
Corresponding Source of the work from the predecessor in interest, if
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
You may not impose any further restrictions on the exercise of the
|
||||||
rights granted or affirmed under this License. For example, you may
|
rights granted or affirmed under this License. For example, you may
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
rights granted under this License, and you may not initiate litigation
|
rights granted under this License, and you may not initiate litigation
|
||||||
@ -464,14 +468,14 @@ rights granted under this License, and you may not initiate litigation
|
|||||||
any patent claim is infringed by making, using, selling, offering for
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
sale, or importing the Program or any portion of it.
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
#### 11. Patents.
|
11. Patents.
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
License of the Program or a work on which the Program is based. The
|
License of the Program or a work on which the Program is based. The
|
||||||
work thus licensed is called the contributor's "contributor version".
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims owned
|
A contributor's "essential patent claims" are all patent claims
|
||||||
or controlled by the contributor, whether already acquired or
|
owned or controlled by the contributor, whether already acquired or
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
by this License, of making, using, or selling its contributor version,
|
by this License, of making, using, or selling its contributor version,
|
||||||
but do not include claims that would be infringed only as a
|
but do not include claims that would be infringed only as a
|
||||||
@ -480,19 +484,19 @@ purposes of this definition, "control" includes the right to grant
|
|||||||
patent sublicenses in a manner consistent with the requirements of
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
this License.
|
this License.
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
patent license under the contributor's essential patent claims, to
|
patent license under the contributor's essential patent claims, to
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
propagate the contents of its contributor version.
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
In the following three paragraphs, a "patent license" is any express
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
(such as an express permission to practice a patent or covenant not to
|
(such as an express permission to practice a patent or covenant not to
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
party means to make such an agreement or commitment not to enforce a
|
party means to make such an agreement or commitment not to enforce a
|
||||||
patent against the party.
|
patent against the party.
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
and the Corresponding Source of the work is not available for anyone
|
and the Corresponding Source of the work is not available for anyone
|
||||||
to copy, free of charge and under the terms of this License, through a
|
to copy, free of charge and under the terms of this License, through a
|
||||||
publicly available network server or other readily accessible means,
|
publicly available network server or other readily accessible means,
|
||||||
@ -506,7 +510,7 @@ covered work in a country, or your recipient's use of the covered work
|
|||||||
in a country, would infringe one or more identifiable patents in that
|
in a country, would infringe one or more identifiable patents in that
|
||||||
country that you have reason to believe are valid.
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
If, pursuant to or in connection with a single transaction or
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
covered work, and grant a patent license to some of the parties
|
covered work, and grant a patent license to some of the parties
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
@ -514,41 +518,40 @@ or convey a specific copy of the covered work, then the patent license
|
|||||||
you grant is automatically extended to all recipients of the covered
|
you grant is automatically extended to all recipients of the covered
|
||||||
work and works based on it.
|
work and works based on it.
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within the
|
A patent license is "discriminatory" if it does not include within
|
||||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
the non-exercise of one or more of the rights that are specifically
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
granted under this License. You may not convey a covered work if you
|
specifically granted under this License. You may not convey a covered
|
||||||
are a party to an arrangement with a third party that is in the
|
work if you are a party to an arrangement with a third party that is
|
||||||
business of distributing software, under which you make payment to the
|
in the business of distributing software, under which you make payment
|
||||||
third party based on the extent of your activity of conveying the
|
to the third party based on the extent of your activity of conveying
|
||||||
work, and under which the third party grants, to any of the parties
|
the work, and under which the third party grants, to any of the
|
||||||
who would receive the covered work from you, a discriminatory patent
|
parties who would receive the covered work from you, a discriminatory
|
||||||
license (a) in connection with copies of the covered work conveyed by
|
patent license (a) in connection with copies of the covered work
|
||||||
you (or copies made from those copies), or (b) primarily for and in
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
connection with specific products or compilations that contain the
|
for and in connection with specific products or compilations that
|
||||||
covered work, unless you entered into that arrangement, or that patent
|
contain the covered work, unless you entered into that arrangement,
|
||||||
license was granted, prior to 28 March 2007.
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
any implied license or other defenses to infringement that may
|
any implied license or other defenses to infringement that may
|
||||||
otherwise be available to you under applicable patent law.
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
#### 12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
covered work so as to satisfy simultaneously your obligations under
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
this License and any other pertinent obligations, then as a
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
consequence you may not convey it at all. For example, if you agree to
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
terms that obligate you to collect a royalty for further conveying
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
from those to whom you convey the Program, the only way you could
|
the Program, the only way you could satisfy both those terms and this
|
||||||
satisfy both those terms and this License would be to refrain entirely
|
License would be to refrain entirely from conveying the Program.
|
||||||
from conveying the Program.
|
|
||||||
|
|
||||||
#### 13. Use with the GNU Affero General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
@ -557,78 +560,76 @@ but the special requirements of the GNU Affero General Public License,
|
|||||||
section 13, concerning interaction through a network will apply to the
|
section 13, concerning interaction through a network will apply to the
|
||||||
combination as such.
|
combination as such.
|
||||||
|
|
||||||
#### 14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
of the GNU General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
detail to address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
Each version is given a distinguishing version number. If the
|
||||||
specifies that a certain numbered version of the GNU General Public
|
Program specifies that a certain numbered version of the GNU General
|
||||||
License "or any later version" applies to it, you have the option of
|
Public License "or any later version" applies to it, you have the
|
||||||
following the terms and conditions either of that numbered version or
|
option of following the terms and conditions either of that numbered
|
||||||
of any later version published by the Free Software Foundation. If the
|
version or of any later version published by the Free Software
|
||||||
Program does not specify a version number of the GNU General Public
|
Foundation. If the Program does not specify a version number of the
|
||||||
License, you may choose any version ever published by the Free
|
GNU General Public License, you may choose any version ever published
|
||||||
Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future versions
|
If the Program specifies that a proxy can decide which future
|
||||||
of the GNU General Public License can be used, that proxy's public
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
statement of acceptance of a version permanently authorizes you to
|
public statement of acceptance of a version permanently authorizes you
|
||||||
choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
Later license versions may give you additional or different
|
||||||
permissions. However, no additional obligations are imposed on any
|
permissions. However, no additional obligations are imposed on any
|
||||||
author or copyright holder as a result of your choosing to follow a
|
author or copyright holder as a result of your choosing to follow a
|
||||||
later version.
|
later version.
|
||||||
|
|
||||||
#### 15. Disclaimer of Warranty.
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
CORRECTION.
|
|
||||||
|
|
||||||
#### 16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
SUCH DAMAGES.
|
||||||
|
|
||||||
#### 17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
above cannot be given local legal effect according to their terms,
|
above cannot be given local legal effect according to their terms,
|
||||||
reviewing courts shall apply local law that most closely approximates
|
reviewing courts shall apply local law that most closely approximates
|
||||||
an absolute waiver of all civil liability in connection with the
|
an absolute waiver of all civil liability in connection with the
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
### How to Apply These Terms to Your New Programs
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
If you develop a new program, and you want it to be of the greatest
|
||||||
possible use to the public, the best way to achieve this is to make it
|
possible use to the public, the best way to achieve this is to make it
|
||||||
free software which everyone can redistribute and change under these
|
free software which everyone can redistribute and change under these terms.
|
||||||
terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to
|
To do so, attach the following notices to the program. It is safest
|
||||||
attach them to the start of each source file to most effectively state
|
to attach them to the start of each source file to most effectively
|
||||||
the exclusion of warranty; and each file should have at least the
|
state the exclusion of warranty; and each file should have at least
|
||||||
"copyright" line and a pointer to where the full notice is found.
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
@ -644,12 +645,11 @@ the exclusion of warranty; and each file should have at least the
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
<program> Copyright (C) <year> <name of author>
|
||||||
@ -657,19 +657,18 @@ notice like this when it starts in an interactive mode:
|
|||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
The hypothetical commands \`show w' and \`show c' should show the
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
appropriate parts of the General Public License. Of course, your
|
parts of the General Public License. Of course, your program's commands
|
||||||
program's commands might be different; for a GUI interface, you would
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
institute, if any, to sign a "copyright disclaimer" for the program, if
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
necessary. For more information on this, and how to apply and follow
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
the GNU GPL, see <http://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your
|
The GNU General Public License does not permit incorporating your program
|
||||||
program into proprietary programs. If your program is a subroutine
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
library, you may consider it more useful to permit linking proprietary
|
may consider it more useful to permit linking proprietary applications with
|
||||||
applications with the library. If this is what you want to do, use the
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
GNU Lesser General Public License instead of this License. But first,
|
Public License instead of this License. But first, please read
|
||||||
please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user